[
  {
    "path": ".devcontainer/README.md",
    "content": "# AnythingLLM Development Container Setup\n\nWelcome to the AnythingLLM development container configuration, designed to create a seamless and feature-rich development environment for this project.\n\n<center><h1><b>PLEASE READ THIS</b></h1></center>\n\n## Prerequisites\n\n- [Docker](https://www.docker.com/get-started)\n- [Visual Studio Code](https://code.visualstudio.com/)\n- [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) VS Code extension\n\n## Features\n\n- **Base Image**: Built on `mcr.microsoft.com/devcontainers/javascript-node:1-18-bookworm`, thus Node.JS LTS v18.\n- **Additional Tools**: Includes `hadolint`, and essential apt-packages such as `curl`, `gnupg`, and more.\n- **Ports**: Configured to auto-forward ports `3000` (Frontend) and `3001` (Backend).\n- **Environment Variables**: Sets `NODE_ENV` to `development` and `ESLINT_USE_FLAT_CONFIG` to `true`.\n- **VS Code Extensions**: A suite of extensions such as `Prettier`, `Docker`, `ESLint`, and more are automatically installed. Please revise if you do not agree with any of these extensions. AI-powered extensions and time trackers are (for now) not included to avoid any privacy concerns, but you can install them later in your own environment.\n\n## Getting Started\n\n1. Using GitHub Codespaces. Just select to create a new workspace, and the devcontainer will be created for you.\n\n2. Using your Local VSCode (Release or Insiders). We suggest you first make a fork of the repo and then clone it to your local machine using VSCode tools. Then open the project folder in VSCode, which will prompt you to open the project in a devcontainer. Select yes, and the devcontainer will be created for you. If this does not happen, you can open the command palette and select \"Remote-Containers: Reopen in Container\".\n\n## On Creation:\n\nWhen the container is built for the first time, it will automatically run `yarn setup` to ensure everything is in place for the Collector, Server and Frontend. This command is expected to be automatically re-run if there is a content change on next reboot.\n\n## Work in the Container:\n\nOnce the container is up, be patient. Some extensions may complain because dependencies are still being installed, and in the Extensions tab, some may ask you to \"Reload\" the project. Don't do that yet. First, wait until all settle down for the first time. We suggest you create a new VSCode profile for this devcontainer, so any configuration and extensions you change, won't affect your default profile.\n\nChecklist:\n\n- [ ] The usual message asking you to start the Server and Frontend in different windows are now \"hidden\" in the building process of the devcontainer. Don't forget to do as suggested.\n- [ ] Open a JavaScript file, for example \"server/index.js\" and check if `eslint` is working. It will complain that `'err' is defined but never used.`. This means it is working.\n- [ ] Open a React File, for example, \"frontend/src/main.jsx,\" and check if `eslint` complains about `Fast refresh only works when a file has exports. Move your component(s) to a separate file.`. Again, it means `eslint` is working. Now check at the status bar if the `Prettier` has a double checkmark :heavy_check_mark: (double). It means Prettier is working. You will see a nice extension `Formatting:`:heavy_check_mark: that can be used to disable the `Format on Save` feature temporarily.\n- [ ] Check if, on the left pane, you have the NPM Scripts (this may be disabled; look at the \"Explorer\" tree-dots up-right). There will be scripts inside the `package.json` files. You will basically need to run the `dev:collector`, `dev:server` and the `dev:frontend` in this order. When the frontend finishes starting, a window browser will open **inside** the VSCode. Still, you can open it outside.\n\n:warning: **Important for all developers** :warning:\n\n- [ ] When you are using the `NODE_ENV=development` the server will not store the configurations you set for security reasons. Please set the proper config on file `.env.development`. The side-effect if you don't, everytime you restart the server, you will be sent to the \"Onboarding\" page again.\n\n**Note when using GitHub Codespaces**\n\n- [ ] When running the \"Server\" for the first time, it will automatically configure its port to be publicly accessible by default, as this is required for the front end to reach the server backend. To know more, read the content of the `.env` file on the frontend folder about this, and if any issues occur, make sure to manually set the port \"Visibility\" of the \"Server\" is set to \"Public\" if needed. Again, this is only needed for developing on GitHub Codespaces.\n\n\n**For the Collector:**\n\n- [x] In the past, the Collector dwelled within the Python domain, but now it has journeyed to the splendid realm of Node.JS. Consequently, the configuration complexities of bygone versions are no longer a concern.\n\n### Now it is ready to start\n\nIn the status bar you will see three shortcuts names `Collector`, `Server` and `Frontend`. Just click-and-wait on that order (don't forget to set the Server port 3001 to Public if you are using GH Codespaces **_before_** starting the Frontend).\n\nNow you can enjoy your time developing instead of reconfiguring everything.\n\n## Debugging with the devcontainers\n\n### For debugging the collector, server and frontend\n\nFirst, make sure the built-in extension (ms-vscode.js-debug) is active (I don't know why it would not be, but just in case). If you want, you can install the nightly version (ms-vscode.js-debug-nightly)\n\nThen, in the \"Run and Debug\" tab (Ctrl+shift+D), you can select on the menu:\n\n- Collector debug. This will start the collector in debug mode and attach the debugger. Works very well.\n- Server debug. This will start the server in debug mode and attach the debugger. Works very well.\n- Frontend debug. This will start the frontend in debug mode and attach the debugger. I am still struggling with this one. I don't know if VSCode can handle the .jsx files seamlessly as the pure .js on the server. Maybe there is a need for a particular configuration for Vite or React. Anyway, it starts. Another two configurations launch Chrome and Edge, and I think we could add breakpoints on .jsx files somehow. The best scenario would be always to use the embedded browser. WIP.\n\nPlease leave comments on the Issues tab or the [![](https://img.shields.io/discord/1114740394715004990?logo=Discord&logoColor=white&label=Discord&labelColor=%235568ee&color=%2355A2DD&link=https%3A%2F%2Fdiscord.gg%2F6UyHPeGZAC)](\"https://discord.gg/6UyHPeGZAC\")\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the\n// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node\n{\n  \"name\": \"Node.js\",\n  // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile\n  // \"build\": {\n  //   \"args\": {\n  //     \"ARG_UID\": \"1000\",\n  //     \"ARG_GID\": \"1000\"\n  //   },\n  //   \"dockerfile\": \"Dockerfile\"\n  // },\n  // \"containerUser\": \"anythingllm\",\n  // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile\n  \"image\": \"mcr.microsoft.com/devcontainers/javascript-node:1-18-bookworm\",\n\n  \"forwardPorts\": [3001, 3000],\n\n  // Features to add to the dev container. More info: https://containers.dev/features.\n  \"features\": {\n    // Docker very useful linter\n    \"ghcr.io/dhoeric/features/hadolint:1\": {\n      \"version\": \"latest\"\n    },\n    // Terraform support\n    \"ghcr.io/devcontainers/features/terraform:1\": {},\n    // Just a wrap to install needed packages\n    \"ghcr.io/devcontainers-contrib/features/apt-packages:1\": {\n      // Dependencies copied from ../docker/Dockerfile plus some dev stuff\n      \"packages\": [\n        \"build-essential\",\n        \"ca-certificates\",\n        \"curl\",\n        \"ffmpeg\",\n        \"fonts-liberation\",\n        \"git\",\n        \"gnupg\",\n        \"htop\",\n        \"less\",\n        \"libappindicator1\",\n        \"libasound2\",\n        \"libatk-bridge2.0-0\",\n        \"libatk1.0-0\",\n        \"libc6\",\n        \"libcairo2\",\n        \"libcups2\",\n        \"libdbus-1-3\",\n        \"libexpat1\",\n        \"libfontconfig1\",\n        \"libgbm1\",\n        \"libgcc1\",\n        \"libgfortran5\",\n        \"libglib2.0-0\",\n        \"libgtk-3-0\",\n        \"libnspr4\",\n        \"libnss3\",\n        \"libpango-1.0-0\",\n        \"libpangocairo-1.0-0\",\n        \"libstdc++6\",\n        \"libx11-6\",\n        \"libx11-xcb1\",\n        \"libxcb1\",\n        \"libxcomposite1\",\n        \"libxcursor1\",\n        \"libxdamage1\",\n        \"libxext6\",\n        \"libxfixes3\",\n        \"libxi6\",\n        \"libxrandr2\",\n        \"libxrender1\",\n        \"libxss1\",\n        \"libxtst6\",\n        \"locales\",\n        \"lsb-release\",\n        \"procps\",\n        \"tzdata\",\n        \"wget\",\n        \"xdg-utils\"\n      ]\n    }\n  },\n  \"updateContentCommand\": \"cd server && yarn && cd ../collector && PUPPETEER_DOWNLOAD_BASE_URL=https://storage.googleapis.com/chrome-for-testing-public yarn && cd ../frontend && yarn && cd .. && yarn setup:envs && yarn prisma:setup && echo \\\"Please run yarn dev:server, yarn dev:collector, and yarn dev:frontend in separate terminal tabs.\\\"\",\n  // Use 'postCreateCommand' to run commands after the container is created.\n  // This configures VITE for github codespaces and installs gh cli\n  \"postCreateCommand\": \"if [ \\\"${CODESPACES}\\\" = \\\"true\\\" ]; then echo 'VITE_API_BASE=\\\"https://$CODESPACE_NAME-3001.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN/api\\\"' > ./frontend/.env && (type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) && sudo mkdir -p -m 755 /etc/apt/keyrings && wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg && echo \\\"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main\\\" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null && sudo apt update && sudo apt install gh -y; fi\",\n  \"portsAttributes\": {\n    \"3001\": {\n      \"label\": \"Backend\",\n      \"onAutoForward\": \"notify\"\n    },\n    \"3000\": {\n      \"label\": \"Frontend\",\n      \"onAutoForward\": \"openPreview\"\n    }\n  },\n  \"capAdd\": [\n    \"SYS_ADMIN\" // needed for puppeteer using headless chrome in sandbox\n  ],\n  \"remoteEnv\": {\n    \"NODE_ENV\": \"development\",\n    \"ESLINT_USE_FLAT_CONFIG\": \"true\",\n    \"ANYTHING_LLM_RUNTIME\": \"docker\"\n  },\n  // \"initializeCommand\": \"echo Initialize....\",\n  \"shutdownAction\": \"stopContainer\",\n  // Configure tool-specific properties.\n  \"customizations\": {\n    \"codespaces\": {\n      \"openFiles\": [\n        \"README.md\",\n        \".devcontainer/README.md\"\n      ]\n    },\n    \"vscode\": {\n      \"openFiles\": [\n        \"README.md\",\n        \".devcontainer/README.md\"\n      ],\n      \"extensions\": [\n        \"bierner.github-markdown-preview\",\n        \"bradlc.vscode-tailwindcss\",\n        \"dbaeumer.vscode-eslint\",\n        \"editorconfig.editorconfig\",\n        \"esbenp.prettier-vscode\",\n        \"exiasr.hadolint\",\n        \"flowtype.flow-for-vscode\",\n        \"gamunu.vscode-yarn\",\n        \"hashicorp.terraform\",\n        \"mariusschulz.yarn-lock-syntax\",\n        \"ms-azuretools.vscode-docker\",\n        \"streetsidesoftware.code-spell-checker\",\n        \"actboy168.tasks\",\n        \"tombonnike.vscode-status-bar-format-toggle\",\n        \"ms-vscode.js-debug\"\n      ],\n      \"settings\": {\n        \"[css]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[dockercompose]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[dockerfile]\": {\n          \"editor.defaultFormatter\": \"ms-azuretools.vscode-docker\"\n        },\n        \"[html]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[javascript]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[javascriptreact]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[json]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[jsonc]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[markdown]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[postcss]\": {\n          \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n        },\n        \"[toml]\": {\n          \"editor.defaultFormatter\": \"tamasfe.even-better-toml\"\n        },\n        \"eslint.debug\": true,\n        \"eslint.enable\": true,\n        \"eslint.experimental.useFlatConfig\": true,\n        \"eslint.run\": \"onSave\",\n        \"files.associations\": {\n          \".*ignore\": \"ignore\",\n          \".editorconfig\": \"editorconfig\",\n          \".env*\": \"properties\",\n          \".flowconfig\": \"ini\",\n          \".prettierrc\": \"json\",\n          \"*.css\": \"tailwindcss\",\n          \"*.md\": \"markdown\",\n          \"*.sh\": \"shellscript\",\n          \"docker-compose.*\": \"dockercompose\",\n          \"Dockerfile*\": \"dockerfile\",\n          \"yarn.lock\": \"yarnlock\"\n        },\n        \"javascript.format.enable\": false,\n        \"javascript.inlayHints.enumMemberValues.enabled\": true,\n        \"javascript.inlayHints.functionLikeReturnTypes.enabled\": true,\n        \"javascript.inlayHints.parameterTypes.enabled\": true,\n        \"javascript.inlayHints.variableTypes.enabled\": true,\n        \"js/ts.implicitProjectConfig.module\": \"CommonJS\",\n        \"json.format.enable\": false,\n        \"json.schemaDownload.enable\": true,\n        \"npm.autoDetect\": \"on\",\n        \"npm.packageManager\": \"yarn\",\n        \"prettier.useEditorConfig\": false,\n        \"tailwindCSS.files.exclude\": [\n          \"**/.git/**\",\n          \"**/node_modules/**\",\n          \"**/.hg/**\",\n          \"**/.svn/**\",\n          \"**/dist/**\"\n        ],\n        \"typescript.validate.enable\": false,\n        \"workbench.editorAssociations\": {\n          \"*.md\": \"vscode.markdown.preview.editor\"\n        }\n      }\n    }\n  }\n  // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.\n  // \"remoteUser\": \"root\"\n}\n"
  },
  {
    "path": ".dockerignore",
    "content": "**/server/utils/agents/aibitat/example/**\n**/server/storage/documents/**\n**/server/storage/vector-cache/**\n**/server/storage/*.db\n**/server/storage/lancedb\n**/collector/hotdir/**\n**/collector/outputs/**\n**/node_modules/\n**/dist/\n**/v-env/\n**/__pycache__/\n**/.env\n**/.env.*\n**/bundleinspector.html\n**/tmp/**\n**/.log\n!docker/.env.example\n!frontend/.env.production"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: https://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n[*]\n# Non-configurable Prettier behaviors\ncharset = utf-8\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n# Configurable Prettier behaviors\n# (change these if your Prettier config differs)\nend_of_line = lf\nindent_style = space\nindent_size = 2\nmax_line_length = 80\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: Mintplex-Labs"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/01_bug.yml",
    "content": "name: 🐛 Bug Report\ndescription: File a bug report for AnythingLLM\ntitle: \"[BUG]: \"\nlabels: [possible bug]\nbody:\n  - type: markdown\n    attributes:\n      value: | \n        Use this template to file a bug report for AnythingLLM. Please be as descriptive as possible to allow everyone to replicate and solve your issue.\n  - type: dropdown\n    id: runtime\n    attributes:\n      label: How are you running AnythingLLM?\n      description: AnythingLLM can be run in many environments, pick the one that best represents where you encounter the bug.\n      options:\n        - Docker (local)\n        - Docker (remote machine)\n        - Local development\n        - AnythingLLM desktop app\n        - All versions\n        - Not listed\n      default: 0\n    validations:\n      required: true\n\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: What happened?\n      description: Also tell us, what did you expect to happen?\n    validations:\n      required: true\n\n  - type: textarea\n    id: reproduction\n    attributes:\n      label: Are there known steps to reproduce?\n      description: |\n        Let us know how to reproduce the bug and we may be able to fix it more\n        quickly. This is not required, but it is helpful.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/02_feature.yml",
    "content": "name: ✨ New Feature suggestion\ndescription: Suggest a new feature for AnythingLLM!\ntitle: \"[FEAT]: \"\nlabels: [enhancement, feature request]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Share a new idea for a feature or improvement. Be sure to search existing\n        issues first to avoid duplicates.\n\n  - type: textarea\n    id: description\n    attributes:\n      label: What would you like to see?\n      description: |\n        Describe the feature and why it would be useful to your use-case as well as others.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/03_documentation.yml",
    "content": "name: 📚 Documentation improvement\ntitle: \"[DOCS]: \"\ndescription: Report an issue or problem with the documentation.\nlabels: [documentation]\n\nbody:\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: Describe the issue with the documentation that is giving you trouble or causing confusion.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: 🧑‍🤝‍🧑 Community Discord\n    url: https://discord.gg/6UyHPeGZAC\n    about: Interact with the Mintplex Labs community here by asking for help, discussing and more!\n"
  },
  {
    "path": ".github/workflows/build-and-push-image-semver.yaml",
    "content": "name: Publish AnythingLLM Docker image on Release (amd64 & arm64)\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  release:\n    types: [published]\n\njobs:\n  push_multi_platform_to_registries:\n    name: Push Docker multi-platform image to multiple registries\n    runs-on: ubuntu-22.04-arm\n    permissions:\n      packages: write\n      contents: read\n    steps:\n      - name: Check out the repo\n        uses: actions/checkout@v4\n\n      - name: Check if DockerHub build needed\n        shell: bash\n        run: |\n          # Check if the secret for USERNAME is set (don't even check for the password)\n          if [[ -z \"${{ secrets.DOCKER_USERNAME }}\" ]]; then\n            echo \"DockerHub build not needed\"\n            echo \"enabled=false\" >> $GITHUB_OUTPUT\n          else\n            echo \"DockerHub build needed\"\n            echo \"enabled=true\" >> $GITHUB_OUTPUT\n          fi\n        id: dockerhub\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n        with:\n          version: v0.22.0\n      \n      - name: Log in to Docker Hub\n        uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a\n        # Only login to the Docker Hub if the repo is mintplex/anythingllm, to allow for forks to build on GHCR\n        if: steps.dockerhub.outputs.enabled == 'true' \n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n      \n      - name: Log in to the Container registry\n        uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      \n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7\n        with:\n          images: |\n            ${{ steps.dockerhub.outputs.enabled == 'true' && 'mintplexlabs/anythingllm' || '' }}\n            ghcr.io/${{ github.repository }}\n          tags: |\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n\n      - name: Build and push multi-platform Docker image\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: ./docker/Dockerfile\n          push: true\n          sbom: true\n          provenance: mode=max\n          platforms: linux/amd64,linux/arm64\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n      \n      # For Docker scout there are some intermediary reported CVEs which exists outside\n      # of execution content or are unreachable by an attacker but exist in image.\n      # We create VEX files for these so they don't show in scout summary. \n      - name: Collect known and verified CVE exceptions\n        id: cve-list\n        run: |\n          # Collect CVEs from filenames in vex folder\n          CVE_NAMES=\"\"\n          for file in ./docker/vex/*.vex.json; do\n            [ -e \"$file\" ] || continue\n            filename=$(basename \"$file\")\n            stripped_filename=${filename%.vex.json}\n            CVE_NAMES+=\" $stripped_filename\"\n          done\n          echo \"CVE_EXCEPTIONS=$CVE_NAMES\" >> $GITHUB_OUTPUT\n        shell: bash\n\n      # About VEX attestations https://docs.docker.com/scout/explore/exceptions/\n      # Justifications https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md#status-justifications\n      - name: Add VEX attestations\n        env:\n          CVE_EXCEPTIONS: ${{ steps.cve-list.outputs.CVE_EXCEPTIONS }}\n        run: |\n          echo $CVE_EXCEPTIONS\n          curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s --\n          for cve in $CVE_EXCEPTIONS; do\n            for tag in \"${{ join(fromJSON(steps.meta.outputs.json).tags, ' ') }}\"; do\n              echo \"Attaching VEX exception $cve to $tag\"\n              docker scout attestation add \\\n              --file \"./docker/vex/$cve.vex.json\" \\\n              --predicate-type https://openvex.dev/ns/v0.2.0 \\\n              $tag\n            done\n          done\n        shell: bash\n"
  },
  {
    "path": ".github/workflows/build-and-push-image.yaml",
    "content": "# This GitHub action is for publishing of the primary image for AnythingLLM\n# It will publish a linux/amd64 and linux/arm64 image at the same time\n# This file should ONLY BY USED FOR `master` BRANCH. \n# TODO: GitHub now has an ubuntu-24.04-arm64 runner, but we still need\n# to use QEMU to build the arm64 image because Chromium is not available for Linux arm64\n# so builds will still fail, or fail much more often. Its inconsistent and frustrating.\nname: Publish AnythingLLM Primary Docker image (amd64/arm64)\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  push:\n    branches: ['master'] # master branch only. Do not modify.\n    paths-ignore:\n      - '**.md'\n      - '.gitmodules'\n      - 'cloud-deployments/**/*'\n      - 'images/**/*'\n      - '.vscode/**/*'\n      - '**/.env.example'\n      - '.github/ISSUE_TEMPLATE/**/*'\n      - '.devcontainer/**/*'\n      - 'embed/**/*' # Embed is submodule\n      - 'browser-extension/**/*' # Chrome extension is submodule\n      - 'server/utils/agents/aibitat/example/**/*' # Do not push new image for local dev testing of new aibitat images.\n      - 'extras/**/*' # Extra is just for news and other local content.\n\njobs:\n  push_multi_platform_to_registries:\n    name: Push Docker multi-platform image to multiple registries\n    runs-on: ubuntu-22.04-arm\n    permissions:\n      packages: write\n      contents: read\n    steps:\n      - name: Check out the repo\n        uses: actions/checkout@v4\n\n      - name: Check if DockerHub build needed\n        shell: bash\n        run: |\n          # Check if the secret for USERNAME is set (don't even check for the password)\n          if [[ -z \"${{ secrets.DOCKER_USERNAME }}\" ]]; then\n            echo \"DockerHub build not needed\"\n            echo \"enabled=false\" >> $GITHUB_OUTPUT\n          else\n            echo \"DockerHub build needed\"\n            echo \"enabled=true\" >> $GITHUB_OUTPUT\n          fi\n        id: dockerhub\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n        with:\n          version: v0.22.0\n      \n      - name: Log in to Docker Hub\n        uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a\n        # Only login to the Docker Hub if the repo is mintplex/anythingllm, to allow for forks to build on GHCR\n        if: steps.dockerhub.outputs.enabled == 'true' \n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKER_PASSWORD }}\n      \n      - name: Log in to the Container registry\n        uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n      \n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7\n        with:\n          images: |\n            ${{ steps.dockerhub.outputs.enabled == 'true' && 'mintplexlabs/anythingllm' || '' }}\n            ghcr.io/${{ github.repository }}\n          tags: |\n            type=raw,value=latest,enable={{is_default_branch}}\n            type=ref,event=branch\n            type=ref,event=tag\n            type=ref,event=pr\n\n      - name: Build and push multi-platform Docker image\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: ./docker/Dockerfile\n          push: true\n          sbom: true\n          provenance: mode=max\n          platforms: linux/amd64,linux/arm64\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n      \n      # For Docker scout there are some intermediary reported CVEs which exists outside\n      # of execution content or are unreachable by an attacker but exist in image.\n      # We create VEX files for these so they don't show in scout summary. \n      - name: Collect known and verified CVE exceptions\n        id: cve-list\n        run: |\n          # Collect CVEs from filenames in vex folder\n          CVE_NAMES=\"\"\n          for file in ./docker/vex/*.vex.json; do\n            [ -e \"$file\" ] || continue\n            filename=$(basename \"$file\")\n            stripped_filename=${filename%.vex.json}\n            CVE_NAMES+=\" $stripped_filename\"\n          done\n          echo \"CVE_EXCEPTIONS=$CVE_NAMES\" >> $GITHUB_OUTPUT\n        shell: bash\n\n      # About VEX attestations https://docs.docker.com/scout/explore/exceptions/\n      # Justifications https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md#status-justifications\n      - name: Add VEX attestations\n        env:\n          CVE_EXCEPTIONS: ${{ steps.cve-list.outputs.CVE_EXCEPTIONS }}\n        run: |\n          echo $CVE_EXCEPTIONS\n          curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s --\n          for cve in $CVE_EXCEPTIONS; do\n            for tag in \"${{ join(fromJSON(steps.meta.outputs.json).tags, ' ') }}\"; do\n              echo \"Attaching VEX exception $cve to $tag\"\n              docker scout attestation add \\\n              --file \"./docker/vex/$cve.vex.json\" \\\n              --predicate-type https://openvex.dev/ns/v0.2.0 \\\n              $tag\n            done\n          done\n        shell: bash"
  },
  {
    "path": ".github/workflows/build-qa-tag.yaml",
    "content": "# Builds a QA GHCR image for a PR when the \"PR: Ready for QA\" label is present.\n# Triggers on:\n#   - \"PR: Ready for QA\" label added to a PR\n#   - New commits pushed to a PR that already has the label will trigger a new build\nname: Build QA GHCR Image\n\non:\n  pull_request:\n    types: [labeled, synchronize]\n    paths-ignore:\n      - \"**.md\"\n      - \".gitmodules\"\n      - \"cloud-deployments/**/*\"\n      - \"images/**/*\"\n      - \".vscode/**/*\"\n      - \"**/.env.example\"\n      - \".github/ISSUE_TEMPLATE/**/*\"\n      - \".devcontainer/**/*\"\n      - \"embed/**/*\"\n      - \"browser-extension/**/*\"\n      - \"extras/**/*\"\n\nconcurrency:\n  group: qa-build-pr-${{ github.event.pull_request.number }}\n  cancel-in-progress: true\n\njobs:\n  build:\n    name: Build and push QA image for PR\n    runs-on: ubuntu-22.04-arm\n    # Run when labeled with \"PR: Ready for QA\"\n    if: >-\n      ${{ contains(github.event.pull_request.labels.*.name, 'PR: Ready for QA') }}\n    permissions:\n      packages: write\n      contents: read\n      pull-requests: write\n    steps:\n      - name: Check out the repo\n        uses: actions/checkout@v4\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n        with:\n          version: v0.22.0\n\n      - name: Log in to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Set lowercase repository owner\n        run: echo \"REPO_OWNER_LC=${GITHUB_REPOSITORY_OWNER,,}\" >> $GITHUB_ENV\n\n      - name: Build and push\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: ./docker/Dockerfile\n          push: true\n          sbom: true\n          provenance: mode=max\n          platforms: linux/arm64\n          tags: ghcr.io/${{ env.REPO_OWNER_LC }}/${{ github.event.repository.name }}:pr-${{ github.event.pull_request.number }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/check-package-versions.yaml",
    "content": "# This GitHub action is for checking the versions of the packages in the project.\n# Any package that is present in both the `server` and `collector` package.json file\n# is checked to ensure that they are the same version.\nname: Check package versions\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  pull_request:\n    types: [opened, synchronize, reopened]\n    paths:\n      - \"server/package.json\"\n      - \"collector/package.json\"\n\njobs:\n  run-script:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v2\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: '18'\n\n      - name: Run verifyPackageVersions.mjs script\n        run: |\n          cd extras/scripts\n          node verifyPackageVersions.mjs\n\n      - name: Fail job on error\n        if: failure()\n        run: exit 1\n"
  },
  {
    "path": ".github/workflows/check-translations.yaml",
    "content": "# This GitHub action is for validation of all languages which translations are offered for\n# in the locales folder in `frontend/src`. All languages are compared to the EN translation\n# schema since that is the fallback language setting. This workflow will run on all PRs that\n# modify any files in the translation directory\nname: Verify translations files\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  pull_request:\n    types: [opened, synchronize, reopened]\n    paths:\n      - \"frontend/src/locales/**.js\"\n\njobs:\n  run-script:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v2\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: '18'\n\n      - name: Run verifyTranslations.mjs script\n        run: |\n          cd frontend/src/locales\n          node verifyTranslations.mjs\n\n      - name: Fail job on error\n        if: failure()\n        run: exit 1\n"
  },
  {
    "path": ".github/workflows/cleanup-qa-tag.yaml",
    "content": "# Cleans up the GHCR image tag when the PR is closed or the \"PR: Ready for QA\" label is removed.\nname: Cleanup QA GHCR Image\n\non:\n  pull_request:\n    types: [closed, unlabeled]\n  workflow_dispatch:\n    inputs:\n      pr_number:\n        description: 'PR number to clean up (e.g., 123)'\n        required: true\n\njobs:\n  cleanup-manual:\n    name: Delete QA GHCR image tag (manual)\n    runs-on: ubuntu-latest\n    if: github.event_name == 'workflow_dispatch'\n    permissions:\n      packages: write\n    steps:\n      - name: Delete PR tag from GHCR\n        env:\n          GH_TOKEN: ${{ secrets.ALLM_RW_PACKAGES }}\n          PR_NUMBER: ${{ inputs.pr_number }}\n        run: |\n          # Must use lowercase - packages are published with lowercase owner\n          ORG_LC=\"${GITHUB_REPOSITORY_OWNER,,}\"\n          REPO_LC=\"${GITHUB_REPOSITORY#*/}\"\n          REPO_LC=\"${REPO_LC,,}\"\n          \n          echo \"Looking for tag: pr-${PR_NUMBER}\"\n          echo \"Package: /orgs/${ORG_LC}/packages/container/${REPO_LC}/versions\"\n          \n          VERSION_ID=$(gh api \\\n            -H \"Accept: application/vnd.github+json\" \\\n            --paginate \\\n            \"/orgs/${ORG_LC}/packages/container/${REPO_LC}/versions\" \\\n            --jq \".[] | select(.metadata.container.tags[] == \\\"pr-${PR_NUMBER}\\\") | .id\")\n          \n          if [ -n \"$VERSION_ID\" ]; then\n            echo \"Deleting package version $VERSION_ID (tag: pr-${PR_NUMBER})\"\n            gh api \\\n              --method DELETE \\\n              -H \"Accept: application/vnd.github+json\" \\\n              \"/orgs/${ORG_LC}/packages/container/${REPO_LC}/versions/$VERSION_ID\"\n          else\n            echo \"No package found with tag pr-${PR_NUMBER}, skipping cleanup\"\n          fi\n\n  cleanup-auto:\n    name: Delete QA GHCR image tag (auto)\n    runs-on: ubuntu-latest\n    if: >-\n      (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'PR: Ready for QA')) ||\n      (github.event.action == 'unlabeled' && github.event.label.name == 'PR: Ready for QA')\n    permissions:\n      packages: write\n    steps:\n      - name: Delete PR tag from GHCR\n        env:\n          GH_TOKEN: ${{ secrets.ALLM_RW_PACKAGES }}\n          PR_NUMBER: ${{ github.event.pull_request.number }}\n        run: |\n          # Must use lowercase - packages are published with lowercase owner\n          ORG_LC=\"${GITHUB_REPOSITORY_OWNER,,}\"\n          REPO_LC=\"${GITHUB_REPOSITORY#*/}\"\n          REPO_LC=\"${REPO_LC,,}\"\n          \n          VERSION_ID=$(gh api \\\n            -H \"Accept: application/vnd.github+json\" \\\n            --paginate \\\n            \"/orgs/${ORG_LC}/packages/container/${REPO_LC}/versions\" \\\n            --jq \".[] | select(.metadata.container.tags[] == \\\"pr-${PR_NUMBER}\\\") | .id\" \\\n            2>/dev/null || true)\n          \n          if [ -n \"$VERSION_ID\" ]; then\n            echo \"Deleting package version $VERSION_ID (tag: pr-${PR_NUMBER})\"\n            gh api \\\n              --method DELETE \\\n              -H \"Accept: application/vnd.github+json\" \\\n              \"/orgs/${ORG_LC}/packages/container/${REPO_LC}/versions/$VERSION_ID\"\n          else\n            echo \"No package found with tag pr-${PR_NUMBER}, skipping cleanup\"\n          fi\n"
  },
  {
    "path": ".github/workflows/lint.yaml",
    "content": "name: Lint\n\nconcurrency:\n  group: lint-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  pull_request:\n    types: [opened, synchronize, reopened]\n    paths:\n      - \"server/**/*.js\"\n      - \"server/eslint.config.mjs\"\n      - \"collector/**/*.js\"\n      - \"collector/eslint.config.mjs\"\n      - \"frontend/src/**/*.js\"\n      - \"frontend/src/**/*.jsx\"\n      - \"frontend/eslint.config.js\"\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: \"18\"\n\n      - name: Cache server dependencies\n        uses: actions/cache@v4\n        with:\n          path: server/node_modules\n          key: ${{ runner.os }}-yarn-server-${{ hashFiles('server/yarn.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-yarn-server-\n\n      - name: Cache frontend dependencies\n        uses: actions/cache@v4\n        with:\n          path: frontend/node_modules\n          key: ${{ runner.os }}-yarn-frontend-${{ hashFiles('frontend/yarn.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-yarn-frontend-\n\n      - name: Cache collector dependencies\n        uses: actions/cache@v4\n        with:\n          path: collector/node_modules\n          key: ${{ runner.os }}-yarn-collector-${{ hashFiles('collector/yarn.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-yarn-collector-\n\n      - name: Install server dependencies\n        run: cd server && yarn install --frozen-lockfile\n\n      - name: Install frontend dependencies\n        run: cd frontend && yarn install --frozen-lockfile\n\n      - name: Install collector dependencies\n        run: cd collector && yarn install --frozen-lockfile\n        env:\n          PUPPETEER_SKIP_DOWNLOAD: \"true\"\n          SHARP_IGNORE_GLOBAL_LIBVIPS: \"true\"\n\n      - name: Lint server\n        run: cd server && yarn lint:check\n\n      - name: Lint frontend\n        run: cd frontend && yarn lint:check\n\n      - name: Lint collector\n        run: cd collector && yarn lint:check\n"
  },
  {
    "path": ".github/workflows/run-tests.yaml",
    "content": "name: Run backend tests\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\non:\n  pull_request:\n    types: [opened, synchronize, reopened]\n    paths:\n      - \"server/**.js\"\n      - \"collector/**.js\"\n\njobs:\n  run-script:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v2\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: '18'\n\n      - name: Cache root dependencies\n        uses: actions/cache@v3\n        with:\n          path: |\n            node_modules\n            ~/.cache/yarn\n          key: ${{ runner.os }}-yarn-root-${{ hashFiles('**/yarn.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-yarn-root-\n\n      - name: Cache server dependencies\n        uses: actions/cache@v3\n        with:\n          path: |\n            server/node_modules\n            ~/.cache/yarn\n          key: ${{ runner.os }}-yarn-server-${{ hashFiles('server/yarn.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-yarn-server-\n\n      - name: Cache collector dependencies\n        uses: actions/cache@v3\n        with:\n          path: |\n            collector/node_modules\n            ~/.cache/yarn\n          key: ${{ runner.os }}-yarn-collector-${{ hashFiles('collector/yarn.lock') }}\n          restore-keys: |\n            ${{ runner.os }}-yarn-collector-\n\n      - name: Install root dependencies\n        if: steps.cache-root.outputs.cache-hit != 'true'\n        run: yarn install --frozen-lockfile\n\n      - name: Install server dependencies\n        if: steps.cache-server.outputs.cache-hit != 'true'\n        run: cd server && yarn install --frozen-lockfile\n\n      - name: Install collector dependencies\n        if: steps.cache-collector.outputs.cache-hit != 'true'\n        run: cd collector && yarn install --frozen-lockfile\n        env:\n          PUPPETEER_SKIP_DOWNLOAD: \"true\"\n          SHARP_IGNORE_GLOBAL_LIBVIPS: \"true\"\n\n      - name: Setup environment and Prisma\n        run: yarn setup:envs && yarn prisma:setup\n\n      - name: Run test suites\n        run: yarn test\n\n      - name: Fail job on error\n        if: failure()\n        run: exit 1\n"
  },
  {
    "path": ".github/workflows/sponsors.yaml",
    "content": "name: Generate Sponsors README\n\non:\n  schedule:\n    - cron: \"0 12 * * 3\" # Run every Wednesday at 12:00 PM UTC\n\npermissions:\n  contents: write\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout 🛎️\n        uses: actions/checkout@v2\n\n      - name: Generate All Sponsors README\n        id: generate-all-sponsors\n        uses: JamesIves/github-sponsors-readme-action@v1\n        with:\n          token: ${{ secrets.SPONSOR_PAT }}\n          file: 'README.md'\n          organization: true\n          active-only: false\n          marker: 'all-sponsors'\n\n      - name: Commit and Push 🚀\n        uses: stefanzweifel/git-auto-commit-action@v5\n        id: auto-commit-action\n        with:\n          commit_message: 'Update Sponsors README'\n          file_pattern: 'README.md'\n\n      - name: Generate PR if changes detected\n        uses: peter-evans/create-pull-request@v7\n        if: steps.auto-commit-action.outputs.files_changed == 'true'\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          title: 'Update Sponsors README'\n          branch: 'chore/update-sponsors'\n          base: 'master'\n          draft: false\n          reviewers: 'timothycarambat'\n          assignees: 'timothycarambat'\n          maintainer-can-modify: true"
  },
  {
    "path": ".gitignore",
    "content": "v-env\n.env\n!.env.example\n\nnode_modules\n__pycache__\nv-env\n.DS_Store\naws_cf_deploy_anything_llm.json\nyarn.lock\n*.bak\n.idea\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"browser-extension\"]\n\tpath = browser-extension\n\turl = https://github.com/Mintplex-Labs/anythingllm-extension.git\n[submodule \"embed\"]\n\tpath = embed\n\turl = https://github.com/Mintplex-Labs/anythingllm-embed.git\n\tbranch = main\n"
  },
  {
    "path": ".hadolint.yaml",
    "content": "failure-threshold: warning\nignored:\n  - DL3008\n  - DL3013\nformat: tty\ntrustedRegistries:\n  - docker.io\n  - gcr.io\n"
  },
  {
    "path": ".nvmrc",
    "content": "v18.18.0\n"
  },
  {
    "path": ".prettierignore",
    "content": "# defaults\n**/.git\n**/.svn\n**/.hg\n**/node_modules\n\n#frontend\nfrontend/bundleinspector.html\n**/dist\n\n#server\nserver/swagger/openapi.json\nserver/**/*.mjs\n\n#embed\n**/static/**\nembed/src/utils/chat/hljs.js\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"endOfLine\": \"lf\",\n  \"semi\": true,\n  \"singleQuote\": false,\n  \"printWidth\": 80,\n  \"trailingComma\": \"es5\",\n  \"bracketSpacing\": true,\n  \"bracketSameLine\": false,\n  \"overrides\": [\n    {\n      \"files\": [\"*.js\", \"*.mjs\", \"*.jsx\"],\n      \"options\": {\n        \"parser\": \"flow\",\n        \"arrowParens\": \"always\"\n      }\n    },\n    {\n      \"files\": [\"*.config.js\"],\n      \"options\": {\n        \"semi\": false,\n        \"parser\": \"flow\",\n        \"trailingComma\": \"none\"\n      }\n    },\n    {\n      \"files\": \"*.html\",\n      \"options\": {\n        \"bracketSameLine\": true\n      }\n    },\n    {\n      \"files\": \".prettierrc\",\n      \"options\": { \"parser\": \"json\" }\n    }\n  ]\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  // Use IntelliSense to learn about possible attributes.\n  // Hover to view descriptions of existing attributes.\n  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Collector debug\",\n      \"request\": \"launch\",\n      \"cwd\": \"${workspaceFolder}/collector\",\n      \"env\": {\n        \"NODE_ENV\": \"development\"\n      },\n      \"runtimeArgs\": [\n        \"index.js\"\n      ],\n      // not using yarn/nodemon because it doesn't work with breakpoints\n      // \"runtimeExecutable\": \"yarn\",\n      \"skipFiles\": [\n        \"<node_internals>/**\"\n      ],\n      \"type\": \"node\"\n    },\n    {\n      \"name\": \"Server debug\",\n      \"request\": \"launch\",\n      \"cwd\": \"${workspaceFolder}/server\",\n      \"env\": {\n        \"NODE_ENV\": \"development\"\n      },\n      \"runtimeArgs\": [\n        \"index.js\"\n      ],\n      // not using yarn/nodemon because it doesn't work with breakpoints\n      // \"runtimeExecutable\": \"yarn\",\n      \"skipFiles\": [\n        \"<node_internals>/**\"\n      ],\n      \"type\": \"node\"\n    },\n    {\n      \"name\": \"Frontend debug\",\n      \"request\": \"launch\",\n      \"cwd\": \"${workspaceFolder}/frontend\",\n      \"env\": {\n        \"NODE_ENV\": \"development\",\n      },\n      \"runtimeExecutable\": \"${workspaceFolder}/frontend/node_modules/.bin/vite\",\n      \"runtimeArgs\": [\n        \"--debug\",\n        \"--host=0.0.0.0\"\n      ],\n      // \"runtimeExecutable\": \"yarn\",\n      \"skipFiles\": [\n        \"<node_internals>/**\"\n      ],\n      \"type\": \"node\"\n    },\n    {\n      \"name\": \"Launch Edge\",\n      \"request\": \"launch\",\n      \"type\": \"msedge\",\n      \"url\": \"http://localhost:3000\",\n      \"webRoot\": \"${workspaceFolder}\"\n    },\n    {\n      \"type\": \"chrome\",\n      \"request\": \"launch\",\n      \"name\": \"Launch Chrome against localhost\",\n      \"url\": \"http://localhost:3000\",\n      \"webRoot\": \"${workspaceFolder}\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"cSpell.words\": [\n    \"adoc\",\n    \"aibitat\",\n    \"AIbitat\",\n    \"allm\",\n    \"anythingllm\",\n    \"Apipie\",\n    \"Astra\",\n    \"Chartable\",\n    \"cleancss\",\n    \"comkey\",\n    \"cooldown\",\n    \"cooldowns\",\n    \"datafile\",\n    \"Deduplicator\",\n    \"Dockerized\",\n    \"docpath\",\n    \"elevenlabs\",\n    \"Embeddable\",\n    \"epub\",\n    \"fireworksai\",\n    \"GROQ\",\n    \"hljs\",\n    \"huggingface\",\n    \"inferencing\",\n    \"koboldcpp\",\n    \"Langchain\",\n    \"lmstudio\",\n    \"localai\",\n    \"mbox\",\n    \"Milvus\",\n    \"Mintplex\",\n    \"mixtral\",\n    \"moderations\",\n    \"novita\",\n    \"numpages\",\n    \"Ollama\",\n    \"Oobabooga\",\n    \"openai\",\n    \"opendocument\",\n    \"openrouter\",\n    \"pagerender\",\n    \"ppio\",\n    \"Qdrant\",\n    \"royalblue\",\n    \"SearchApi\",\n    \"searxng\",\n    \"SerpApi\",\n    \"Serper\",\n    \"Serply\",\n    \"streamable\",\n    \"textgenwebui\",\n    \"togetherai\",\n    \"Unembed\",\n    \"uuidv\",\n    \"vectordbs\",\n    \"Weaviate\",\n    \"XAILLM\",\n    \"Zilliz\"\n  ],\n  \"eslint.experimental.useFlatConfig\": true,\n  \"docker.languageserver.formatter.ignoreMultilineInstructions\": true\n}"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n  // See https://go.microsoft.com/fwlink/?LinkId=733558\n  // for the documentation about the tasks.json format\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"type\": \"shell\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}/collector\",\n        \"statusbar\": {\n          \"color\": \"#ffea00\",\n          \"detail\": \"Runs the collector\",\n          \"label\": \"Collector: $(play) run\",\n          \"running\": {\n            \"color\": \"#ffea00\",\n            \"label\": \"Collector: $(gear~spin) running\"\n          }\n        }\n      },\n      \"command\": \"cd ${workspaceFolder}/collector/ && yarn dev\",\n      \"runOptions\": {\n        \"instanceLimit\": 1,\n        \"reevaluateOnRerun\": true\n      },\n      \"presentation\": {\n        \"echo\": true,\n        \"reveal\": \"always\",\n        \"focus\": false,\n        \"panel\": \"shared\",\n        \"showReuseMessage\": true,\n        \"clear\": false\n      },\n      \"label\": \"Collector: run\"\n    },\n    {\n      \"type\": \"shell\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}/server\",\n        \"statusbar\": {\n          \"color\": \"#ffea00\",\n          \"detail\": \"Runs the server\",\n          \"label\": \"Server: $(play) run\",\n          \"running\": {\n            \"color\": \"#ffea00\",\n            \"label\": \"Server: $(gear~spin) running\"\n          }\n        }\n      },\n      \"command\": \"if [ \\\"${CODESPACES}\\\" = \\\"true\\\" ]; then while ! gh codespace ports -c $CODESPACE_NAME | grep 3001; do sleep 1; done; gh codespace ports visibility 3001:public -c $CODESPACE_NAME; fi & cd ${workspaceFolder}/server/ && yarn dev\",\n      \"runOptions\": {\n        \"instanceLimit\": 1,\n        \"reevaluateOnRerun\": true\n      },\n      \"presentation\": {\n        \"echo\": true,\n        \"reveal\": \"always\",\n        \"focus\": false,\n        \"panel\": \"shared\",\n        \"showReuseMessage\": true,\n        \"clear\": false\n      },\n      \"label\": \"Server: run\"\n    },\n    {\n      \"type\": \"shell\",\n      \"options\": {\n        \"cwd\": \"${workspaceFolder}/frontend\",\n        \"statusbar\": {\n          \"color\": \"#ffea00\",\n          \"detail\": \"Runs the frontend\",\n          \"label\": \"Frontend: $(play) run\",\n          \"running\": {\n            \"color\": \"#ffea00\",\n            \"label\": \"Frontend: $(gear~spin) running\"\n          }\n        }\n      },\n      \"command\": \"cd ${workspaceFolder}/frontend/ && yarn dev\",\n      \"runOptions\": {\n        \"instanceLimit\": 1,\n        \"reevaluateOnRerun\": true\n      },\n      \"presentation\": {\n        \"echo\": true,\n        \"reveal\": \"always\",\n        \"focus\": false,\n        \"panel\": \"shared\",\n        \"showReuseMessage\": true,\n        \"clear\": false\n      },\n      \"label\": \"Frontend: run\"\n    }\n  ]\n}\n"
  },
  {
    "path": "BARE_METAL.md",
    "content": "# Run AnythingLLM in production without Docker\n\n> [!WARNING]\n> This method of deployment is **not supported** by the core-team and is to be used as a reference for your deployment.\n> You are fully responsible for securing your deployment and data in this mode.\n> **Any issues** experienced from bare-metal or non-containerized deployments will be **not** answered or supported.\n\nHere you can find the scripts and known working process to run AnythingLLM outside of a Docker container.\n\n### Minimum Requirements\n> [!TIP]\n> You should aim for at least 2GB of RAM. Disk storage is proportional to however much data\n> you will be storing (documents, vectors, models, etc). Minimum 10GB recommended.\n\n- NodeJS v18\n- Yarn\n\n\n## Getting started\n\n1. Clone the repo into your server as the user who the application will run as.\n`git clone git@github.com:Mintplex-Labs/anything-llm.git`\n\n2. `cd anything-llm` and run `yarn setup`. This will install all dependencies to run in production as well as debug the application.\n\n3. `cp server/.env.example server/.env` to create the basic ENV file for where instance settings will be read from on service start.\n\n4. Ensure that the `server/.env` file has _at least_ these keys to start. These values will persist and this file will be automatically written and managed after your first successful boot.\n```\nSTORAGE_DIR=\"/your/absolute/path/to/server/storage\"\n```\n\n5. Edit the `frontend/.env` file for the `VITE_BASE_API` to now be set to `/api`. This is documented in the .env for which one you should use.\n```\n# VITE_API_BASE='http://localhost:3001/api' # Use this URL when developing locally\n# VITE_API_BASE=\"https://$CODESPACE_NAME-3001.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN/api\" # for GitHub Codespaces\nVITE_API_BASE='/api' # Use this URL deploying on non-localhost address OR in docker.\n```\n\n## To start the application\n\nAnythingLLM is comprised of three main sections. The `frontend`, `server`, and `collector`. When running in production you will be running `server` and `collector` on two different processes, with a build step for compilation of the frontend.\n\n1. Build the frontend application.\n`cd frontend && yarn build` - this will produce a `frontend/dist` folder that will be used later.\n\n2. Copy `frontend/dist` to `server/public` - `cp -R frontend/dist server/public`.\nThis should create a folder in `server` named `public` which contains a top level `index.html` file and various other files/folders.\n\n3. Migrate and prepare your database file.\n```\ncd server && npx prisma generate --schema=./prisma/schema.prisma\ncd server && npx prisma migrate deploy --schema=./prisma/schema.prisma\n```\n\n4. Boot the server in production\n`cd server && NODE_ENV=production node index.js &`\n\n5. Boot the collection in another process\n`cd collector && NODE_ENV=production node index.js &`\n\nAnythingLLM should now be running on `http://localhost:3001`!\n\n## Updating AnythingLLM\n\nTo update AnythingLLM with future updates you can `git pull origin master` to pull in the latest code and then repeat steps 2 - 5 to deploy with all changes fully.\n\n_note_ You should ensure that each folder runs `yarn` again to ensure packages are up to date in case any dependencies were added, changed, or removed.\n\n_note_ You should `pkill node` before running an update so that you are not running multiple AnythingLLM processes on the same instance as this can cause conflicts.\n\n\n### Example update script\n\n```shell\n#!/bin/bash\n\ncd $HOME/anything-llm &&\\\ngit checkout . &&\\\ngit pull origin master &&\\\necho \"HEAD pulled to commit $(git log -1 --pretty=format:\"%h\" | tail -n 1)\"\n\necho \"Freezing current ENVs\"\ncurl -I \"http://localhost:3001/api/env-dump\" | head -n 1|cut -d$' ' -f2\n\necho \"Rebuilding Frontend\"\ncd $HOME/anything-llm/frontend && yarn && yarn build && cd $HOME/anything-llm\n\necho \"Copying to Server Public\"\nrm -rf server/public\ncp -r frontend/dist server/public\n\necho \"Killing node processes\"\npkill node\n\necho \"Installing collector dependencies\"\ncd $HOME/anything-llm/collector && yarn\n\necho \"Installing server dependencies & running migrations\"\ncd $HOME/anything-llm/server && yarn\ncd $HOME/anything-llm/server && npx prisma migrate deploy --schema=./prisma/schema.prisma\ncd $HOME/anything-llm/server && npx prisma generate\n\necho \"Booting up services.\"\ntruncate -s 0 /logs/server.log # Or any other log file location.\ntruncate -s 0 /logs/collector.log\n\ncd $HOME/anything-llm/server\n(NODE_ENV=production node index.js) &> /logs/server.log &\n\ncd $HOME/anything-llm/collector\n(NODE_ENV=production node index.js) &> /logs/collector.log &\n```\n\n## Using Nginx?\n\nIf you are using Nginx, you can use the following example configuration to proxy the requests to the server. Chats for streaming require **websocket** connections, so you need to ensure that the Nginx configuration is set up to support websockets. You can do this with a simple reverse proxy configuration.\n\n```nginx\nserver {\n   # Enable websocket connections for agent protocol.\n   location ~* ^/api/agent-invocation/(.*) {\n      proxy_pass http://0.0.0.0:3001;\n      proxy_http_version 1.1;\n      proxy_set_header Upgrade $http_upgrade;\n      proxy_set_header Connection \"Upgrade\";\n   }\n\n   listen 80;\n   server_name [insert FQDN here];\n   location / {\n      # Prevent timeouts on long-running requests.\n      proxy_connect_timeout       605;\n      proxy_send_timeout          605;\n      proxy_read_timeout          605;\n      send_timeout                605;\n      keepalive_timeout           605;\n\n      # Enable readable HTTP Streaming for LLM streamed responses\n      proxy_buffering off; \n      proxy_cache off;\n\n      # Proxy your locally running service\n      proxy_pass  http://0.0.0.0:3001;\n    }\n}\n```"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to AnythingLLM\n\nAnythingLLM is an open-source project and we welcome contributions from the community.\n\n## Reporting Issues\n\nIf you encounter a bug or have a feature request, please open an issue on the\n[GitHub issue tracker](https://github.com/mintplex-labs/anything-llm).\n\n## Picking an issue\n\nWe track issues on the GitHub issue tracker. If you are looking for something to\nwork on, check the [good first issue](https://github.com/mintplex-labs/anything-llm/contribute) label. These issues are typically the best described and have the smallest scope. There may be issues that are not labeled as good first issue, but are still a good starting point.\n\nIf there's an issue you are interested in working on, please leave a comment on the issue. This will help us avoid duplicate work. Additionally, if you have questions about the issue, please ask them in the issue comments. We are happy to provide guidance on how to approach the issue.\n\n## Before you start\n\nKeep in mind that we are a small team and have limited resources. We will do our best to review and merge your PRs, but please be patient. Ultimately, **we become the maintainer** of your changes. It is our responsibility to make sure that the changes are working as expected and are of high quality as well as being compatible with the rest of the project both for existing users and for future users & features.\n\nBefore you start working on an issue, please read the following so that you don't waste time on something that is not a good fit for the project or is more suitable for a personal fork. We would rather answer a comment on an issue than close a PR after you've spent time on it. Your time is valuable and we appreciate your time and effort to make AnythingLLM better.\n\n0. (most important) If you are making a PR that does not have a corresponding issue, **it will not be merged.** _The only exception to this is language translations._\n\n1. If you are modifying the permission system for a new role or something custom, you are likely better off forking the project and building your own version since this is a core part of the project and is only to be maintained by the AnythingLLM team.\n\n2. Integrations (LLM, Vector DB, etc.) are reviewed at our discretion. We will eventually get to them. Do not expect us to merge your integration PR instantly since there are often many moving parts and we want to make sure we get it right. We will get to it!\n\n3. It is our discretion to merge or not merge a PR. We value every contribution, but we also value the quality of the code and the user experience we envision for the project. It is a fine line to walk when running a project like this and please understand that merging or not merging a PR is not a reflection of the quality of the contribution and is not personal. We will do our best to provide feedback on the PR and help you make the changes necessary to get it merged.\n\n4. **Security** is always important. If you have a security concern, please do not open an issue. Instead, please open a CVE on our designated reporting platform [Huntr](https://huntr.com) or contact us at [team@mintplexlabs.com](mailto:team@mintplexlabs.com).\n\n## Configuring Git\n\nFirst, fork the repository on GitHub, then clone your fork:\n\n```bash\ngit clone https://github.com/<username>/anything-llm.git\ncd anything-llm\n```\n\nThen add the main repository as a remote:\n\n```bash\ngit remote add upstream https://github.com/mintplex-labs/anything-llm.git\ngit fetch upstream\n```\n\n## Setting up your development environment\n\nIn the root of the repository, run:\n\n```bash\nyarn setup\n```\n\nThis will install the dependencies, set up the proper and expected ENV files for the project, and run the prisma setup script.\nNext, run:\n\n```bash\nyarn dev:all\n```\nThis will start the server, frontend, and collector in development mode. Changes to the code will be hot reloaded.\n\n## Best practices for pull requests\n\nFor the best chance of having your pull request accepted, please follow these guidelines:\n\n1. Unit test all bug fixes and new features. Your code will not be merged if it\n   doesn't have tests.\n1. If you change the public API, update the documentation in the `anythingllm-docs` repository.\n1. Aim to minimize the number of changes in each pull request. Keep to solving\n   one problem at a time, when possible.\n1. Before marking a pull request ready-for-review, do a self review of your code.\n   Is it clear why you are making the changes? Are the changes easy to understand?\n1. Use [conventional commit messages](https://www.conventionalcommits.org/en/) as pull request titles. Examples:\n    * New feature: `feat: adding foo API`\n    * Bug fix: `fix: issue with foo API`\n    * Documentation change: `docs: adding foo API documentation`\n1. If your pull request is a work in progress, leave the pull request as a draft.\n   We will assume the pull request is ready for review when it is opened.\n1. When writing tests, test the error cases. Make sure they have understandable\n   error messages.\n\n## Project structure\n\nThe core library is written in Node.js. There are additional sub-repositories for the embed widget and browser extension. These are not part of the core AnythingLLM project, but are maintained by the AnythingLLM team.\n\n* `server`: Node.js server source code\n* `frontend`: React frontend source code\n* `collector`: Python collector source code\n\n## Release process\n\nChanges to the core AnythingLLM project are released through the `master` branch. When a PR is merged into `master`, a new version of the package is published to Docker and GitHub Container Registry under the `latest` tag.\n\nWhen a new version is released, the following steps are taken a new image is built and pushed to Docker Hub and GitHub Container Registry under the associated version tag. Version tags are of the format `v<major>.<minor>.<patch>` and are pinned code, while `latest` is the latest version of the code at any point in time.\n\n### Desktop propagation\n\nChanges to the desktop app are downstream of the core AnythingLLM project. Releases of the desktop app are published at the same time as the core AnythingLLM project. Code from the core AnythingLLM project is copied into the desktop app into an Electron wrapper. The Electron wrapper that wraps around the core AnythingLLM project is **not** part of the core AnythingLLM project, but is maintained by the AnythingLLM team.\n\n## License\n\nBy contributing to AnythingLLM (this repository), you agree to license your contributions under the MIT license.\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License\n\nCopyright (c) Mintplex Labs Inc.\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\nall copies 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\nTHE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "<a name=\"readme-top\"></a>\n\n<p align=\"center\">\n  <a href=\"https://anythingllm.com\"><img src=\"https://github.com/Mintplex-Labs/anything-llm/blob/master/images/wordmark.png?raw=true\" alt=\"AnythingLLM logo\"></a>\n</p>\n\n<div align='center'>\n<a href=\"https://trendshift.io/repositories/2415\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/2415\" alt=\"Mintplex-Labs%2Fanything-llm | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n</div>\n\n<p align=\"center\">\n    <b>AnythingLLM:</b> The all-in-one AI app you were looking for.<br />\n    Chat with your docs, use AI Agents, hyper-configurable, multi-user, & no frustrating setup required.\n</p>\n\n<p align=\"center\">\n  <a href=\"https://discord.gg/6UyHPeGZAC\" target=\"_blank\">\n      <img src=\"https://img.shields.io/badge/chat-mintplex_labs-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAH1UExURQAAAP////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////r6+ubn5+7u7/3+/v39/enq6urq6/v7+97f39rb26eoqT1BQ0pOT4+Rkuzs7cnKykZKS0NHSHl8fdzd3ejo6UxPUUBDRdzc3RwgIh8jJSAkJm5xcvHx8aanqB4iJFBTVezt7V5hYlJVVuLj43p9fiImKCMnKZKUlaaoqSElJ21wcfT09O3u7uvr6zE0Nr6/wCUpK5qcnf7+/nh7fEdKTHx+f0tPUOTl5aipqiouMGtubz5CRDQ4OsTGxufn515hY7a3uH1/gXBydIOFhlVYWvX29qaoqCQoKs7Pz/Pz87/AwUtOUNfY2dHR0mhrbOvr7E5RUy8zNXR2d/f39+Xl5UZJSx0hIzQ3Odra2/z8/GlsbaGjpERHSezs7L/BwScrLTQ4Odna2zM3Obm7u3x/gKSmp9jZ2T1AQu/v71pdXkVISr2+vygsLiInKTg7PaOlpisvMcXGxzk8PldaXPLy8u7u7rm6u7S1tsDBwvj4+MPExbe4ueXm5s/Q0Kyf7ewAAAAodFJOUwAABClsrNjx/QM2l9/7lhmI6jTB/kA1GgKJN+nea6vy/MLZQYeVKK3rVA5tAAAAAWJLR0QB/wIt3gAAAAd0SU1FB+cKBAAmMZBHjXIAAAISSURBVDjLY2CAAkYmZhZWNnYODnY2VhZmJkYGVMDIycXNw6sBBbw8fFycyEoYGfkFBDVQgKAAPyMjQl5IWEQDDYgIC8FUMDKKsmlgAWyiEBWMjGJY5YEqxMAqGMWFNXAAYXGgAkYJSQ2cQFKCkYFRShq3AmkpRgYJbghbU0tbB0Tr6ukbgGhDI10gySfBwCwDUWBsYmpmDqQtLK2sbTQ0bO3sHYA8GWYGWWj4WTs6Obu4ami4OTm7exhqeHp5+4DCVJZBDmqdr7ufn3+ArkZgkJ+fU3CIRmgYWFiOARYGvo5OQUHhEUAFTkF+kVHRsLBgkIeyYmLjwoOc4hMSk5JTnINS06DC8gwcEEZ6RqZGlpOfc3ZObl5+gZ+TR2ERWFyBQQFMF5eklmqUpQb5+ReU61ZUOvkFVVXXQBSAraitq29o1GiKcfLzc29u0mjxBzq0tQ0kww5xZHtHUGeXhkZhdxBYgZ4d0LI6c4gjwd7siQQraOp1AivQ6CuAKZCDBBRQQQNQgUb/BGf3cqCCiZOcnCe3QQIKHNRTpk6bDgpZjRkzg3pBQTBrdtCcuZCgluAD0vPmL1gIdvSixUuWgqNs2YJ+DUhkEYxuggkGmOQUcckrioPTJCOXEnZ5JS5YslbGnuyVERlDDFvGEUPOWvwqaH6RVkHKeuDMK6SKnHlVhTgx8jeTmqy6Eij7K6nLqiGyPwChsa1MUrnq1wAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0xMC0wNFQwMDozODo0OSswMDowMB9V0a8AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMTAtMDRUMDA6Mzg6NDkrMDA6MDBuCGkTAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDIzLTEwLTA0VDAwOjM4OjQ5KzAwOjAwOR1IzAAAAABJRU5ErkJggg==\" alt=\"Discord\">\n  </a> |\n  <a href=\"https://github.com/Mintplex-Labs/anything-llm/blob/master/LICENSE\" target=\"_blank\">\n      <img src=\"https://img.shields.io/static/v1?label=license&message=MIT&color=white\" alt=\"License\">\n  </a> |\n  <a href=\"https://docs.anythingllm.com\" target=\"_blank\">\n    Docs\n  </a> |\n   <a href=\"https://my.mintplexlabs.com/aio-checkout?product=anythingllm\" target=\"_blank\">\n    Hosted Instance\n  </a>\n</p>\n\n<p align=\"center\">\n  <b>English</b> · <a href='./locales/README.zh-CN.md'>简体中文</a> · <a href='./locales/README.ja-JP.md'>日本語</a>\n</p>\n\n<p align=\"center\">\n👉 AnythingLLM for desktop (Mac, Windows, & Linux)! <a href=\"https://anythingllm.com/download\" target=\"_blank\"> Download Now</a>\n</p>\n\nChat with your docs. Automate complex workflows with AI Agents. Hyper-configurable, multi-user ready, battle-tested—and runs locally by default with zero setup friction.\n\n![Chatting](https://github.com/Mintplex-Labs/anything-llm/releases/download/v1.11.2/AnythingLLM720p.gif)\n\n<details>\n<summary><kbd>Watch the demo!</kbd></summary>\n\n[![Watch the video](/images/youtube.png)](https://youtu.be/f95rGD9trL0)\n\n</details>\n\n### Product Overview\n\nAnythingLLM is the all-in-one AI application that lets you build a private, fully-featured ChatGPT—without compromises. Connect your favorite local or cloud LLM, ingest your documents, and start chatting in minutes. Out of the box you get built-in agents, multi-user support, vector databases, and document pipelines — no extra configuration required.\n\nAnythingLLM supports multiple users as well where you can control the access and experience per user without compromising the security or privacy of the instance or your intellectual property.\n\n## Cool features of AnythingLLM\n\n- [Intelligent Skill Selection](https://docs.anythingllm.com/agent/intelligent-tool-selection) Enable **unlimited** tools for your models while reducing token usage by up to 80% per query\n- [**No-code AI Agent builder**](https://docs.anythingllm.com/agent-flows/overview)\n- [**Full MCP-compatibility**](https://docs.anythingllm.com/mcp-compatibility/overview)\n- **Multi-modal support (both closed and open-source LLMs!)**\n- [**Custom AI Agents**](https://docs.anythingllm.com/agent/custom/introduction)\n- 👤 Multi-user instance support and permissioning _Docker version only_\n- 🦾 Agents inside your workspace (browse the web, etc)\n- 💬 [Custom Embeddable Chat widget for your website](https://github.com/Mintplex-Labs/anythingllm-embed/blob/main/README.md) _Docker version only_\n- 📖 Multiple document type support (PDF, TXT, DOCX, etc)\n- Intuitive chat UI with drag-and-drop uploads and source citations.\n- Production-ready for any cloud deployment.\n- Works with all popular [closed and open-source LLM providers](#supported-llms-embedder-models-speech-models-and-vector-databases).\n- Built-in optimizations for large document sets—lower costs and faster responses than other chat UIs.\n- Full Developer API for custom integrations!\n- ...and much more—install in minutes and see for yourself.\n\n### Supported LLMs, Embedder Models, Speech models, and Vector Databases\n\n**Large Language Models (LLMs):**\n\n- [Any open-source llama.cpp compatible model](/server/storage/models/README.md#text-generation-llm-selection)\n- [OpenAI](https://openai.com)\n- [OpenAI (Generic)](https://openai.com)\n- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)\n- [AWS Bedrock](https://aws.amazon.com/bedrock/)\n- [Anthropic](https://www.anthropic.com/)\n- [NVIDIA NIM (chat models)](https://build.nvidia.com/explore/discover)\n- [Google Gemini Pro](https://ai.google.dev/)\n- [Hugging Face (chat models)](https://huggingface.co/)\n- [Ollama (chat models)](https://ollama.ai/)\n- [LM Studio (all models)](https://lmstudio.ai)\n- [LocalAI (all models)](https://localai.io/)\n- [Together AI (chat models)](https://www.together.ai/)\n- [Fireworks AI (chat models)](https://fireworks.ai/)\n- [Perplexity (chat models)](https://www.perplexity.ai/)\n- [OpenRouter (chat models)](https://openrouter.ai/)\n- [DeepSeek (chat models)](https://deepseek.com/)\n- [Mistral](https://mistral.ai/)\n- [Groq](https://groq.com/)\n- [Cohere](https://cohere.com/)\n- [KoboldCPP](https://github.com/LostRuins/koboldcpp)\n- [LiteLLM](https://github.com/BerriAI/litellm)\n- [Text Generation Web UI](https://github.com/oobabooga/text-generation-webui)\n- [Apipie](https://apipie.ai/)\n- [xAI](https://x.ai/)\n- [Z.AI (chat models)](https://z.ai/model-api)\n- [Novita AI (chat models)](https://novita.ai/model-api/product/llm-api?utm_source=github_anything-llm&utm_medium=github_readme&utm_campaign=link)\n- [PPIO](https://ppinfra.com?utm_source=github_anything-llm)\n- [Gitee AI](https://ai.gitee.com/)\n- [Moonshot AI](https://www.moonshot.ai/)\n- [Microsoft Foundry Local](https://github.com/microsoft/Foundry-Local)\n- [CometAPI (chat models)](https://api.cometapi.com/)\n- [Docker Model Runner](https://docs.docker.com/ai/model-runner/)\n- [PrivateModeAI (chat models)](https://privatemode.ai/)\n- [SambaNova Cloud (chat models)](https://cloud.sambanova.ai/)\n- [Lemonade by AMD](https://lemonade-server.ai)\n\n**Embedder models:**\n\n- [AnythingLLM Native Embedder](/server/storage/models/README.md) (default)\n- [OpenAI](https://openai.com)\n- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)\n- [LocalAI (all)](https://localai.io/)\n- [Ollama (all)](https://ollama.ai/)\n- [LM Studio (all)](https://lmstudio.ai)\n- [Cohere](https://cohere.com/)\n\n**Audio Transcription models:**\n\n- [AnythingLLM Built-in](https://github.com/Mintplex-Labs/anything-llm/tree/master/server/storage/models#audiovideo-transcription) (default)\n- [OpenAI](https://openai.com/)\n\n**TTS (text-to-speech) support:**\n\n- Native Browser Built-in (default)\n- [PiperTTSLocal - runs in browser](https://github.com/rhasspy/piper)\n- [OpenAI TTS](https://platform.openai.com/docs/guides/text-to-speech/voice-options)\n- [ElevenLabs](https://elevenlabs.io/)\n- Any OpenAI Compatible TTS service.\n\n**STT (speech-to-text) support:**\n\n- Native Browser Built-in (default)\n\n**Vector Databases:**\n\n- [LanceDB](https://github.com/lancedb/lancedb) (default)\n- [PGVector](https://github.com/pgvector/pgvector)\n- [Astra DB](https://www.datastax.com/products/datastax-astra)\n- [Pinecone](https://pinecone.io)\n- [Chroma & ChromaCloud](https://trychroma.com)\n- [Weaviate](https://weaviate.io)\n- [Qdrant](https://qdrant.tech)\n- [Milvus](https://milvus.io)\n- [Zilliz](https://zilliz.com)\n\n### Technical Overview\n\nThis monorepo consists of six main sections:\n\n- `frontend`: A viteJS + React frontend that you can run to easily create and manage all your content the LLM can use.\n- `server`: A NodeJS express server to handle all the interactions and do all the vectorDB management and LLM interactions.\n- `collector`: NodeJS express server that processes and parses documents from the UI.\n- `docker`: Docker instructions and build process + information for building from source.\n- `embed`: Submodule for generation & creation of the [web embed widget](https://github.com/Mintplex-Labs/anythingllm-embed).\n- `browser-extension`: Submodule for the [chrome browser extension](https://github.com/Mintplex-Labs/anythingllm-extension).\n\n## 🛳 Self-Hosting\n\nMintplex Labs & the community maintain a number of deployment methods, scripts, and templates that you can use to run AnythingLLM locally. Refer to the table below to read how to deploy on your preferred environment or to automatically deploy.\n| Docker | AWS | GCP | Digital Ocean | Render.com |\n|----------------------------------------|----|-----|---------------|------------|\n| [![Deploy on Docker][docker-btn]][docker-deploy] | [![Deploy on AWS][aws-btn]][aws-deploy] | [![Deploy on GCP][gcp-btn]][gcp-deploy] | [![Deploy on DigitalOcean][do-btn]][do-deploy] | [![Deploy on Render.com][render-btn]][render-deploy] |\n\n| Railway                                             | RepoCloud                                                 | Elestio                                             | Northflank                                                   |\n| --------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------ |\n| [![Deploy on Railway][railway-btn]][railway-deploy] | [![Deploy on RepoCloud][repocloud-btn]][repocloud-deploy] | [![Deploy on Elestio][elestio-btn]][elestio-deploy] | [![Deploy on Northflank][northflank-btn]][northflank-deploy] |\n\n[or set up a production AnythingLLM instance without Docker →](./BARE_METAL.md)\n\n## How to setup for development\n\n- `yarn setup` To fill in the required `.env` files you'll need in each of the application sections (from root of repo).\n  - Go fill those out before proceeding. Ensure `server/.env.development` is filled or else things won't work right.\n- `yarn dev:server` To boot the server locally (from root of repo).\n- `yarn dev:frontend` To boot the frontend locally (from root of repo).\n- `yarn dev:collector` To then run the document collector (from root of repo).\n\n[Learn about documents](./server/storage/documents/DOCUMENTS.md)\n\n## Telemetry & Privacy\n\nAnythingLLM by Mintplex Labs Inc contains a telemetry feature that collects anonymous usage information.\n\n<details>\n<summary><kbd>More about Telemetry & Privacy for AnythingLLM</kbd></summary>\n\n### Why?\n\nWe use this information to help us understand how AnythingLLM is used, to help us prioritize work on new features and bug fixes, and to help us improve AnythingLLM's performance and stability.\n\n### Opting out\n\nSet `DISABLE_TELEMETRY` in your server or docker .env settings to \"true\" to opt out of telemetry. You can also do this in-app by going to the sidebar > `Privacy` and disabling telemetry.\n\n### What do you explicitly track?\n\nWe will only track usage details that help us make product and roadmap decisions, specifically:\n\n- Type of your installation (Docker or Desktop)\n\n- When a document is added or removed. No information _about_ the document. Just that the event occurred. This gives us an idea of use.\n\n- Type of vector database in use. This helps us prioritize changes when updates arrive for that provider.\n\n- Type of LLM provider & model tag in use. This helps us prioritize changes when updates arrive for that provider or model, or combination thereof. eg: reasoning vs regular, multi-modal models, etc.\n\n- When a chat is sent. This is the most regular \"event\" and gives us an idea of the daily-activity of this project across all installations. Again, only the **event** is sent - we have no information on the nature or content of the chat itself.\n\nYou can verify these claims by finding all locations `Telemetry.sendTelemetry` is called. Additionally these events are written to the output log so you can also see the specific data which was sent - if enabled. **No IP or other identifying information is collected**. The Telemetry provider is [PostHog](https://posthog.com/) - an open-source telemetry collection service.\n\nWe take privacy very seriously, and we hope you understand that we want to learn how our tool is used, without using annoying popup surveys, so we can build something worth using. The anonymous data is _never_ shared with third parties, ever.\n\n[View all telemetry events in source code](https://github.com/search?q=repo%3AMintplex-Labs%2Fanything-llm%20.sendTelemetry(&type=code)\n\n</details>\n\n## 👋 Contributing\n\n- [Contributing to AnythingLLM](./CONTRIBUTING.md) - How to contribute to AnythingLLM.\n\n## 💖 Sponsors\n\n### Premium Sponsors\n\n<!-- premium-sponsors (reserved for $100/mth sponsors who request to be called out here and/or are non-private sponsors) -->\n<a href=\"https://www.dcsdigital.co.uk\" target=\"_blank\">\n  <img src=\"https://a8cforagenciesportfolio.wordpress.com/wp-content/uploads/2024/08/logo-image-232621379.png\" height=\"100px\" alt=\"User avatar: DCS DIGITAL\" />\n</a>\n<!-- premium-sponsors -->\n\n### All Sponsors\n\n<!-- all-sponsors --><a href=\"https://github.com/jaschadub\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;jaschadub.png\" width=\"60px\" alt=\"User avatar: Jascha\" /></a><a href=\"https://github.com/KickingAss2024\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;KickingAss2024.png\" width=\"60px\" alt=\"User avatar: KickAss\" /></a><a href=\"https://github.com/ShadowArcanist\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;ShadowArcanist.png\" width=\"60px\" alt=\"User avatar: ShadowArcanist\" /></a><a href=\"https://github.com/AtlasVIA\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;AtlasVIA.png\" width=\"60px\" alt=\"User avatar: Atlas\" /></a><a href=\"https://github.com/cope\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;cope.png\" width=\"60px\" alt=\"User avatar: Predrag Stojadinović\" /></a><a href=\"https://github.com/DiegoSpinola\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;DiegoSpinola.png\" width=\"60px\" alt=\"User avatar: Diego Spinola\" /></a><a href=\"https://github.com/PortlandKyGuy\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;PortlandKyGuy.png\" width=\"60px\" alt=\"User avatar: Kyle\" /></a><a href=\"https://github.com/peperunas\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;peperunas.png\" width=\"60px\" alt=\"User avatar: Giulio De Pasquale\" /></a><a href=\"https://github.com/jasoncdavis0\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;jasoncdavis0.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/macstadium\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;macstadium.png\" width=\"60px\" alt=\"User avatar: MacStadium\" /></a><a href=\"https://github.com/armlynobinguar\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;armlynobinguar.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/MikeHago\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;MikeHago.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/maaisde\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;maaisde.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/mhollier117\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;mhollier117.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/pleabargain\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;pleabargain.png\" width=\"60px\" alt=\"User avatar: Dennis\" /></a><a href=\"https://github.com/broichan\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;broichan.png\" width=\"60px\" alt=\"User avatar: Michael Hamilton, Ph.D.\" /></a><a href=\"https://github.com/azim-charaniya\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;azim-charaniya.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/gabriellemon\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;gabriellemon.png\" width=\"60px\" alt=\"User avatar: TernaryLabs\" /></a><a href=\"https://github.com/CelaDaniel\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;CelaDaniel.png\" width=\"60px\" alt=\"User avatar: Daniel Cela\" /></a><a href=\"https://github.com/altrsadmin\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;altrsadmin.png\" width=\"60px\" alt=\"User avatar: Alesso\" /></a><a href=\"https://github.com/bitjungle\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;bitjungle.png\" width=\"60px\" alt=\"User avatar: Rune Mathisen\" /></a><a href=\"https://github.com/pcrossleyAC\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;pcrossleyAC.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/saroj-pattnaik\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;saroj-pattnaik.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/techmedic5\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;techmedic5.png\" width=\"60px\" alt=\"User avatar: Alan\" /></a><a href=\"https://github.com/ddocta\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;ddocta.png\" width=\"60px\" alt=\"User avatar: Damien Peters\" /></a><a href=\"https://github.com/dcsdigital\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;dcsdigital.png\" width=\"60px\" alt=\"User avatar: DCS Digital\" /></a><a href=\"https://github.com/pm7y\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;pm7y.png\" width=\"60px\" alt=\"User avatar: Paul Mcilreavy\" /></a><a href=\"https://github.com/tilwolf\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;tilwolf.png\" width=\"60px\" alt=\"User avatar: Til Wolf\" /></a><a href=\"https://github.com/ozzyoss77\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;ozzyoss77.png\" width=\"60px\" alt=\"User avatar: Leopoldo Crhistian Riverin Gomez\" /></a><a href=\"https://github.com/AlphaEcho11\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;AlphaEcho11.png\" width=\"60px\" alt=\"User avatar: AJEsau\" /></a><a href=\"https://github.com/svanomm\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;svanomm.png\" width=\"60px\" alt=\"User avatar: Steven VanOmmeren\" /></a><a href=\"https://github.com/socketbox\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;socketbox.png\" width=\"60px\" alt=\"User avatar: Casey Boettcher\" /></a><a href=\"https://github.com/zebbern\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;zebbern.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/avineetbespin\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;avineetbespin.png\" width=\"60px\" alt=\"User avatar: Avineet\" /></a><a href=\"https://github.com/invictus-1\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;invictus-1.png\" width=\"60px\" alt=\"User avatar: Chris\" /></a><a href=\"https://github.com/mirbyte\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;mirbyte.png\" width=\"60px\" alt=\"User avatar: mirko\" /></a><a href=\"https://github.com/bisonbet\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;bisonbet.png\" width=\"60px\" alt=\"User avatar: Tim Champ\" /></a><a href=\"https://github.com/Sinkingdev\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;Sinkingdev.png\" width=\"60px\" alt=\"User avatar: Peter Mathisen\" /></a><a href=\"https://github.com/Ed-STEM\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;Ed-STEM.png\" width=\"60px\" alt=\"User avatar: Ed di Girolamo\" /></a><a href=\"https://github.com/milkowski\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;milkowski.png\" width=\"60px\" alt=\"User avatar: Wojciech Miłkowski\" /></a><a href=\"https://github.com/ADS-Fund\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;ADS-Fund.png\" width=\"60px\" alt=\"User avatar: ADS Fund\" /></a><a href=\"https://github.com/arc46-io\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;arc46-io.png\" width=\"60px\" alt=\"User avatar: arc46 GmbH\" /></a><a href=\"https://github.com/liyin2015\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;liyin2015.png\" width=\"60px\" alt=\"User avatar: Li Yin\" /></a><a href=\"https://github.com/SylphAI-Inc\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;SylphAI-Inc.png\" width=\"60px\" alt=\"User avatar: SylphAI\" /></a><!-- all-sponsors -->\n\n## 🌟 Contributors\n\n[![anythingllm contributors](https://contrib.rocks/image?repo=mintplex-labs/anything-llm)](https://github.com/mintplex-labs/anything-llm/graphs/contributors)\n\n[![Star History Chart](https://api.star-history.com/svg?repos=mintplex-labs/anything-llm&type=Timeline)](https://star-history.com/#mintplex-labs/anything-llm&Date)\n\n## 🔗 More Products\n\n- **[VectorAdmin][vector-admin]:** An all-in-one GUI & tool-suite for managing vector databases.\n- **[OpenAI Assistant Swarm][assistant-swarm]:** Turn your entire library of OpenAI assistants into one single army commanded from a single agent.\n\n<div align=\"right\">\n\n[![][back-to-top]](#readme-top)\n\n</div>\n\n---\n\nCopyright © 2026 [Mintplex Labs][profile-link]. <br />\nThis project is [MIT](./LICENSE) licensed.\n\n<!-- LINK GROUP -->\n\n[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-222628?style=flat-square\n[profile-link]: https://github.com/mintplex-labs\n[vector-admin]: https://github.com/mintplex-labs/vector-admin\n[assistant-swarm]: https://github.com/Mintplex-Labs/openai-assistant-swarm\n[docker-btn]: ./images/deployBtns/docker.png\n[docker-deploy]: ./docker/HOW_TO_USE_DOCKER.md\n[aws-btn]: ./images/deployBtns/aws.png\n[aws-deploy]: ./cloud-deployments/aws/cloudformation/DEPLOY.md\n[gcp-btn]: https://deploy.cloud.run/button.svg\n[gcp-deploy]: ./cloud-deployments/gcp/deployment/DEPLOY.md\n[do-btn]: https://www.deploytodo.com/do-btn-blue.svg\n[do-deploy]: ./cloud-deployments/digitalocean/terraform/DEPLOY.md\n[render-btn]: https://render.com/images/deploy-to-render-button.svg\n[render-deploy]: https://render.com/deploy?repo=https://github.com/Mintplex-Labs/anything-llm&branch=render\n[render-btn]: https://render.com/images/deploy-to-render-button.svg\n[render-deploy]: https://render.com/deploy?repo=https://github.com/Mintplex-Labs/anything-llm&branch=render\n[railway-btn]: https://railway.app/button.svg\n[railway-deploy]: https://railway.app/template/HNSCS1?referralCode=WFgJkn\n[repocloud-btn]: https://d16t0pc4846x52.cloudfront.net/deploylobe.svg\n[repocloud-deploy]: https://repocloud.io/details/?app_id=276\n[elestio-btn]: https://elest.io/images/logos/deploy-to-elestio-btn.png\n[elestio-deploy]: https://elest.io/open-source/anythingllm\n[northflank-btn]: https://assets.northflank.com/deploy_to_northflank_smm_36700fb050.svg\n[northflank-deploy]: https://northflank.com/stacks/deploy-anythingllm\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nUse this section to tell people about which versions of your project are\ncurrently being supported with security updates.\n\n| Version | Supported          |\n| ------- | ------------------ |\n| 0.1.x   | :white_check_mark: |\n\n\n## Reporting a Vulnerability\n\nIf a security concern is found that you would like to disclose you can create a PR for it or if you would like to clear this issue before posting you can email [Core Mintplex Labs Team](mailto:team@mintplexlabs.com).\n"
  },
  {
    "path": "TERMS_SELF_HOSTED.md",
    "content": "# AnythingLLM Self-Hosted: Data Privacy & Terms of Service\n\nThis document outlines the privacy standards, data handling procedures, and licensing terms for the self-hosted version of AnythingLLM, developed by Mintplex Labs Inc.\n\n## 1. Data Sovereignty & Local-First Architecture\nAnythingLLM is designed as a **local-first** application. When utilizing the self-hosted version (Docker, Desktop, or Source):\n* **No External Access:** Mintplex Labs Inc. does not host, store, or have access to any documents, chat histories, workspace settings, or embeddings created within your instance.\n* **On-Premise Storage:** All data resides strictly on the infrastructure provisioned and managed by the user or their organization.\n* **Air-Gap Capability:** AnythingLLM can be operated in a strictly air-gapped environment with no internet connectivity, provided local LLM and Vector database providers (e.g., Ollama, LocalAI, LanceDB) are utilized.\n\n## 2. Telemetry and Analytics\nTo improve software performance and stability, AnythingLLM includes an optional telemetry feature.\n* **Anonymity:** Collected data is strictly anonymous and contains no Personally Identifiable Information (PII), document content, chat logs, fingerprinting data, or any other sensitive information. Purely usage based data is collected.\n* **Opt-Out:** Users may disable telemetry at any time via the **Settings** menu within the application. Once disabled, no usage data is transmitted to Mintplex Labs.\n\n## 3. Third-Party Integrations\nAnythingLLM allows users to connect to external services (e.g., OpenAI, Anthropic, Pinecone). \n* **Data Transmission:** When these services are enabled, data is transmitted directly from your instance to the third-party provider. \n* **Governing Terms:** Data handled by third-party providers is subject to their respective Terms of Service and Privacy Policies. Mintplex Labs is not responsible for the data practices of these external entities.\n\n_by default, AnythingLLM does **everything on-device first** - so you would have to manually configure and enable these integrations to be subject to third party terms._\n\n## 4. Security & Network\n* **No \"Phone Home\":** Aside from [optional telemetry](https://github.com/Mintplex-Labs/anything-llm?tab=readme-ov-file#telemetry--privacy), the software does not require an external connection to Mintplex Labs servers to function.\n* **Environment Security:** The user is responsible for securing the host environment, including network firewalls, SSL/TLS encryption, and access control for the AnythingLLM instance.\n* **CDN Assets:** Out of a convience to international users, we use a hosted CDN to mirror some critical path models (eg: the default embedder and reranking ONNX models) which are not available in all regions. These models are downloaded from our CDN as a fallback, and any air-gapped installations you can either download these models manually or use another provider. Assets of these nature are downloaded once and cached in your associated local storage.\n\n## 5. Licensing and Liability\n* **License:** The AnythingLLM core is provided under the **MIT License**.\n* **No Warranty:** As per the license agreement, the software is provided \"as is,\" without warranty of any kind, express or implied, including but not limited to the warranties of merchantability or fitness for a particular purpose.\n* **Liability:** In no event shall the authors or copyright holders be liable for any claim, damages, or other liability arising from the use of the software.\n\n## 6. Support and Compatibility\nWhile Mintplex Labs prioritizes stability and backward compatibility, the self-hosted version is used at the user's discretion. Formal Service Level Agreements (SLAs) are not provided for the standard self-hosted version unless otherwise negotiated via a separate enterprise agreement.\n\n---\n*Last Updated: March 2026*"
  },
  {
    "path": "cloud-deployments/aws/cloudformation/DEPLOY.md",
    "content": "# How to deploy a private AnythingLLM instance on AWS\n\nWith an AWS account you can easily deploy a private AnythingLLM instance on AWS. This will create a url that you can access from any browser over HTTP (HTTPS not supported). This single instance will run on your own keys and they will not be exposed - however if you want your instance to be protected it is highly recommend that you set a password once setup is complete.\n\n**Quick Launch (EASY)**\n1. Log in to your AWS account\n2. Open [CloudFormation](https://us-west-1.console.aws.amazon.com/cloudformation/home)\n3. Ensure you are deploying in a geographic zone that is nearest to your physical location to reduce latency.\n4. Click `Create Stack`\n\n![Create Stack](../../../images/screenshots/create_stack.png)\n\n5. Use the file `cloudformation_create_anythingllm.json` as your JSON template.\n\n![Upload Stack](../../../images/screenshots/upload.png)\n\n6. Click Deploy.  \n7. Wait for stack events to finish and be marked as `Completed`\n8. View `Outputs` tab.\n\n![Stack Output](../../../images/screenshots/cf_outputs.png)\n\n9. Wait for all resources to be built. Now wait until instance is available on `[InstanceIP]:3001`.\nThis process may take up to 10 minutes. See **Note** below on how to visualize this process.\n\nThe output of this cloudformation stack will be:\n- 1 EC2 Instance\n- 1 Security Group with 0.0.0.0/0 access on port 3001\n- 1 EC2 Instance Volume `gb2` of 10Gib minimum - customizable pre-deploy.\n\n**Requirements**\n- An AWS account with billing information.\n\n## Please read this notice before submitting issues about your deployment\n\n**Note:** \nYour instance will not be available instantly. Depending on the instance size you launched with it can take 5-10 minutes to fully boot up.\n\nIf you want to check the instance's progress, navigate to [your deployed EC2 instances](https://us-west-1.console.aws.amazon.com/ec2/home) and connect to your instance via SSH in browser.\n\nOnce connected run `sudo tail -f /var/log/cloud-init-output.log` and wait for the file to conclude deployment of the docker image.\nYou should see an output like this\n```\n[+] Running 2/2\n ⠿ Network docker_anything-llm  Created \n ⠿ Container anything-llm       Started  \n```\n\nAdditionally, your use of this deployment process means you are responsible for any costs of these AWS resources fully."
  },
  {
    "path": "cloud-deployments/aws/cloudformation/aws_https_instructions.md",
    "content": "# How to Configure HTTPS for Anything LLM AWS private deployment\nInstructions for manual https configuration after generating and running the aws cloudformation template (aws_build_from_source_no_credentials.json). Tested on following browsers: Firefox version 119, Chrome version 118, Edge 118.\n\n**Requirements**\n- Successful deployment of Amazon Linux 2023 EC2 instance with Docker container running Anything LLM\n- Admin priv to configure Elastic IP for EC2 instance via AWS Management Console UI\n- Admin priv to configure DNS services (i.e. AWS Route 53) via AWS Management Console UI\n- Admin priv to configure EC2 Security Group rules via AWS Management Console UI\n\n## Step 1: Allocate and assign Elastic IP Address to your deployed EC2 instance\n1. Follow AWS instructions on allocating EIP here: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html#using-instance-addressing-eips-allocating\n2. Follow AWS instructions on assigning EIP to EC2 instance here: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html#using-instance-addressing-eips-associating  \n\n## Step 2: Configure DNS A record to resolve to the previously assigned EC2 instance via EIP \nThese instructions assume that you already have a top-level domain configured and are using a subdomain \nto access AnythingLLM.\n1. Follow AWS instructions on routing traffic to EC2 instance here: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-ec2-instance.html \n\n## Step 3: Install and enable nginx\nThese instructions are for CLI configuration and assume you are logged in to EC2 instance as the ec2-user.\n1. $sudo yum install nginx -y\n2. $sudo systemctl enable nginx && sudo systemctl start nginx\n\n## Step 4: Install certbot\nThese instructions are for CLI configuration and assume you are logged in to EC2 instance as the ec2-user.\n1. $sudo yum install -y augeas-libs\n2. $sudo python3 -m venv /opt/certbot/\n3. $sudo /opt/certbot/bin/pip install --upgrade pip\n4. $sudo /opt/certbot/bin/pip install certbot certbot-nginx\n5. $sudo ln -s /opt/certbot/bin/certbot /usr/bin/certbot\n\n## Step 5: Configure temporary Inbound Traffic Rule for Security Group to certbot DNS verification\n1. Follow AWS instructions on creating inbound rule (http port 80 0.0.0.0/0) for EC2 security group here: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/working-with-security-groups.html#adding-security-group-rule\n\n## Step 6: Comment out default http NGINX proxy configuration\nThese instructions are for CLI configuration and assume you are logged in to EC2 instance as the ec2-user.\n1. $sudo vi /etc/nginx/nginx.conf\n2. In the nginx.conf file, comment out the default server block configuration for http/port 80. It should look something like the following:\n```\n#    server {\n#        listen       80;\n#        listen       [::]:80;\n#        server_name  _;\n#        root         /usr/share/nginx/html;\n#\n#        # Load configuration files for the default server block.\n#        include /etc/nginx/default.d/*.conf;\n#\n#        error_page 404 /404.html;\n#        location = /404.html {\n#        }\n#\n#        error_page 500 502 503 504 /50x.html;\n#        location = /50x.html {\n#        }\n#    }\n```\n3. Enter ':wq' to save the changes to the nginx default config\n\n## Step 7: Create simple http proxy configuration for AnythingLLM \nThese instructions are for CLI configuration and assume you are logged in to EC2 instance as the ec2-user.\n1. $sudo vi /etc/nginx/conf.d/anything.conf\n2. Add the following configuration ensuring that you add your FQDN:.\n\n```\nserver {\n   # Enable websocket connections for agent protocol.\n   location ~* ^/api/agent-invocation/(.*) {\n      proxy_pass http://0.0.0.0:3001;\n      proxy_http_version 1.1;\n      proxy_set_header Upgrade $http_upgrade;\n      proxy_set_header Connection \"Upgrade\";\n   }\n\n   listen 80;\n   server_name [insert FQDN here];\n   location / {\n      # Prevent timeouts on long-running requests.\n      proxy_connect_timeout       605;\n      proxy_send_timeout          605;\n      proxy_read_timeout          605;\n      send_timeout                605;\n      keepalive_timeout           605;\n\n      # Enable readable HTTP Streaming for LLM streamed responses\n      proxy_buffering off; \n      proxy_cache off;\n\n      # Proxy your locally running service\n      proxy_pass  http://0.0.0.0:3001;\n    }\n}\n```\n3. Enter ':wq' to save the changes to the anything config file\n\n## Step 8: Test nginx http proxy config and restart nginx service\nThese instructions are for CLI configuration and assume you are logged in to EC2 instance as the ec2-user.\n1. $sudo nginx -t\n2. $sudo systemctl restart nginx\n3. Navigate to http://FQDN in a browser and you should be proxied to the AnythingLLM web UI.\n\n## Step 9: Generate/install cert\nThese instructions are for CLI configuration and assume you are logged in to EC2 instance as the ec2-user.\n1. $sudo certbot --nginx -d [Insert FQDN here] \n    Example command: $sudo certbot --nginx -d anythingllm.exampleorganization.org\n    This command will generate the appropriate certificate files, write the files to /etc/letsencrypt/live/yourFQDN, and make updates to the nginx\n    configuration file for anythingllm located at /etc/nginx/conf.d/anything.llm\n3. Enter the email address you would like to use for updates.\n4. Accept the terms of service.\n5. Accept or decline to receive communication from LetsEncrypt.\n\n## Step 10: Test Cert installation\n1. $sudo cat /etc/nginx/conf.d/anything.conf\nYour should see a completely updated configuration that includes https/443 and a redirect configuration for http/80. \n2. Navigate to https://FQDN in a browser and you should be proxied to the AnythingLLM web UI.\n\n## Step 11: (Optional) Remove temporary Inbound Traffic Rule for Security Group to certbot DNS verification\n1. Follow AWS instructions on deleting inbound rule (http port 80 0.0.0.0/0) for EC2 security group here: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/working-with-security-groups.html#deleting-security-group-rule\n"
  },
  {
    "path": "cloud-deployments/aws/cloudformation/cloudformation_create_anythingllm.json",
    "content": "{\n  \"AWSTemplateFormatVersion\": \"2010-09-09\",\n  \"Description\": \"Create a stack that runs AnythingLLM on a single instance\",\n  \"Parameters\": {\n    \"InstanceType\": {\n      \"Description\": \"EC2 instance type\",\n      \"Type\": \"String\",\n      \"Default\": \"t3.small\"\n    },\n    \"InstanceVolume\": {\n      \"Description\": \"Storage size of disk on Instance in GB\",\n      \"Type\": \"Number\",\n      \"Default\": 10,\n      \"MinValue\": 4\n    }\n  },\n  \"Resources\": {\n    \"AnythingLLMInstance\": {\n      \"Type\": \"AWS::EC2::Instance\",\n      \"Properties\": {\n        \"ImageId\": {\n          \"Fn::FindInMap\": [\n            \"Region2AMI\",\n            {\n              \"Ref\": \"AWS::Region\"\n            },\n            \"AMI\"\n          ]\n        },\n        \"InstanceType\": {\n          \"Ref\": \"InstanceType\"\n        },\n        \"SecurityGroupIds\": [\n          {\n            \"Ref\": \"AnythingLLMInstanceSecurityGroup\"\n          }\n        ],\n        \"BlockDeviceMappings\": [\n          {\n            \"DeviceName\": {\n              \"Fn::FindInMap\": [\n                \"Region2AMI\",\n                {\n                  \"Ref\": \"AWS::Region\"\n                },\n                \"RootDeviceName\"\n              ]\n            },\n            \"Ebs\": {\n              \"VolumeSize\": {\n                \"Ref\": \"InstanceVolume\"\n              }\n            }\n          }\n        ],\n        \"UserData\": {\n          \"Fn::Base64\": {\n            \"Fn::Join\": [\n              \"\",\n              [\n                \"Content-Type: multipart/mixed; boundary=\\\"//\\\"\\n\",\n                \"MIME-Version: 1.0\\n\",\n                \"\\n\",\n                \"--//\\n\",\n                \"Content-Type: text/cloud-config; charset=\\\"us-ascii\\\"\\n\",\n                \"MIME-Version: 1.0\\n\",\n                \"Content-Transfer-Encoding: 7bit\\n\",\n                \"Content-Disposition: attachment; filename=\\\"cloud-config.txt\\\"\\n\",\n                \"\\n\",\n                \"\\n\",\n                \"#cloud-config\\n\",\n                \"cloud_final_modules:\\n\",\n                \"- [scripts-user, once-per-instance]\\n\",\n                \"\\n\",\n                \"\\n\",\n                \"--//\\n\",\n                \"Content-Type: text/x-shellscript; charset=\\\"us-ascii\\\"\\n\",\n                \"MIME-Version: 1.0\\n\",\n                \"Content-Transfer-Encoding: 7bit\\n\",\n                \"Content-Disposition: attachment; filename=\\\"userdata.txt\\\"\\n\",\n                \"\\n\",\n                \"\\n\",\n                \"#!/bin/bash\\n\",\n                \"# check output of userdata script with sudo tail -f /var/log/cloud-init-output.log\\n\",\n                \"sudo yum install docker iptables -y\\n\",\n                \"sudo iptables -A OUTPUT -m owner ! --uid-owner root -d 169.254.169.254 -j DROP\\n\",\n                \"sudo systemctl enable docker\\n\",\n                \"sudo systemctl start docker\\n\",\n                \"mkdir -p /home/ec2-user/anythingllm\\n\",\n                \"touch /home/ec2-user/anythingllm/.env\\n\",\n                \"sudo chown ec2-user:ec2-user -R /home/ec2-user/anythingllm\\n\",\n                \"docker pull mintplexlabs/anythingllm\\n\",\n                \"docker run -d -p 3001:3001 --cap-add SYS_ADMIN -v /home/ec2-user/anythingllm:/app/server/storage -v /home/ec2-user/anythingllm/.env:/app/server/.env -e STORAGE_DIR=\\\"/app/server/storage\\\" mintplexlabs/anythingllm\\n\",\n                \"echo \\\"Container ID: $(sudo docker ps --latest --quiet)\\\"\\n\",\n                \"export ONLINE=$(curl -Is http://localhost:3001/api/ping | head -n 1|cut -d$' ' -f2)\\n\",\n                \"echo \\\"Health check: $ONLINE\\\"\\n\",\n                \"echo \\\"Setup complete! AnythingLLM instance is now online!\\\"\\n\",\n                \"\\n\",\n                \"--//--\\n\"\n              ]\n            ]\n          }\n        }\n      }\n    },\n    \"AnythingLLMInstanceSecurityGroup\": {\n      \"Type\": \"AWS::EC2::SecurityGroup\",\n      \"Properties\": {\n        \"GroupDescription\": \"AnythingLLM Instance Security Group\",\n        \"SecurityGroupIngress\": [\n          {\n            \"IpProtocol\": \"tcp\",\n            \"FromPort\": \"22\",\n            \"ToPort\": \"22\",\n            \"CidrIp\": \"0.0.0.0/0\"\n          },\n          {\n            \"IpProtocol\": \"tcp\",\n            \"FromPort\": \"3001\",\n            \"ToPort\": \"3001\",\n            \"CidrIp\": \"0.0.0.0/0\"\n          },\n          {\n            \"IpProtocol\": \"tcp\",\n            \"FromPort\": \"3001\",\n            \"ToPort\": \"3001\",\n            \"CidrIpv6\": \"::/0\"\n          }\n        ]\n      }\n    }\n  },\n  \"Outputs\": {\n    \"ServerIp\": {\n      \"Description\": \"IP address of the AnythingLLM instance\",\n      \"Value\": {\n        \"Fn::GetAtt\": [\n          \"AnythingLLMInstance\",\n          \"PublicIp\"\n        ]\n      }\n    },\n    \"ServerURL\": {\n      \"Description\": \"URL of the AnythingLLM server\",\n      \"Value\": {\n        \"Fn::Join\": [\n          \"\",\n          [\n            \"http://\",\n            {\n              \"Fn::GetAtt\": [\n                \"AnythingLLMInstance\",\n                \"PublicIp\"\n              ]\n            },\n            \":3001\"\n          ]\n        ]\n      }\n    }\n  },\n  \"Mappings\": {\n    \"Region2AMI\": {\n      \"ap-south-1\": {\n        \"AMI\": \"ami-0e6329e222e662a52\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"eu-north-1\": {\n        \"AMI\": \"ami-08c308b1bb265e927\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"eu-west-3\": {\n        \"AMI\": \"ami-069d1ea6bc64443f0\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"eu-west-2\": {\n        \"AMI\": \"ami-06a566ca43e14780d\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"eu-west-1\": {\n        \"AMI\": \"ami-0a8dc52684ee2fee2\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"ap-northeast-3\": {\n        \"AMI\": \"ami-0c8a89b455fae8513\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"ap-northeast-2\": {\n        \"AMI\": \"ami-0ff56409a6e8ea2a0\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"ap-northeast-1\": {\n        \"AMI\": \"ami-0ab0bbbd329f565e6\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"ca-central-1\": {\n        \"AMI\": \"ami-033c256a10931f206\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"sa-east-1\": {\n        \"AMI\": \"ami-0dabf4dab6b183eef\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"ap-southeast-1\": {\n        \"AMI\": \"ami-0dc5785603ad4ff54\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"ap-southeast-2\": {\n        \"AMI\": \"ami-0c5d61202c3b9c33e\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"eu-central-1\": {\n        \"AMI\": \"ami-004359656ecac6a95\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"us-east-1\": {\n        \"AMI\": \"ami-0cff7528ff583bf9a\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"us-east-2\": {\n        \"AMI\": \"ami-02238ac43d6385ab3\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"us-west-1\": {\n        \"AMI\": \"ami-01163e76c844a2129\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      },\n      \"us-west-2\": {\n        \"AMI\": \"ami-0ceecbb0f30a902a6\",\n        \"RootDeviceName\": \"/dev/xvda\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "cloud-deployments/digitalocean/terraform/DEPLOY.md",
    "content": "# How to deploy a private AnythingLLM instance on DigitalOcean using Terraform\n\nWith a DigitalOcean account, you can easily deploy a private AnythingLLM instance using Terraform. This will create a URL that you can access from any browser over HTTP (HTTPS not supported). This single instance will run on your own keys, and they will not be exposed. However, if you want your instance to be protected, it is highly recommended that you set a password once setup is complete.\n\nThe output of this Terraform configuration will be:\n- 1 DigitalOcean Droplet\n- An IP address to access your application\n\n**Requirements**\n- An DigitalOcean  account with billing information\n- Terraform installed on your local machine\n  - Follow the instructions in the [official Terraform documentation](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli) for your operating system.\n\n## How to deploy on DigitalOcean\nOpen your terminal and navigate to the `docker` folder\n1. Create a `.env` file by cloning the `.env.example`. \n2. Navigate to `digitalocean/terraform` folder.\n3. Replace the token value in the provider \"digitalocean\" block in main.tf with your DigitalOcean API token.\n4. Run the following commands to initialize Terraform, review the infrastructure changes, and apply them:\n    ```\n    terraform init  \n    terraform plan  \n    terraform apply  \n    ```\nConfirm the changes by typing yes when prompted.\n5. Once the deployment is complete, Terraform will output the public IP address of your droplet. You can access your application using this IP address.\n\n## How to deploy on DigitalOcean\nTo delete the resources created by Terraform, run the following command in the terminal:\n`\nterraform destroy  \n`\n\n## Please read this notice before submitting issues about your deployment\n\n**Note:** \nYour instance will not be available instantly. Depending on the instance size you launched with it can take anywhere from 5-10 minutes to fully boot up.\n\nIf you want to check the instances progress, navigate to [your deployed instances](https://cloud.digitalocean.com/droplets) and connect to your instance via SSH in browser.\n\nOnce connected run `sudo tail -f /var/log/cloud-init-output.log` and wait for the file to conclude deployment of the docker image.\n\n\nAdditionally, your use of this deployment process means you are responsible for any costs of these Digital Ocean resources fully.\n"
  },
  {
    "path": "cloud-deployments/digitalocean/terraform/main.tf",
    "content": "terraform {\n  required_version = \">= 1.0.0\"\n\n  required_providers {\n    digitalocean = {\n      source  = \"digitalocean/digitalocean\"\n      version = \"~> 2.0\"\n    }\n  }\n}\n\nprovider \"digitalocean\" {  \n  # Add your DigitalOcean API token here  \n  token = \"DigitalOcean API token\"  \n}  \n\n  \nresource \"digitalocean_droplet\" \"anything_llm_instance\" {  \n  image  = \"ubuntu-24-04-x64\"  \n  name   = \"anything-llm-instance\"  \n  region = \"nyc3\"  \n  size   = \"s-2vcpu-2gb\"  \n  \n  user_data = templatefile(\"user_data.tp1\", {   \n    env_content = local.formatted_env_content \n  })\n}  \n\nlocals {  \n  env_content = file(\"../../../docker/.env\")  \n  formatted_env_content = join(\"\\n\", [  \n    for line in split(\"\\n\", local.env_content) :  \n    line  \n    if !(  \n      (  \n        substr(line, 0, 1) == \"#\"  \n      ) ||  \n      (  \n        substr(line, 0, 3) == \"UID\"  \n      ) ||  \n      (  \n        substr(line, 0, 3) == \"GID\"  \n      ) ||  \n      (  \n        substr(line, 0, 11) == \"CLOUD_BUILD\"  \n      ) ||  \n      (  \n        line == \"\"  \n      )  \n    )  \n  ])  \n}"
  },
  {
    "path": "cloud-deployments/digitalocean/terraform/outputs.tf",
    "content": "output \"ip_address\" {\n  value = digitalocean_droplet.anything_llm_instance.ipv4_address\n  description = \"The public IP address of your droplet application.\"\n}"
  },
  {
    "path": "cloud-deployments/digitalocean/terraform/user_data.tp1",
    "content": "#!/bin/bash  \n# check output of userdata script with sudo tail -f /var/log/cloud-init-output.log \n  \nsudo apt-get update  \nsudo apt-get install -y docker.io  \nsudo usermod -a -G docker ubuntu\n  \nsudo systemctl enable docker  \nsudo systemctl start docker  \n  \nmkdir -p /home/anythingllm\ncat <<EOF >/home/anythingllm/.env\n${env_content}\nEOF\n\nsudo docker pull mintplexlabs/anythingllm\nsudo docker run -d -p 3001:3001 --cap-add SYS_ADMIN -v /home/anythingllm:/app/server/storage -v /home/anythingllm/.env:/app/server/.env -e STORAGE_DIR=\"/app/server/storage\" mintplexlabs/anythingllm\necho \"Container ID: $(sudo docker ps --latest --quiet)\"  \n  \nexport ONLINE=$(curl -Is http://localhost:3001/api/ping | head -n 1|cut -d$' ' -f2)  \necho \"Health check: $ONLINE\"  \necho \"Setup complete! AnythingLLM instance is now online!\"  \n"
  },
  {
    "path": "cloud-deployments/gcp/deployment/DEPLOY.md",
    "content": "# How to deploy a private AnythingLLM instance on GCP\n\nWith a GCP account you can easily deploy a private AnythingLLM instance on GCP. This will create a url that you can access from any browser over HTTP (HTTPS not supported). This single instance will run on your own keys and they will not be exposed - however if you want your instance to be protected it is highly recommend that you set a password once setup is complete.\n\nThe output of this cloudformation stack will be:\n- 1 GCP VM\n- 1 Security Group with 0.0.0.0/0 access on Ports 22 & 3001\n- 1 GCP VM Volume `gb2` of 10Gib minimum\n\n**Requirements**\n- An GCP account with billing information.\n\n## How to deploy on GCP\nOpen your terminal\n1. Log in to your GCP account using the following command:\n    ```\n    gcloud auth login \n    ```\n\n2. After successful login, Run the following command to create a deployment using the Deployment Manager CLI:\n\n  ```\n\n  gcloud deployment-manager deployments create anything-llm-deployment --config gcp/deployment/gcp_deploy_anything_llm.yaml\n\n  ```\n\nOnce you execute these steps, the CLI will initiate the deployment process on GCP based on your configuration file. You can monitor the deployment status and view the outputs using the Google Cloud Console or the Deployment Manager CLI commands.\n\n```\ngcloud compute instances get-serial-port-output anything-llm-instance \n```\n\nssh into the instance\n\n```\ngcloud compute ssh anything-llm-instance \n```\n\nDelete the deployment\n```\ngcloud deployment-manager deployments delete anything-llm-deployment \n```\n\n## Please read this notice before submitting issues about your deployment\n\n**Note:** \nYour instance will not be available instantly. Depending on the instance size you launched with it can take anywhere from 5-10 minutes to fully boot up.\n\nIf you want to check the instances progress, navigate to [your deployed instances](https://console.cloud.google.com/compute/instances) and connect to your instance via SSH in browser.\n\nOnce connected run `sudo tail -f /var/log/cloud-init-output.log` and wait for the file to conclude deployment of the docker image.\n\nAdditionally, your use of this deployment process means you are responsible for any costs of these GCP resources fully.\n"
  },
  {
    "path": "cloud-deployments/gcp/deployment/gcp_deploy_anything_llm.yaml",
    "content": "resources:  \n  - name: anything-llm-instance  \n    type: compute.v1.instance  \n    properties:  \n      zone: us-central1-a  \n      machineType: zones/us-central1-a/machineTypes/n1-standard-1  \n      disks:  \n        - deviceName: boot  \n          type: PERSISTENT  \n          boot: true  \n          autoDelete: true  \n          initializeParams:  \n            sourceImage: projects/ubuntu-os-cloud/global/images/family/ubuntu-2004-lts  \n            diskSizeGb: 10  \n      networkInterfaces:  \n        - network: global/networks/default  \n          accessConfigs:  \n            - name: External NAT  \n              type: ONE_TO_ONE_NAT  \n      metadata:  \n        items:  \n          - key: startup-script  \n            value: |  \n              #!/bin/bash  \n              # check output of userdata script with sudo tail -f /var/log/cloud-init-output.log  \n\n              sudo apt-get update  \n              sudo apt-get install -y docker.io  \n              sudo usermod -a -G docker ubuntu\n              sudo systemctl enable docker  \n              sudo systemctl start docker  \n\n              mkdir -p /home/anythingllm\n              touch /home/anythingllm/.env\n              sudo chown -R ubuntu:ubuntu /home/anythingllm\n               \n              sudo docker pull mintplexlabs/anythingllm\n              sudo docker run -d -p 3001:3001 --cap-add SYS_ADMIN -v /home/anythingllm:/app/server/storage -v /home/anythingllm/.env:/app/server/.env -e STORAGE_DIR=\"/app/server/storage\" mintplexlabs/anythingllm\n              echo \"Container ID: $(sudo docker ps --latest --quiet)\"  \n\n              export ONLINE=$(curl -Is http://localhost:3001/api/ping | head -n 1|cut -d$' ' -f2)  \n              echo \"Health check: $ONLINE\"  \n\n              echo \"Setup complete! AnythingLLM instance is now online!\"  \n\n"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/.helmignore",
    "content": "# Patterns to ignore when building packages.\n# This supports shell glob matching, relative path matching, and\n# negation (prefixed with !). Only one pattern per line.\n.DS_Store\n# Common VCS dirs\n.git/\n.gitignore\n.bzr/\n.bzrignore\n.hg/\n.hgignore\n.svn/\n# Common backup files\n*.swp\n*.bak\n*.tmp\n*.orig\n*~\n# Various IDEs\n.project\n.idea/\n*.tmproj\n.vscode/\n"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/Chart.yaml",
    "content": "apiVersion: v2\nname: anythingllm\ndescription: The all-in-one Desktop & Docker AI application with built-in RAG, AI agents, No-code agent builder, MCP compatibility, and more.\ntype: application\nversion: 1.0.0\nappVersion: \"1.85.0\"\nicon: https://raw.githubusercontent.com/Mintplex-Labs/anything-llm/refs/heads/master/frontend/public/favicon.png"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/README.md",
    "content": "# anythingllm\n\n![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.85.0](https://img.shields.io/badge/AppVersion-1.85.0-informational?style=flat-square)\n\n![AnythingLLM](https://raw.githubusercontent.com/Mintplex-Labs/anything-llm/master/images/wordmark.png)\n\n[AnythingLLM](https://github.com/Mintplex-Labs/anything-llm)\n\nThe all-in-one Desktop & Docker AI application with built-in RAG, AI agents, No-code agent builder, MCP compatibility, and more.\n\n**Configuration & Usage**\n\n- **Config vs Secrets:** This chart exposes application configuration via two mechanisms:\n  - `config` (in `values.yaml`) — rendered into a `ConfigMap` and injected using `envFrom` in the pod. Do NOT place sensitive values (API keys, secrets) in `config` because `ConfigMap`s are not encrypted.\n  - `env` / `envFrom` — the preferred way to inject secrets. Use Kubernetes `Secret` objects and reference them from `env` (with `valueFrom.secretKeyRef`) or `envFrom.secretRef`.\n\n- **Storage & STORAGE_DIR mapping:** The chart creates (or mounts) a `PersistentVolumeClaim` using the `persistentVolume.*` settings. The container mount path is set from `persistentVolume.mountPath`. Ensure the container `STORAGE_DIR` config key matches that path (defaults are set in `values.yaml`).\n\n**Providing API keys & secrets (recommended)**\n\nUse Kubernetes Secrets. Below are example workflows and `values.yaml` snippets.\n\n1) Create a Kubernetes Secret with API keys:\n\n```\nkubectl create secret generic openai-secret --from-literal=OPENAI_KEY=\"sk-...\"\n# or from a file\n# kubectl create secret generic openai-secret --from-file=OPENAI_KEY=/path/to/keyfile\n```\n\n2) Reference the Secret from `values.yaml` using `envFrom` (recommended when your secret contains multiple env keys):\n\n```yaml\nenvFrom:\n  - secretRef:\n      name: openai-secret\n```\n\nThis will inject all key/value pairs from the `openai-secret` Secret as environment variables in the container.\n\n3) Or reference a single secret key via `env` (explicit mapping):\n\n```yaml\nenv:\n  - name: OPENAI_KEY\n    valueFrom:\n      secretKeyRef:\n        name: openai-secret\n        key: OPENAI_KEY\n```\n\nNotes:\n- Avoid placing secret values into `config:` (the chart's `ConfigMap`) — `ConfigMap`s are visible to anyone who can read the namespace. Use `Secret` objects for any credentials/tokens.\n- If you use a GitOps workflow, consider integrating an external secret operator (ExternalSecrets, SealedSecrets, etc.) so you don't store raw secrets in Git.\n\n**Example `values-secret.yaml` to pass during `helm install`**\n\n```yaml\nimage:\n  repository: mintplexlabs/anythingllm\n  tag: \"1.11.2\"\n\nservice:\n  type: ClusterIP\n  port: 3001\n\n# Reference secret containing API keys\nenvFrom:\n  - secretRef:\n      name: openai-secret\n\n# Optionally override other values\npersistentVolume:\n  size: 16Gi\n  mountPath: /storage\n```\n\nInstall with:\n\n```\nhelm install my-anythingllm ./anythingllm -f values-secret.yaml\n```\n\n**Best practices & tips**\n\n- Use `envFrom` for convenience when many environment variables are stored in a single `Secret` and use `env`/`valueFrom` for explicit single-key mappings.\n- Use `kubectl create secret generic` or your secrets management solution. If you need to reference multiple different provider keys (OpenAI, Anthropic, etc.), create a single `Secret` with multiple keys or multiple Secrets and add multiple `envFrom` entries.\n- Keep probe paths and `service.port` aligned. If your probes fail after deployment, check that the probe `port` matches the container port (or named port `http`) and that the `path` is valid.\n- For storage, if you have a pre-existing PVC set `persistentVolume.existingClaim` to the PVC name; the chart will mount that claim (and will not attempt to create a new PVC).\n- For production, provide resource `requests` and `limits` in `values.yaml` to prevent scheduler starvation and to control cost.\n\n## Values\n\n| Key | Type | Default | Description |\n|-----|------|---------|-------------|\n| affinity | object | `{}` |  |\n| config.DISABLE_TELEMETRY | string | `\"true\"` |  |\n| config.GID | string | `\"1000\"` |  |\n| config.NODE_ENV | string | `\"production\"` |  |\n| config.STORAGE_DIR | string | `\"/storage\"` |  |\n| config.UID | string | `\"1000\"` |  |\n| env | object | `{}` |  |\n| envFrom | object | `{}` |  |\n| fullnameOverride | string | `\"\"` |  |\n| image.pullPolicy | string | `\"IfNotPresent\"` |  |\n| image.repository | string | `\"mintplexlabs/anythingllm\"` |  |\n| image.tag | string | `\"1.11.2\"` |  |\n| imagePullSecrets | list | `[]` |  |\n| ingress.annotations | object | `{}` |  |\n| ingress.className | string | `\"\"` |  |\n| ingress.enabled | bool | `false` |  |\n| ingress.hosts[0].host | string | `\"chart-example.local\"` |  |\n| ingress.hosts[0].paths[0].path | string | `\"/\"` |  |\n| ingress.hosts[0].paths[0].pathType | string | `\"ImplementationSpecific\"` |  |\n| ingress.tls | list | `[]` |  |\n| initContainers | list | `[]` |  |\n| livenessProbe.failureThreshold | int | `3` |  |\n| livenessProbe.httpGet.path | string | `\"/v1/api/health\"` |  |\n| livenessProbe.httpGet.port | int | `8888` |  |\n| livenessProbe.initialDelaySeconds | int | `15` |  |\n| livenessProbe.periodSeconds | int | `5` |  |\n| nameOverride | string | `\"\"` |  |\n| nodeSelector | object | `{}` |  |\n| persistentVolume.accessModes[0] | string | `\"ReadWriteOnce\"` |  |\n| persistentVolume.annotations | object | `{}` |  |\n| persistentVolume.existingClaim | string | `\"\"` |  |\n| persistentVolume.labels | object | `{}` |  |\n| persistentVolume.mountPath | string | `\"/storage\"` |  |\n| persistentVolume.size | string | `\"8Gi\"` |  |\n| podAnnotations | object | `{}` |  |\n| podLabels | object | `{}` |  |\n| podSecurityContext.fsGroup | int | `1000` |  |\n| readinessProbe.httpGet.path | string | `\"/v1/api/health\"` |  |\n| readinessProbe.httpGet.port | int | `8888` |  |\n| readinessProbe.initialDelaySeconds | int | `15` |  |\n| readinessProbe.periodSeconds | int | `5` |  |\n| readinessProbe.successThreshold | int | `2` |  |\n| replicaCount | int | `1` |  |\n| resources | object | `{}` |  |\n| securityContext | object | `{}` |  |\n| service.port | int | `3001` |  |\n| service.type | string | `\"ClusterIP\"` |  |\n| serviceAccount.annotations | object | `{}` |  |\n| serviceAccount.automount | bool | `true` |  |\n| serviceAccount.create | bool | `true` |  |\n| serviceAccount.name | string | `\"\"` |  |\n| tolerations | list | `[]` |  |\n| volumeMounts | list | `[]` |  |\n| volumes | list | `[]` |  |"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/README.md.gotmpl",
    "content": "{{ template \"chart.header\" . }}\n{{ template \"chart.deprecationWarning\" . }}\n\n{{ template \"chart.badgesSection\" . }}\n\n![AnythingLLM](https://raw.githubusercontent.com/Mintplex-Labs/anything-llm/master/images/wordmark.png)\n\n[AnythingLLM](https://github.com/Mintplex-Labs/anything-llm)\n\n{{ template \"chart.description\" . }}\n\n{{ template \"chart.homepageLine\" . }}\n\n{{ template \"chart.maintainersSection\" . }}\n\n{{ template \"chart.sourcesSection\" . }}\n\n{{ template \"chart.requirementsSection\" . }}\n\n**Configuration & Usage**\n\n- **Config vs Secrets:** This chart exposes application configuration via two mechanisms:\n  - `config` (in `values.yaml`) — rendered into a `ConfigMap` and injected using `envFrom` in the pod. Do NOT place sensitive values (API keys, secrets) in `config` because `ConfigMap`s are not encrypted.\n  - `env` / `envFrom` — the preferred way to inject secrets. Use Kubernetes `Secret` objects and reference them from `env` (with `valueFrom.secretKeyRef`) or `envFrom.secretRef`.\n\n- **Storage & STORAGE_DIR mapping:** The chart creates (or mounts) a `PersistentVolumeClaim` using the `persistentVolume.*` settings. The container mount path is set from `persistentVolume.mountPath`. Ensure the container `STORAGE_DIR` config key matches that path (defaults are set in `values.yaml`).\n\n\n**Providing API keys & secrets (recommended)**\n\nUse Kubernetes Secrets. Below are example workflows and `values.yaml` snippets.\n\n1) Create a Kubernetes Secret with API keys:\n\n```\nkubectl create secret generic openai-secret --from-literal=OPENAI_KEY=\"sk-...\" \n# or from a file\n# kubectl create secret generic openai-secret --from-file=OPENAI_KEY=/path/to/keyfile\n```\n\n2) Reference the Secret from `values.yaml` using `envFrom` (recommended when your secret contains multiple env keys):\n\n```yaml\nenvFrom:\n  - secretRef:\n      name: openai-secret\n```\n\nThis will inject all key/value pairs from the `openai-secret` Secret as environment variables in the container.\n\n3) Or reference a single secret key via `env` (explicit mapping):\n\n```yaml\nenv:\n  - name: OPENAI_KEY\n    valueFrom:\n      secretKeyRef:\n        name: openai-secret\n        key: OPENAI_KEY\n```\n\nNotes:\n- Avoid placing secret values into `config:` (the chart's `ConfigMap`) — `ConfigMap`s are visible to anyone who can read the namespace. Use `Secret` objects for any credentials/tokens.\n- If you use a GitOps workflow, consider integrating an external secret operator (ExternalSecrets, SealedSecrets, etc.) so you don't store raw secrets in Git.\n\n\n**Example `values-secret.yaml` to pass during `helm install`**\n\n```yaml\nimage:\n  repository: mintplexlabs/anythingllm\n  tag: \"1.11.2\"\n\nservice:\n  type: ClusterIP\n  port: 3001\n\n# Reference secret containing API keys\nenvFrom:\n  - secretRef:\n      name: openai-secret\n\n# Optionally override other values\npersistentVolume:\n  size: 16Gi\n  mountPath: /storage\n```\n\nInstall with:\n\n```\nhelm install my-anythingllm ./anythingllm -f values-secret.yaml\n```\n\n**Best practices & tips**\n\n- Use `envFrom` for convenience when many environment variables are stored in a single `Secret` and use `env`/`valueFrom` for explicit single-key mappings.\n- Use `kubectl create secret generic` or your secrets management solution. If you need to reference multiple different provider keys (OpenAI, Anthropic, etc.), create a single `Secret` with multiple keys or multiple Secrets and add multiple `envFrom` entries.\n- Keep probe paths and `service.port` aligned. If your probes fail after deployment, check that the probe `port` matches the container port (or named port `http`) and that the `path` is valid.\n- For storage, if you have a pre-existing PVC set `persistentVolume.existingClaim` to the PVC name; the chart will mount that claim (and will not attempt to create a new PVC).\n- For production, provide resource `requests` and `limits` in `values.yaml` to prevent scheduler starvation and to control cost.\n\n{{ template \"chart.valuesSection\" . }}"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/templates/NOTES.txt",
    "content": "1. Get the application URL by running these commands:\n\n{{- if .Values.ingress.enabled }}\n{{- range $host := .Values.ingress.hosts }}\n  {{- range .paths }}\n  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}\n  {{- end }}\n{{- end }}\n\n{{- else if contains \"NodePort\" .Values.service.type }}\n  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath=\"{.spec.ports[0].nodePort}\" services {{ include \"anythingllm.fullname\" . }})\n  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath=\"{.items[0].status.addresses[0].address}\")\n  echo http://$NODE_IP:$NODE_PORT\n\n{{- else if contains \"LoadBalancer\" .Values.service.type }}\n  NOTE: It may take a few minutes for the LoadBalancer IP to be available.\n        You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include \"anythingllm.fullname\" . }}'\n  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include \"anythingllm.fullname\" . }} --template \"{{\"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}\"}}\")\n  echo http://$SERVICE_IP:{{ .Values.service.port }}\n\n{{- else if contains \"ClusterIP\" .Values.service.type }}\n  export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l \"app.kubernetes.io/name={{ include \"anythingllm.name\" . }},app.kubernetes.io/instance={{ .Release.Name }}\" -o jsonpath=\"{.items[0].metadata.name}\")\n  export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath=\"{.spec.containers[0].ports[0].containerPort}\")\n  echo \"To access locally, run:\"\n  echo \"  kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT\"\n  echo \"Then visit http://127.0.0.1:8080\"\n\n{{- end }}\n"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/templates/_helpers.tpl",
    "content": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"anythingllm.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCreate a default fully qualified app name.\nWe truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).\nIf release name contains chart name it will be used as a full name.\n*/}}\n{{- define \"anythingllm.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"anythingllm.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"anythingllm.labels\" -}}\nhelm.sh/chart: {{ include \"anythingllm.chart\" . }}\n{{ include \"anythingllm.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"anythingllm.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"anythingllm.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}\n\n{{/*\nCreate the name of the service account to use\n*/}}\n{{- define \"anythingllm.serviceAccountName\" -}}\n{{- if .Values.serviceAccount.create }}\n{{- default (include \"anythingllm.fullname\" .) .Values.serviceAccount.name }}\n{{- else }}\n{{- default \"default\" .Values.serviceAccount.name }}\n{{- end }}\n{{- end }}\n"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/templates/configmap.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  labels:\n    {{- include \"anythingllm.labels\" . | nindent 4 }}\n  name: {{ include \"anythingllm.fullname\" . }}-config\ndata:\n{{- range $key, $value := .Values.config }}\n  {{ $key }}: \"{{ $value }}\"\n{{- end }}"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/templates/deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"anythingllm.fullname\" . }}\n  labels:\n    {{- include \"anythingllm.labels\" . | nindent 4 }}\nspec:\n  replicas: {{ .Values.replicaCount }}\n  selector:\n    matchLabels:\n      {{- include \"anythingllm.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      {{- with .Values.podAnnotations }}\n      annotations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      labels:\n        {{- include \"anythingllm.labels\" . | nindent 8 }}\n        {{- with .Values.podLabels }}\n        {{- toYaml . | nindent 8 }}\n        {{- end }}\n    spec:\n      {{- with .Values.imagePullSecrets }}\n      imagePullSecrets:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      serviceAccountName: {{ include \"anythingllm.serviceAccountName\" . }}\n      securityContext:\n        {{- toYaml .Values.podSecurityContext | nindent 8 }}\n      {{- with .Values.initContainers }}\n      initContainers:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.strategy }}\n      strategy:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      containers:\n        - name: {{ .Chart.Name }}\n          securityContext:\n            {{- toYaml .Values.securityContext | nindent 12 }}\n          image: \"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n          imagePullPolicy: {{ .Values.image.pullPolicy }}\n          {{- with .Values.env }}\n          env:\n            {{- toYaml . | nindent 12 }}\n          {{- end }}\n          envFrom:\n            - configMapRef:\n                name: {{ include \"anythingllm.fullname\" . }}-config\n          {{- with .Values.envFrom }}\n            {{- toYaml . | nindent 12 }}\n          {{- end }}\n          ports:\n            - name: http\n              containerPort: {{ .Values.service.port }}\n              protocol: TCP\n          livenessProbe:\n            {{- toYaml .Values.livenessProbe | nindent 12 }}\n          readinessProbe:\n            {{- toYaml .Values.readinessProbe | nindent 12 }}\n          resources:\n            {{- toYaml .Values.resources | nindent 12 }}\n          volumeMounts:\n            - name: storage\n              mountPath: {{ .Values.persistentVolume.mountPath }}\n      volumes:\n        - name: storage\n          persistentVolumeClaim:\n            claimName: {{ include \"anythingllm.fullname\" . }}-storage-claim\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/templates/extra-objects.yaml",
    "content": "{{ range .Values.extraObjects }}\n---\n{{ tpl (toYaml .) $ }}\n{{ end }}\n"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/templates/ingress.yaml",
    "content": "{{- if .Values.ingress.enabled -}}\n{{- $fullName := include \"anythingllm.fullname\" . -}}\n{{- $svcPort := .Values.service.port -}}\n{{- if and .Values.ingress.className (not (semverCompare \">=1.18-0\" .Capabilities.KubeVersion.GitVersion)) }}\n  {{- if not (hasKey .Values.ingress.annotations \"kubernetes.io/ingress.class\") }}\n  {{- $_ := set .Values.ingress.annotations \"kubernetes.io/ingress.class\" .Values.ingress.className}}\n  {{- end }}\n{{- end }}\n{{- if semverCompare \">=1.19-0\" .Capabilities.KubeVersion.GitVersion -}}\napiVersion: networking.k8s.io/v1\n{{- else if semverCompare \">=1.14-0\" .Capabilities.KubeVersion.GitVersion -}}\napiVersion: networking.k8s.io/v1beta1\n{{- else -}}\napiVersion: extensions/v1beta1\n{{- end }}\nkind: Ingress\nmetadata:\n  name: {{ $fullName }}\n  labels:\n    {{- include \"anythingllm.labels\" . | nindent 4 }}\n  {{- with .Values.ingress.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  {{- if and .Values.ingress.className (semverCompare \">=1.18-0\" .Capabilities.KubeVersion.GitVersion) }}\n  ingressClassName: {{ .Values.ingress.className }}\n  {{- end }}\n  {{- if .Values.ingress.tls }}\n  tls:\n    {{- range .Values.ingress.tls }}\n    - hosts:\n        {{- range .hosts }}\n        - {{ . | quote }}\n        {{- end }}\n      secretName: {{ .secretName }}\n    {{- end }}\n  {{- end }}\n  rules:\n    {{- range .Values.ingress.hosts }}\n    - host: {{ .host | quote }}\n      http:\n        paths:\n          {{- range .paths }}\n          - path: {{ .path }}\n            {{- if and .pathType (semverCompare \">=1.18-0\" $.Capabilities.KubeVersion.GitVersion) }}\n            pathType: {{ .pathType }}\n            {{- end }}\n            backend:\n              {{- if semverCompare \">=1.19-0\" $.Capabilities.KubeVersion.GitVersion }}\n              service:\n                name: {{ $fullName }}\n                port:\n                  number: {{ $svcPort }}\n              {{- else }}\n              serviceName: {{ $fullName }}\n              servicePort: {{ $svcPort }}\n              {{- end }}\n          {{- end }}\n    {{- end }}\n{{- end }}\n"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/templates/pvc.yaml",
    "content": "apiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  {{- if .Values.persistentVolume.annotations }}\n  annotations:\n{{ toYaml .Values.persistentVolume.annotations | indent 4 }}\n  {{- end }}\n  labels:\n    {{- include \"anythingllm.labels\" . | nindent 4 }}\n    {{- with .Values.persistentVolume.labels }}\n       {{- toYaml . | nindent 4 }}\n    {{- end }}\n  name: {{ include \"anythingllm.fullname\" . }}-storage-claim\nspec:\n  accessModes:\n    {{- toYaml .Values.persistentVolume.accessModes | nindent 4 }}\n{{- if .Values.persistentVolume.storageClass }}\n{{- if (eq \"-\" .Values.persistentVolume.storageClass) }}\n  storageClassName: \"\"\n{{- else }}\n  storageClassName: \"{{ .Values.persistentVolume.storageClass }}\"\n{{- end }}\n{{- end }}\n  resources:\n    requests:\n      storage: {{ .Values.persistentVolume.size }}\n{{- if .Values.persistentVolume.volumeName }}\n  volumeName: \"{{ .Values.persistentVolume.volumeName }}\"\n{{- end -}}\n  {{- with .Values.persistentVolume.selector }}\n  selector:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/templates/service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"anythingllm.fullname\" . }}\n  labels:\n    {{- include \"anythingllm.labels\" . | nindent 4 }}\nspec:\n  type: {{ .Values.service.type }}\n  ports:\n    - port: {{ .Values.service.port }}\n      targetPort: http\n      protocol: TCP\n      name: http\n  selector:\n    {{- include \"anythingllm.selectorLabels\" . | nindent 4 }}\n"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/templates/serviceaccount.yaml",
    "content": "{{- if .Values.serviceAccount.create -}}\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {{ include \"anythingllm.serviceAccountName\" . }}\n  labels:\n    {{- include \"anythingllm.labels\" . | nindent 4 }}\n  {{- with .Values.serviceAccount.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nautomountServiceAccountToken: {{ .Values.serviceAccount.automount }}\n{{- end }}\n"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/templates/tests/test-connection.yaml",
    "content": "apiVersion: v1\nkind: Pod\nmetadata:\n  name: \"{{ include \"anythingllm.fullname\" . }}-test-connection\"\n  labels:\n    {{- include \"anythingllm.labels\" . | nindent 4 }}\n  annotations:\n    \"helm.sh/hook\": test\nspec:\n  containers:\n    - name: healthcheck\n      image: curlimages/curl:8.1.2\n      command: [\"sh\", \"-c\"]\n      args:\n        - \"curl -fsS -o /dev/null http://{{ include \"anythingllm.fullname\" . }}:{{ .Values.service.port }}|| exit 1\"\n  restartPolicy: Never"
  },
  {
    "path": "cloud-deployments/helm/charts/anythingllm/values.yaml",
    "content": "replicaCount: 1\n\ninitContainers: []\n  # - name: init-myservice\n  #   image: busybox\n  #   command: ['sh', '-c', 'chown -R 1000:1000 /storage']\n\nimage:\n  repository: mintplexlabs/anythingllm\n  pullPolicy: IfNotPresent\n  tag: \"1.11.2\"\n\nimagePullSecrets: []\nnameOverride: \"\"\nfullnameOverride: \"\"\n\npersistentVolume:\n  # AnythingLLM storage data Persistent Volume access modes\n  # Must match those of existing PV or dynamic provisioner\n  # Ref: http://kubernetes.io/docs/user-guide/persistent-volumes/\n  #\n  accessModes:\n    - ReadWriteOnce\n\n  # AnythingLLM storage data Persistent Volume labels\n  #\n  labels: {}\n\n  # AnythingLLM storage data Persistent Volume annotations\n  #\n  annotations: {}\n\n  # AnythingLLM storage data Persistent Volume  existing claim name\n  # If defined, PVC must be created manually before volume will be bound\n  #\n  existingClaim: \"\"\n\n  # AnythingLLM storage data Persistent Volume size\n  #\n  size: 8Gi\n\n  # AnythingLLM storage data Persistent Volume mount path\n  # Must match the STORAGE_DIR config value\n  #\n  mountPath: /app/server/storage\n\n  # AnythingLLM storage data Persistent Volume Storage Class\n  # If defined, storageClassName: <storageClass>\n  # If set to \"-\", storageClassName: \"\", which disables dynamic provisioning\n  # If undefined (the default) or set to null, no storageClassName spec is\n  #   set, choosing the default provisioner.  (gp2 on AWS, standard on\n  #   GKE, AWS & OpenStack)\n  #\n  storageClass: \"\"\n\n  # AnythingLLM storage data Persistent Volume Claim Selector\n  # Useful if Persistent Volumes have been provisioned in advance\n  # Ref: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#selector\n  #\n  selector: {}\n  # selector:\n  #  matchLabels:\n  #    release: \"stable\"\n  #  matchExpressions:\n  #    - { key: environment, operator: In, values: [ dev ] }\n  \n  # AnythingLLM storage data Persistent Volume Name\n  # Useful if Persistent Volumes have been provisioned in advance and you want to use a specific one\n  #\n  volumeName: \"\"\n\nserviceAccount:\n  # Specifies whether a service account should be created\n  create: true\n  # Automatically mount a ServiceAccount's API credentials?\n  automount: true\n  # Annotations to add to the service account\n  annotations: {}\n  # The name of the service account to use.\n  # If not set and create is true, a name is generated using the fullname template\n  name: \"\"\n\n# The Anything LLM application  deployment strategy\n# This is set to \"Recreate\" by default as AnythingLLM support is not yet\n# production ready.  Once it is, this can be changed to \"RollingUpdate\"\n# Ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy\n#\nstrategy:\n  # Type of deployment. Can be \"Recreate\" or \"RollingUpdate\". Default is \"Recreate\"\n  type: Recreate\n  # If type is \"RollingUpdate\", the following values can be set:\n  # rollingUpdate:\n  #   maxUnavailable: 1\n  #   maxSurge: 1\n\npodAnnotations: {}\npodLabels: {}\n\npodSecurityContext:\n  # fsGroup needs to be set as the same as the uid/gid used to run the application\n  # in order to have the right permissions on mounted volumes\n  fsGroup: 1000\n\nsecurityContext: {}\n  # capabilities:\n  #   drop:\n  #   - ALL\n  # readOnlyRootFilesystem: true\n  # runAsNonRoot: true\n  # runAsUser: 1000\n\n# AnythingLLM configuration options, these are stored in a ConfigMap and passed\n# to the container as environment variables.\n# See https://github.com/Mintplex-Labs/anything-llm/blob/render/docker/.env.example\n# for all available environment variables to use as configuration options\n#\nconfig:\n  DISABLE_TELEMETRY: \"true\"\n  NODE_ENV: production\n  STORAGE_DIR: /app/server/storage\n  UID: \"1000\"\n  GID: \"1000\"\n\n# The preferred method for setting secret environment variables\n# Ref: https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure/#define-a-container-environment-variable-with-data-from-a-single-secret\n#\nenv: {}\n# - name: OPEN_AI_KEY\n#   valueFrom:\n#     secretKeyRef:\n#       name: openai-secret\n#       key: openai_key\n\n# Typically used to reference a pre existing Secret containing multiple environment variables\n# Ref: https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure/#define-a-container-environment-variable-with-data-from-a-single-secret\n#\nenvFrom: {}\n  # - secretRef:\n  #     name: mysecret\n\nservice:\n  type: ClusterIP\n  port: 3001\n\ningress:\n  enabled: false\n  className: \"\"\n  annotations: {}\n    # kubernetes.io/ingress.class: nginx\n    # kubernetes.io/tls-acme: \"true\"\n  hosts:\n    - host: chart-example.local\n      paths:\n        - path: /\n          pathType: ImplementationSpecific\n  tls: []\n  #  - secretName: chart-example-tls\n  #    hosts:\n  #      - chart-example.local\n\nresources: {}\n  # We usually recommend not to specify default resources and to leave this as a conscious\n  # choice for the user. This also increases chances charts run on environments with little\n  # resources, such as Minikube. If you do want to specify resources, uncomment the following\n  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.\n  # limits:\n  #   cpu: 100m\n  #   memory: 128Mi\n  # requests:\n  #   cpu: 100m\n  #   memory: 128Mi\n\nreadinessProbe:\n  httpGet:\n    path: /v1/api/health\n    port: 8888\n  initialDelaySeconds: 15\n  periodSeconds: 5\n  successThreshold: 2\nlivenessProbe:\n  httpGet:\n    path: /v1/api/health\n    port: 8888\n  initialDelaySeconds: 15\n  periodSeconds: 5\n  failureThreshold: 3\n\n# Additional volumes on the output Deployment definition.\n#\nvolumes: []\n# - name: foo\n#   secret:\n#     secretName: mysecret\n#     optional: false\n\n# Additional volumeMounts on the output Deployment definition.\n#\nvolumeMounts: []\n# - name: foo\n#   mountPath: \"/etc/foo\"\n#   readOnly: true\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n\n## Array of extra manifests/obhects to create\n#\nextraObjects: []\n# - apiVersion: external-secrets.io/v1beta1\n#   kind: ExternalSecret\n#   metadata:\n#     name: open-ai-api-key-external-secret\n#     namespace: default\n#   spec:\n#     refreshInterval: 1h\n#     secretStoreRef:\n#       name: vault\n#       kind: ClusterSecretStore\n#     target:\n#       name: open-ai-api-key-secret\n#       template:\n#         type: Opaque\n#     data:\n#       - secretKey: open_ai_key\n#         remoteRef:\n#           key: secret/data/anything-llm\n#           property: open_ai_key\n\n"
  },
  {
    "path": "cloud-deployments/huggingface-spaces/Dockerfile",
    "content": "# With this dockerfile in a Huggingface space you will get an entire AnythingLLM instance running\n# in your space with all features you would normally get from the docker based version of AnythingLLM.\n#\n# How to use\n# - Login to https://huggingface.co/spaces\n# - Click on \"Create new Space\"\n# - Name the space and select \"Docker\" as the SDK w/ a blank template\n# - The default 2vCPU/16GB machine is OK. The more the merrier.\n# - Decide if you want your AnythingLLM Space public or private.\n#   **You might want to stay private until you at least set a password or enable multi-user mode**\n# - Click \"Create Space\"\n# - Click on \"Settings\" on top of page (https://huggingface.co/spaces/<username>/<space-name>/settings)\n# - Scroll to \"Persistent Storage\" and select the lowest tier of now - you can upgrade if you run out.\n# - Confirm and continue storage upgrade\n# - Go to \"Files\" Tab (https://huggingface.co/spaces/<username>/<space-name>/tree/main)\n# - Click \"Add Files\"\n# - Upload this file or create a file named `Dockerfile` and copy-paste this content into it. \"Commit to main\" and save.\n# - Your container will build and boot. You now have AnythingLLM on HuggingFace. Your data is stored in the persistent storage attached.\n# Have Fun 🤗 \n# Have issues? Check the logs on HuggingFace for clues.\nFROM mintplexlabs/anythingllm:render\n\nUSER root\nRUN mkdir -p /data/storage\nRUN ln -s /data/storage /storage\nUSER anythingllm\n\nENV STORAGE_DIR=\"/data/storage\"\nENV SERVER_PORT=7860\n\nENTRYPOINT [\"/bin/bash\", \"/usr/local/bin/render-entrypoint.sh\"]"
  },
  {
    "path": "cloud-deployments/k8/manifest.yaml",
    "content": "---\napiVersion: v1                                                                                                                                           \nkind: PersistentVolume                                                                                                                                   \nmetadata:                                                                                                                                                \n  name: anything-llm-volume                                                                                                                              \n  annotations:                                                                                                                                           \n    pv.beta.kubernetes.io/uid: \"1000\"                                                                                                                    \n    pv.beta.kubernetes.io/gid: \"1000\"                                                                                                                    \nspec:                                                                                                                                                    \n  storageClassName: gp2                                                                                                                                  \n  capacity:                                                                                                                                              \n    storage: 5Gi                                                                                                                                        \n  accessModes:                                                                                                                                           \n    - ReadWriteOnce                                                                                                                                      \n  awsElasticBlockStore:    \n    # This is the volume UUID from AWS EC2 EBS Volumes list.                                                                                                                              \n    volumeID: \"{{ anythingllm_awsElasticBlockStore_volumeID }}\"                                                                                                                           \n    fsType: ext4\n  nodeAffinity:                                                                                                                                          \n    required:                                                                                                                                            \n      nodeSelectorTerms:                                                                                                                                 \n      - matchExpressions:                                                                                                                                \n        - key: topology.kubernetes.io/zone                                                                                                               \n          operator: In                                                                                                                                   \n          values:                                                                                                                                        \n          - us-east-1c  \n---\napiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: anything-llm-volume-claim\n  namespace: \"{{ namespace }}\"\nspec:\n  accessModes:\n    - ReadWriteOnce\n  resources:\n    requests:\n      storage: 5Gi\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: anything-llm\n  namespace: \"{{ namespace }}\"\n  labels:\n    anything-llm: \"true\"\nspec:\n  selector:\n    matchLabels:\n      k8s-app: anything-llm\n  replicas: 1\n  strategy:\n    type: RollingUpdate\n    rollingUpdate:\n      maxSurge: 0%\n      maxUnavailable: 100%\n  template:\n    metadata:\n      labels:\n        anything-llm: \"true\"\n        k8s-app: anything-llm\n        app.kubernetes.io/name: anything-llm\n        app.kubernetes.io/part-of: anything-llm\n      annotations:\n        prometheus.io/scrape: \"true\"\n        prometheus.io/path: /metrics\n        prometheus.io/port: \"9090\"\n    spec:\n      serviceAccountName: \"default\"\n      terminationGracePeriodSeconds: 10\n      securityContext:                                                                                                                                                              \n        fsGroup: 1000\n        runAsNonRoot: true                                                                                                                                                          \n        runAsGroup: 1000\n        runAsUser: 1000\n      affinity:                                                                                                                                                                                                                                                                          \n        nodeAffinity:                                                                                                                                                                                                                                                                    \n          requiredDuringSchedulingIgnoredDuringExecution:                                                                                                                                                                                                                                \n            nodeSelectorTerms:                                                                                                                                                                                                                                                           \n            - matchExpressions:                                                                                                                                                                                                                                                          \n              - key: topology.kubernetes.io/zone                                                                                                                                                                                                                                         \n                operator: In                                                                                                                                                                                                                                                             \n                values:                                                                                                                                                                                                                                                                  \n                - us-east-1c  \n      containers:\n      - name: anything-llm\n        resources:\n          limits:\n            memory: \"1Gi\"\n            cpu: \"500m\"\n          requests:\n            memory: \"512Mi\"\n            cpu: \"250m\"\n        imagePullPolicy: IfNotPresent\n        image: \"mintplexlabs/anythingllm:render\"\n        securityContext:                     \n          allowPrivilegeEscalation: true                                                                                                                                                                                                                                                 \n          capabilities:                                                                                                                                                                                                                                                                  \n            add:                                                                                                                                                                                                                                                                         \n              - SYS_ADMIN                                                                                                                                                                                                                                                                \n          runAsNonRoot: true                                                                                                                                                                                                                                                             \n          runAsGroup: 1000                                                                                                                                                                                                                                                               \n          runAsUser: 1000                                                                                                                                       \n        command: \n          # Specify a command to override the Dockerfile's ENTRYPOINT.\n          - /bin/bash\n          - -c\n          - |\n            set -x -e\n            sleep 3\n            echo \"AWS_REGION: $AWS_REGION\"\n            echo \"SERVER_PORT: $SERVER_PORT\"\n            echo \"NODE_ENV: $NODE_ENV\"\n            echo \"STORAGE_DIR: $STORAGE_DIR\"\n            {\n              cd /app/server/ &&\n                npx prisma generate --schema=./prisma/schema.prisma &&\n                npx prisma migrate deploy --schema=./prisma/schema.prisma &&\n                node /app/server/index.js\n              echo \"Server process exited with status $?\"\n            } &\n            { \n              node /app/collector/index.js\n              echo \"Collector process exited with status $?\"\n            } &\n            wait -n\n            exit $?\n        readinessProbe:\n          httpGet:\n            path: /v1/api/health\n            port: 8888\n          initialDelaySeconds: 15\n          periodSeconds: 5\n          successThreshold: 2\n        livenessProbe:\n          httpGet:\n            path: /v1/api/health\n            port: 8888\n          initialDelaySeconds: 15\n          periodSeconds: 5\n          failureThreshold: 3\n        env:\n          - name: AWS_REGION\n            value: \"{{ aws_region }}\"\n          - name: AWS_ACCESS_KEY_ID\n            value: \"{{ aws_access_id }}\"\n          - name: AWS_SECRET_ACCESS_KEY\n            value: \"{{ aws_access_secret }}\"\n          - name: SERVER_PORT\n            value: \"3001\"\n          - name: JWT_SECRET\n            value: \"my-random-string-for-seeding\" # Please generate random string at least 12 chars long.\n          - name: STORAGE_DIR\n            value: \"/storage\"\n          - name: NODE_ENV\n            value: \"production\"\n          - name: UID\n            value: \"1000\"\n          - name: GID\n            value: \"1000\"\n        volumeMounts: \n          - name: anything-llm-server-storage-volume-mount\n            mountPath: /storage                                                                                                                                                  \n      volumes:\n        - name: anything-llm-server-storage-volume-mount\n          persistentVolumeClaim:\n            claimName: anything-llm-volume-claim\n---\n# This serves the UI and the backend.\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: anything-llm-ingress\n  namespace: \"{{ namespace }}\"\n  annotations:\n    external-dns.alpha.kubernetes.io/hostname: \"{{ namespace }}-chat.{{ base_domain }}\"\n    kubernetes.io/ingress.class: \"internal-ingress\"\n    nginx.ingress.kubernetes.io/rewrite-target: /\n    ingress.kubernetes.io/ssl-redirect: \"false\"\nspec:\n  rules:\n  - host: \"{{ namespace }}-chat.{{ base_domain }}\"\n    http:\n      paths:\n      - path: /\n        pathType: Prefix\n        backend:\n          service:\n            name: anything-llm-svc\n            port: \n              number: 3001\n  tls: # < placing a host in the TLS config will indicate a cert should be created\n    - hosts:\n        - \"{{ namespace }}-chat.{{ base_domain }}\"\n      secretName: letsencrypt-prod\n---\napiVersion: v1\nkind: Service\nmetadata:\n  labels:\n    kubernetes.io/name: anything-llm\n  name: anything-llm-svc\n  namespace: \"{{ namespace }}\"\nspec:\n  ports:\n  # \"port\" is external port, and \"targetPort\" is internal.\n  - port: 3301\n    targetPort: 3001\n    name: traffic\n  - port: 9090\n    targetPort: 9090\n    name: metrics\n  selector:\n    k8s-app: anything-llm"
  },
  {
    "path": "collector/.env.example",
    "content": "# Placeholder .env file for collector runtime\n\n# This enables HTTP request/response logging in development. Set value to truthy string to enable, leave empty value or comment out to disable\n# ENABLE_HTTP_LOGGER=\"\"\n# This enables timestamps for the HTTP Logger. Set value to true to enable, leave empty or comment out to disable\n# ENABLE_HTTP_LOGGER_TIMESTAMPS=\"\""
  },
  {
    "path": "collector/.gitignore",
    "content": "hotdir/*\n!hotdir/__HOTDIR__.md\nyarn-error.log\n!yarn.lock\noutputs\nscripts\n.env.development\n.env.production\n.env.test\n"
  },
  {
    "path": "collector/.nvmrc",
    "content": "v18.18.0"
  },
  {
    "path": "collector/__tests__/utils/WhisperProviders/ffmpeg/index.test.js",
    "content": "process.env.STORAGE_DIR = \"test-storage\";\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\n// Mock fix-path as a noop to prevent SIGSEGV (segfault)\n// Returns ESM-style default export for dynamic import()\njest.mock(\"fix-path\", () => ({ default: jest.fn() }));\n\nconst { FFMPEGWrapper } = require(\"../../../../utils/WhisperProviders/ffmpeg\");\n\nconst describeRunner = process.env.GITHUB_ACTIONS ? describe.skip : describe;\n\ndescribeRunner(\"FFMPEGWrapper\", () => {\n  /** @type { import(\"../../../../utils/WhisperProviders/ffmpeg/index\").FFMPEGWrapper } */\n  let ffmpeg;\n  const testDir = path.resolve(__dirname, \"../../../../storage/tmp\");\n  const inputPath = path.resolve(testDir, \"test-input.wav\");\n  const outputPath = path.resolve(testDir, \"test-output.wav\");\n\n  beforeEach(() => {\n    ffmpeg = new FFMPEGWrapper();\n  });\n\n  afterEach(() => {\n    if (fs.existsSync(inputPath)) fs.rmSync(inputPath);\n    if (fs.existsSync(outputPath)) fs.rmSync(outputPath);\n  });\n\n  it(\"should find ffmpeg executable\", async () => {\n    const knownPath = await ffmpeg.ffmpegPath();\n    expect(knownPath).toBeDefined();\n    expect(typeof knownPath).toBe(\"string\");\n    expect(knownPath.length).toBeGreaterThan(0);\n  });\n\n  it(\"should validate ffmpeg executable\", async () => {\n    const knownPath = await ffmpeg.ffmpegPath();\n    expect(ffmpeg.isValidFFMPEG(knownPath)).toBe(true);\n  });\n\n  it(\"should return false for invalid ffmpeg path\", () => {\n    expect(ffmpeg.isValidFFMPEG(\"/invalid/path/to/ffmpeg\")).toBe(false);\n  });\n\n  it(\"should convert audio file to wav format\", async () => {\n    if (!fs.existsSync(testDir)) fs.mkdirSync(testDir, { recursive: true });\n\n    const sampleUrl =\n      \"https://github.com/ringcentral/ringcentral-api-docs/blob/main/resources/sample1.wav?raw=true\";\n\n    const response = await fetch(sampleUrl);\n    if (!response.ok)\n      throw new Error(\n        `Failed to download sample file: ${response.statusText}`\n      );\n\n    const buffer = await response.arrayBuffer();\n    fs.writeFileSync(inputPath, Buffer.from(buffer));\n\n    const result = await ffmpeg.convertAudioToWav(inputPath, outputPath);\n\n    expect(result).toBe(true);\n    expect(fs.existsSync(outputPath)).toBe(true);\n\n    const stats = fs.statSync(outputPath);\n    expect(stats.size).toBeGreaterThan(0);\n  }, 30000);\n\n  it(\"should throw error when conversion fails\", () => {\n    const nonExistentFile = path.resolve(testDir, \"non-existent-file.wav\");\n    const outputPath = path.resolve(testDir, \"test-output-fail.wav\");\n\n    expect(async () => {\n      return await ffmpeg.convertAudioToWav(nonExistentFile, outputPath);\n    }).rejects.toThrow(`Input file ${nonExistentFile} does not exist.`);\n  });\n});\n"
  },
  {
    "path": "collector/__tests__/utils/url/index.test.js",
    "content": "process.env.STORAGE_DIR = \"test-storage\"; // needed for tests to run\nconst { validURL, validateURL, validYoutubeVideoUrl } = require(\"../../../utils/url\");\n\n// Mock the RuntimeSettings module\njest.mock(\"../../../utils/runtimeSettings\", () => {\n  const mockInstance = {\n    get: jest.fn(),\n    set: jest.fn(),\n  };\n  return jest.fn().mockImplementation(() => mockInstance);\n});\n\ndescribe(\"validURL\", () => {\n  let mockRuntimeSettings;\n\n  beforeEach(() => {\n    const RuntimeSettings = require(\"../../../utils/runtimeSettings\");\n    mockRuntimeSettings = new RuntimeSettings();\n    jest.clearAllMocks();\n  });\n\n  it(\"should validate a valid URL\", () => {\n    mockRuntimeSettings.get.mockImplementation((key) => {\n      if (key === \"allowAnyIp\") return false;\n      if (key === \"seenAnyIpWarning\") return true; // silence the warning for tests\n      return false;\n    });\n\n    expect(validURL(\"https://www.google.com\")).toBe(true);\n    expect(validURL(\"http://www.google.com\")).toBe(true);\n\n    // JS URL does not require extensions, so in theory\n    // these should be valid\n    expect(validURL(\"https://random\")).toBe(true);\n    expect(validURL(\"http://123\")).toBe(true);\n\n    // missing protocols\n    expect(validURL(\"www.google.com\")).toBe(false);\n    expect(validURL(\"google.com\")).toBe(false);\n\n    // invalid protocols\n    expect(validURL(\"ftp://www.google.com\")).toBe(false);\n    expect(validURL(\"mailto://www.google.com\")).toBe(false);\n    expect(validURL(\"tel://www.google.com\")).toBe(false);\n    expect(validURL(\"data://www.google.com\")).toBe(false);\n  });\n\n  it(\"should block private/local IPs when allowAnyIp is false (default behavior)\", () => {\n    mockRuntimeSettings.get.mockImplementation((key) => {\n      if (key === \"allowAnyIp\") return false;\n      if (key === \"seenAnyIpWarning\") return true; // silence the warning for tests\n      return false;\n    });\n\n    expect(validURL(\"http://192.168.1.1\")).toBe(false);\n    expect(validURL(\"http://10.0.0.1\")).toBe(false);\n    expect(validURL(\"http://172.16.0.1\")).toBe(false);\n\n    // But localhost should still be allowed\n    expect(validURL(\"http://127.0.0.1\")).toBe(true);\n    expect(validURL(\"http://0.0.0.0\")).toBe(true);\n  });\n\n  it(\"should allow any IP when allowAnyIp is true\", () => {\n    mockRuntimeSettings.get.mockImplementation((key) => {\n      if (key === \"allowAnyIp\") return true;\n      if (key === \"seenAnyIpWarning\") return true; // silence the warning for tests\n      return false;\n    });\n\n    expect(validURL(\"http://192.168.1.1\")).toBe(true);\n    expect(validURL(\"http://10.0.0.1\")).toBe(true);\n    expect(validURL(\"http://172.16.0.1\")).toBe(true);\n  });\n});\n\ndescribe(\"validateURL\", () => {\n  it(\"should return the same URL if it's already valid\", () => {\n    expect(validateURL(\"https://www.google.com\")).toBe(\n      \"https://www.google.com\"\n    );\n    expect(validateURL(\"http://www.google.com\")).toBe(\"http://www.google.com\");\n    expect(validateURL(\"https://random\")).toBe(\"https://random\");\n\n    // With numbers as a url this will turn into an ip\n    expect(validateURL(\"123\")).toBe(\"https://0.0.0.123\");\n    expect(validateURL(\"123.123.123.123\")).toBe(\"https://123.123.123.123\");\n    expect(validateURL(\"http://127.0.123.45\")).toBe(\"http://127.0.123.45\");\n  });\n\n  it(\"should assume https:// if the URL doesn't have a protocol\", () => {\n    expect(validateURL(\"www.google.com\")).toBe(\"https://www.google.com\");\n    expect(validateURL(\"google.com\")).toBe(\"https://google.com\");\n    expect(validateURL(\"EXAMPLE.com/ABCDEF/q1=UPPER\")).toBe(\"https://example.com/ABCDEF/q1=UPPER\");\n    expect(validateURL(\"ftp://www.google.com\")).toBe(\"ftp://www.google.com\");\n    expect(validateURL(\"mailto://www.google.com\")).toBe(\n      \"mailto://www.google.com\"\n    );\n    expect(validateURL(\"tel://www.google.com\")).toBe(\"tel://www.google.com\");\n    expect(validateURL(\"data://www.google.com\")).toBe(\"data://www.google.com\");\n  });\n\n  it(\"should remove trailing slashes post-validation\", () => {\n    expect(validateURL(\"https://www.google.com/\")).toBe(\n      \"https://www.google.com\"\n    );\n    expect(validateURL(\"http://www.google.com/\")).toBe(\"http://www.google.com\");\n    expect(validateURL(\"https://random/\")).toBe(\"https://random\");\n    expect(validateURL(\"https://example.com/ABCDEF/\")).toBe(\"https://example.com/ABCDEF\");\n  });\n\n  it(\"should handle edge cases and bad data inputs\", () => {\n    expect(validateURL({})).toBe(\"\");\n    expect(validateURL(null)).toBe(\"\");\n    expect(validateURL(undefined)).toBe(\"\");\n    expect(validateURL(124512)).toBe(\"\");\n    expect(validateURL(\"\")).toBe(\"\");\n    expect(validateURL(\" \")).toBe(\"\");\n    expect(validateURL(\" look here! \")).toBe(\"look here!\");\n  });\n\n  it(\"should preserve case of characters in URL pathname\", () => {\n    expect(validateURL(\"https://example.com/To/ResOURce?q1=Value&qZ22=UPPE!R\"))\n      .toBe(\"https://example.com/To/ResOURce?q1=Value&qZ22=UPPE!R\");\n    expect(validateURL(\"https://sample.com/uPeRCaSe\"))\n      .toBe(\"https://sample.com/uPeRCaSe\");\n    expect(validateURL(\"Example.com/PATH/To/Resource?q2=Value&q1=UPPER\"))\n      .toBe(\"https://example.com/PATH/To/Resource?q2=Value&q1=UPPER\");\n  });\n});\n\n\ndescribe(\"validYoutubeVideoUrl\", () => {\n  const ID = \"dQw4w9WgXcQ\"; // 11-char valid video id\n\n  it(\"returns true for youtube watch URLs with v param\", () => {\n    expect(validYoutubeVideoUrl(`https://www.youtube.com/watch?v=${ID}`)).toBe(\n      true\n    );\n    expect(validYoutubeVideoUrl(`https://youtube.com/watch?v=${ID}&t=10s`)).toBe(\n      true\n    );\n    expect(validYoutubeVideoUrl(`https://m.youtube.com/watch?v=${ID}`)).toBe(true);\n    expect(validYoutubeVideoUrl(`youtube.com/watch?v=${ID}`)).toBe(true);\n  });\n\n  it(\"returns true for youtu.be short URLs\", () => {\n    expect(validYoutubeVideoUrl(`https://youtu.be/${ID}`)).toBe(true);\n    expect(validYoutubeVideoUrl(`https://youtu.be/${ID}?si=abc`)).toBe(true);\n    // extra path segments after id should still validate the id component\n    expect(validYoutubeVideoUrl(`https://youtu.be/${ID}/extra`)).toBe(true);\n  });\n\n  it(\"returns true for embed and v path formats\", () => {\n    expect(validYoutubeVideoUrl(`https://www.youtube.com/embed/${ID}`)).toBe(true);\n    expect(validYoutubeVideoUrl(`https://youtube.com/v/${ID}`)).toBe(true);\n  });\n\n  it(\"returns false for non-YouTube hosts\", () => {\n    expect(validYoutubeVideoUrl(\"https://example.com/watch?v=dQw4w9WgXcQ\")).toBe(\n      false\n    );\n    expect(validYoutubeVideoUrl(\"https://vimeo.com/123456\")).toBe(false);\n  });\n\n  it(\"returns false for unrelated YouTube paths without a video id\", () => {\n    expect(validYoutubeVideoUrl(\"https://www.youtube.com/user/somechannel\")).toBe(\n      false\n    );\n    expect(validYoutubeVideoUrl(\"https://www.youtube.com/\")).toBe(false);\n  });\n\n  it(\"returns false for empty or bad inputs\", () => {\n    expect(validYoutubeVideoUrl(\"\")).toBe(false);\n    expect(validYoutubeVideoUrl(null)).toBe(false);\n    expect(validYoutubeVideoUrl(undefined)).toBe(false);\n  });\n\n  it(\"returns the video ID for valid YouTube video URLs\", () => {\n    expect(validYoutubeVideoUrl(`https://www.youtube.com/watch?v=${ID}`, true)).toBe(ID);\n    expect(validYoutubeVideoUrl(`https://youtube.com/watch?v=${ID}&t=10s`, true)).toBe(ID);\n    expect(validYoutubeVideoUrl(`https://m.youtube.com/watch?v=${ID}`, true)).toBe(ID);\n    expect(validYoutubeVideoUrl(`youtube.com/watch?v=${ID}`, true)).toBe(ID);\n    expect(validYoutubeVideoUrl(`https://youtu.be/${ID}`, true)).toBe(ID);\n    expect(validYoutubeVideoUrl(`https://youtu.be/${ID}?si=abc`, true)).toBe(ID);\n    expect(validYoutubeVideoUrl(`https://youtu.be/${ID}/extra`, true)).toBe(ID);\n    expect(validYoutubeVideoUrl(`https://www.youtube.com/embed/${ID}`, true)).toBe(ID);\n    expect(validYoutubeVideoUrl(`https://youtube.com/v/${ID}`, true)).toBe(ID);\n    // invalid video IDs\n    expect(validYoutubeVideoUrl(`https://www.youtube.com/watch?v=invalid`, true)).toBe(null);\n    expect(validYoutubeVideoUrl(`https://youtube.com/watch?v=invalid`, true)).toBe(null);\n    expect(validYoutubeVideoUrl(`https://m.youtube.com/watch?v=invalid`, true)).toBe(null);\n    expect(validYoutubeVideoUrl(`youtube.com/watch`, true)).toBe(null);\n    expect(validYoutubeVideoUrl(`https://youtu.be/invalid`, true)).toBe(null);\n    expect(validYoutubeVideoUrl(`https://youtu.be/invalid?si=abc`, true)).toBe(null);\n  });\n});\n"
  },
  {
    "path": "collector/eslint.config.mjs",
    "content": "import js from \"@eslint/js\";\nimport globals from \"globals\";\nimport { defineConfig } from \"eslint/config\";\nimport pluginPrettier from \"eslint-plugin-prettier\";\nimport configPrettier from \"eslint-config-prettier\";\nimport unusedImports from \"eslint-plugin-unused-imports\";\n\nexport default defineConfig([\n  { ignores: [\"__tests__/**\"] },\n  {\n    files: [\"**/*.{js,mjs,cjs}\"],\n    plugins: { js, prettier: pluginPrettier, \"unused-imports\": unusedImports },\n    extends: [\"js/recommended\"],\n    languageOptions: { globals: { ...globals.node, ...globals.browser } },\n    rules: {\n      ...configPrettier.rules,\n      \"prettier/prettier\": \"error\",\n      \"no-case-declarations\": \"off\",\n      \"no-prototype-builtins\": \"off\",\n      \"no-async-promise-executor\": \"off\",\n      \"no-extra-boolean-cast\": \"off\",\n      \"no-empty\": \"off\",\n      \"no-unused-private-class-members\": \"warn\",\n      \"no-unused-vars\": \"off\",\n      \"unused-imports/no-unused-imports\": \"error\",\n      \"unused-imports/no-unused-vars\": [\n        \"error\",\n        {\n          vars: \"all\",\n          varsIgnorePattern: \"^_\",\n          args: \"after-used\",\n          argsIgnorePattern: \"^_\",\n        },\n      ],\n    },\n  },\n  { files: [\"**/*.js\"], languageOptions: { sourceType: \"commonjs\" } },\n]);\n"
  },
  {
    "path": "collector/extensions/index.js",
    "content": "const { setDataSigner } = require(\"../middleware/setDataSigner\");\nconst { verifyPayloadIntegrity } = require(\"../middleware/verifyIntegrity\");\nconst {\n  resolveRepoLoader,\n  resolveRepoLoaderFunction,\n} = require(\"../utils/extensions/RepoLoader\");\nconst { reqBody } = require(\"../utils/http\");\nconst { validURL, validateURL } = require(\"../utils/url\");\nconst RESYNC_METHODS = require(\"./resync\");\nconst { loadObsidianVault } = require(\"../utils/extensions/ObsidianVault\");\n\nfunction extensions(app) {\n  if (!app) return;\n\n  app.post(\n    \"/ext/resync-source-document\",\n    [verifyPayloadIntegrity, setDataSigner],\n    async function (request, response) {\n      try {\n        const { type, options } = reqBody(request);\n        if (!RESYNC_METHODS.hasOwnProperty(type))\n          throw new Error(`Type \"${type}\" is not a valid type to sync.`);\n        return await RESYNC_METHODS[type](options, response);\n      } catch (e) {\n        console.error(e);\n        response.status(200).json({\n          success: false,\n          content: null,\n          reason: e.message || \"A processing error occurred.\",\n        });\n      }\n      return;\n    }\n  );\n\n  app.post(\n    \"/ext/:repo_platform-repo\",\n    [verifyPayloadIntegrity, setDataSigner],\n    async function (request, response) {\n      try {\n        const loadRepo = resolveRepoLoaderFunction(\n          request.params.repo_platform\n        );\n        const { success, reason, data } = await loadRepo(\n          reqBody(request),\n          response\n        );\n        response.status(200).json({\n          success,\n          reason,\n          data,\n        });\n      } catch (e) {\n        console.error(e);\n        response.status(200).json({\n          success: false,\n          reason: e.message || \"A processing error occurred.\",\n          data: {},\n        });\n      }\n      return;\n    }\n  );\n\n  // gets all branches for a specific repo\n  app.post(\n    \"/ext/:repo_platform-repo/branches\",\n    [verifyPayloadIntegrity],\n    async function (request, response) {\n      try {\n        const RepoLoader = resolveRepoLoader(request.params.repo_platform);\n        const allBranches = await new RepoLoader(\n          reqBody(request)\n        ).getRepoBranches();\n        response.status(200).json({\n          success: true,\n          reason: null,\n          data: {\n            branches: allBranches,\n          },\n        });\n      } catch (e) {\n        console.error(e);\n        response.status(400).json({\n          success: false,\n          reason: e.message,\n          data: {\n            branches: [],\n          },\n        });\n      }\n      return;\n    }\n  );\n\n  app.post(\n    \"/ext/youtube-transcript\",\n    [verifyPayloadIntegrity],\n    async function (request, response) {\n      try {\n        const {\n          loadYouTubeTranscript,\n        } = require(\"../utils/extensions/YoutubeTranscript\");\n        const { success, reason, data } = await loadYouTubeTranscript(\n          reqBody(request)\n        );\n        response.status(200).json({ success, reason, data });\n      } catch (e) {\n        console.error(e);\n        response.status(400).json({\n          success: false,\n          reason: e.message,\n          data: {\n            title: null,\n            author: null,\n          },\n        });\n      }\n      return;\n    }\n  );\n\n  app.post(\n    \"/ext/website-depth\",\n    [verifyPayloadIntegrity],\n    async function (request, response) {\n      try {\n        const websiteDepth = require(\"../utils/extensions/WebsiteDepth\");\n        const { url, depth = 1, maxLinks = 20 } = reqBody(request);\n        const validatedUrl = validateURL(url);\n        if (!validURL(validatedUrl)) throw new Error(\"Not a valid URL.\");\n        const scrapedData = await websiteDepth(validatedUrl, depth, maxLinks);\n        response.status(200).json({ success: true, data: scrapedData });\n      } catch (e) {\n        console.error(e);\n        response.status(400).json({ success: false, reason: e.message });\n      }\n      return;\n    }\n  );\n\n  app.post(\n    \"/ext/confluence\",\n    [verifyPayloadIntegrity, setDataSigner],\n    async function (request, response) {\n      try {\n        const { loadConfluence } = require(\"../utils/extensions/Confluence\");\n        const { success, reason, data } = await loadConfluence(\n          reqBody(request),\n          response\n        );\n        response.status(200).json({ success, reason, data });\n      } catch (e) {\n        console.error(e);\n        response.status(400).json({\n          success: false,\n          reason: e.message,\n          data: {\n            title: null,\n            author: null,\n          },\n        });\n      }\n      return;\n    }\n  );\n\n  app.post(\n    \"/ext/drupalwiki\",\n    [verifyPayloadIntegrity, setDataSigner],\n    async function (request, response) {\n      try {\n        const {\n          loadAndStoreSpaces,\n        } = require(\"../utils/extensions/DrupalWiki\");\n        const { success, reason, data } = await loadAndStoreSpaces(\n          reqBody(request),\n          response\n        );\n        response.status(200).json({ success, reason, data });\n      } catch (e) {\n        console.error(e);\n        response.status(400).json({\n          success: false,\n          reason: e.message,\n          data: {\n            title: null,\n            author: null,\n          },\n        });\n      }\n      return;\n    }\n  );\n\n  app.post(\n    \"/ext/obsidian/vault\",\n    [verifyPayloadIntegrity, setDataSigner],\n    async function (request, response) {\n      try {\n        const { files } = reqBody(request);\n        const result = await loadObsidianVault({ files });\n        response.status(200).json(result);\n      } catch (e) {\n        console.error(e);\n        response.status(400).json({\n          success: false,\n          reason: e.message,\n          data: null,\n        });\n      }\n      return;\n    }\n  );\n\n  app.post(\n    \"/ext/paperless-ngx\",\n    [verifyPayloadIntegrity, setDataSigner],\n    async function (request, response) {\n      try {\n        const {\n          loadPaperlessNgx,\n        } = require(\"../utils/extensions/PaperlessNgx\");\n        const result = await loadPaperlessNgx(reqBody(request), response);\n        response.status(200).json(result);\n      } catch (e) {\n        console.error(e);\n        response.status(400).json({\n          success: false,\n          reason: e.message,\n          data: null,\n        });\n      }\n      return;\n    }\n  );\n}\n\nmodule.exports = extensions;\n"
  },
  {
    "path": "collector/extensions/resync/index.js",
    "content": "const { getLinkText } = require(\"../../processLink\");\n\n/**\n * Fetches the content of a raw link. Returns the content as a text string of the link in question.\n * @param {object} data - metadata from document (eg: link)\n * @param {import(\"../../middleware/setDataSigner\").ResponseWithSigner} response\n */\nasync function resyncLink({ link }, response) {\n  if (!link) throw new Error(\"Invalid link provided\");\n  try {\n    const { success, content = null, reason } = await getLinkText(link);\n    if (!success) throw new Error(`Failed to sync link content. ${reason}`);\n    response.status(200).json({ success, content });\n  } catch (e) {\n    console.error(e);\n    response.status(200).json({\n      success: false,\n      content: null,\n    });\n  }\n}\n\n/**\n * Fetches the content of a YouTube link. Returns the content as a text string of the video in question.\n * We offer this as there may be some videos where a transcription could be manually edited after initial scraping\n * but in general - transcriptions often never change.\n * @param {object} data - metadata from document (eg: link)\n * @param {import(\"../../middleware/setDataSigner\").ResponseWithSigner} response\n */\nasync function resyncYouTube({ link }, response) {\n  if (!link) throw new Error(\"Invalid link provided\");\n  try {\n    const {\n      fetchVideoTranscriptContent,\n    } = require(\"../../utils/extensions/YoutubeTranscript\");\n    const { success, reason, content } = await fetchVideoTranscriptContent({\n      url: link,\n    });\n    if (!success)\n      throw new Error(`Failed to sync YouTube video transcript. ${reason}`);\n    response.status(200).json({ success, content });\n  } catch (e) {\n    console.error(e);\n    response.status(200).json({\n      success: false,\n      content: null,\n    });\n  }\n}\n\n/**\n * Fetches the content of a specific confluence page via its chunkSource.\n * Returns the content as a text string of the page in question and only that page.\n * @param {object} data - metadata from document (eg: chunkSource)\n * @param {import(\"../../middleware/setDataSigner\").ResponseWithSigner} response\n */\nasync function resyncConfluence({ chunkSource }, response) {\n  if (!chunkSource) throw new Error(\"Invalid source property provided\");\n  try {\n    // Confluence data is `payload` encrypted. So we need to expand its\n    // encrypted payload back into query params so we can reFetch the page with same access token/params.\n    const source = response.locals.encryptionWorker.expandPayload(chunkSource);\n    const {\n      fetchConfluencePage,\n    } = require(\"../../utils/extensions/Confluence\");\n    const { success, reason, content } = await fetchConfluencePage({\n      pageUrl: `https:${source.pathname}`, // need to add back the real protocol\n      baseUrl: source.searchParams.get(\"baseUrl\"),\n      spaceKey: source.searchParams.get(\"spaceKey\"),\n      accessToken: source.searchParams.get(\"token\"),\n      username: source.searchParams.get(\"username\"),\n      cloud: source.searchParams.get(\"cloud\") === \"true\",\n      bypassSSL: source.searchParams.get(\"bypassSSL\") === \"true\",\n    });\n\n    if (!success)\n      throw new Error(`Failed to sync Confluence page content. ${reason}`);\n    response.status(200).json({ success, content });\n  } catch (e) {\n    console.error(e);\n    response.status(200).json({\n      success: false,\n      content: null,\n    });\n  }\n}\n\n/**\n * Fetches the content of a specific confluence page via its chunkSource.\n * Returns the content as a text string of the page in question and only that page.\n * @param {object} data - metadata from document (eg: chunkSource)\n * @param {import(\"../../middleware/setDataSigner\").ResponseWithSigner} response\n */\nasync function resyncGithub({ chunkSource }, response) {\n  if (!chunkSource) throw new Error(\"Invalid source property provided\");\n  try {\n    // Github file data is `payload` encrypted (might contain PAT). So we need to expand its\n    // encrypted payload back into query params so we can reFetch the page with same access token/params.\n    const source = response.locals.encryptionWorker.expandPayload(chunkSource);\n    const {\n      fetchGithubFile,\n    } = require(\"../../utils/extensions/RepoLoader/GithubRepo\");\n    const { success, reason, content } = await fetchGithubFile({\n      repoUrl: `https:${source.pathname}`, // need to add back the real protocol\n      branch: source.searchParams.get(\"branch\"),\n      accessToken: source.searchParams.get(\"pat\"),\n      sourceFilePath: source.searchParams.get(\"path\"),\n    });\n\n    if (!success)\n      throw new Error(`Failed to sync GitHub file content. ${reason}`);\n    response.status(200).json({ success, content });\n  } catch (e) {\n    console.error(e);\n    response.status(200).json({\n      success: false,\n      content: null,\n    });\n  }\n}\n\n/**\n * Fetches the content of a specific DrupalWiki page via its chunkSource.\n * Returns the content as a text string of the page in question and only that page.\n * @param {object} data - metadata from document (eg: chunkSource)\n * @param {import(\"../../middleware/setDataSigner\").ResponseWithSigner} response\n */\nasync function resyncDrupalWiki({ chunkSource }, response) {\n  if (!chunkSource) throw new Error(\"Invalid source property provided\");\n  try {\n    // DrupalWiki data is `payload` encrypted. So we need to expand its\n    // encrypted payload back into query params so we can reFetch the page with same access token/params.\n    const source = response.locals.encryptionWorker.expandPayload(chunkSource);\n    const { loadPage } = require(\"../../utils/extensions/DrupalWiki\");\n    const { success, reason, content } = await loadPage({\n      baseUrl: source.searchParams.get(\"baseUrl\"),\n      pageId: source.searchParams.get(\"pageId\"),\n      accessToken: source.searchParams.get(\"accessToken\"),\n    });\n\n    if (!success) {\n      console.error(`Failed to sync DrupalWiki page content. ${reason}`);\n      response.status(200).json({\n        success: false,\n        content: null,\n      });\n    } else {\n      response.status(200).json({ success, content });\n    }\n  } catch (e) {\n    console.error(e);\n    response.status(200).json({\n      success: false,\n      content: null,\n    });\n  }\n}\n\n/**\n * Fetches the content of a specific Paperless-ngx document via its chunkSource.\n * Returns the content as a text string of the document.\n * @param {object} data - metadata from document (eg: chunkSource)\n * @param {import(\"../../middleware/setDataSigner\").ResponseWithSigner} response\n */\nasync function resyncPaperlessNgx({ chunkSource }, response) {\n  if (!chunkSource) throw new Error(\"Invalid source property provided\");\n  try {\n    const source = response.locals.encryptionWorker.expandPayload(chunkSource);\n    const {\n      PaperlessNgxLoader,\n    } = require(\"../../utils/extensions/PaperlessNgx/PaperlessNgxLoader\");\n    const loader = new PaperlessNgxLoader({\n      baseUrl: source.searchParams.get(\"baseUrl\"),\n      apiToken: source.searchParams.get(\"token\"),\n    });\n    const documentId = source.pathname.split(\"//\")[1];\n    const content = await loader.fetchDocumentContent(documentId);\n\n    if (!content) throw new Error(\"Failed to fetch document content\");\n    response.status(200).json({ success: true, content });\n  } catch (e) {\n    console.error(e);\n    response.status(200).json({\n      success: false,\n      content: null,\n    });\n  }\n}\n\nmodule.exports = {\n  link: resyncLink,\n  youtube: resyncYouTube,\n  confluence: resyncConfluence,\n  github: resyncGithub,\n  drupalwiki: resyncDrupalWiki,\n  \"paperless-ngx\": resyncPaperlessNgx,\n};\n"
  },
  {
    "path": "collector/hotdir/__HOTDIR__.md",
    "content": "### What is the \"Hot directory\"\n\nThis is a pre-set file location that documents will be written to when uploaded by AnythingLLM. There is really no need to touch it."
  },
  {
    "path": "collector/index.js",
    "content": "process.env.NODE_ENV === \"development\"\n  ? require(\"dotenv\").config({ path: `.env.${process.env.NODE_ENV}` })\n  : require(\"dotenv\").config();\n\nrequire(\"./utils/logger\")();\nconst express = require(\"express\");\nconst bodyParser = require(\"body-parser\");\nconst cors = require(\"cors\");\nconst path = require(\"path\");\nconst { ACCEPTED_MIMES } = require(\"./utils/constants\");\nconst { reqBody } = require(\"./utils/http\");\nconst { processSingleFile } = require(\"./processSingleFile\");\nconst { processLink, getLinkText } = require(\"./processLink\");\nconst { wipeCollectorStorage } = require(\"./utils/files\");\nconst extensions = require(\"./extensions\");\nconst { processRawText } = require(\"./processRawText\");\nconst { verifyPayloadIntegrity } = require(\"./middleware/verifyIntegrity\");\nconst { httpLogger } = require(\"./middleware/httpLogger\");\nconst app = express();\nconst FILE_LIMIT = \"3GB\";\n\n// Only log HTTP requests in development mode and if the ENABLE_HTTP_LOGGER environment variable is set to true\nif (\n  process.env.NODE_ENV === \"development\" &&\n  !!process.env.ENABLE_HTTP_LOGGER\n) {\n  app.use(\n    httpLogger({\n      enableTimestamps: !!process.env.ENABLE_HTTP_LOGGER_TIMESTAMPS,\n    })\n  );\n}\napp.use(cors({ origin: true }));\napp.use(\n  bodyParser.text({ limit: FILE_LIMIT }),\n  bodyParser.json({ limit: FILE_LIMIT }),\n  bodyParser.urlencoded({\n    limit: FILE_LIMIT,\n    extended: true,\n  })\n);\n\napp.post(\n  \"/process\",\n  [verifyPayloadIntegrity],\n  async function (request, response) {\n    const { filename, options = {}, metadata = {} } = reqBody(request);\n    try {\n      const targetFilename = path\n        .normalize(filename)\n        .replace(/^(\\.\\.(\\/|\\\\|$))+/, \"\");\n      const {\n        success,\n        reason,\n        documents = [],\n      } = await processSingleFile(targetFilename, options, metadata);\n      response\n        .status(200)\n        .json({ filename: targetFilename, success, reason, documents });\n    } catch (e) {\n      console.error(e);\n      response.status(200).json({\n        filename: filename,\n        success: false,\n        reason: \"A processing error occurred.\",\n        documents: [],\n      });\n    }\n    return;\n  }\n);\n\napp.post(\n  \"/parse\",\n  [verifyPayloadIntegrity],\n  async function (request, response) {\n    const { filename, options = {} } = reqBody(request);\n    try {\n      const targetFilename = path\n        .normalize(filename)\n        .replace(/^(\\.\\.(\\/|\\\\|$))+/, \"\");\n      const {\n        success,\n        reason,\n        documents = [],\n      } = await processSingleFile(targetFilename, {\n        ...options,\n        parseOnly: true,\n      });\n      response\n        .status(200)\n        .json({ filename: targetFilename, success, reason, documents });\n    } catch (e) {\n      console.error(e);\n      response.status(200).json({\n        filename: filename,\n        success: false,\n        reason: \"A processing error occurred.\",\n        documents: [],\n      });\n    }\n    return;\n  }\n);\n\napp.post(\n  \"/process-link\",\n  [verifyPayloadIntegrity],\n  async function (request, response) {\n    const { link, scraperHeaders = {}, metadata = {} } = reqBody(request);\n    try {\n      const {\n        success,\n        reason,\n        documents = [],\n      } = await processLink(link, scraperHeaders, metadata);\n      response.status(200).json({ url: link, success, reason, documents });\n    } catch (e) {\n      console.error(e);\n      response.status(200).json({\n        url: link,\n        success: false,\n        reason: \"A processing error occurred.\",\n        documents: [],\n      });\n    }\n    return;\n  }\n);\n\napp.post(\n  \"/util/get-link\",\n  [verifyPayloadIntegrity],\n  async function (request, response) {\n    const { link, captureAs = \"text\" } = reqBody(request);\n    try {\n      const { success, content = null } = await getLinkText(link, captureAs);\n      response.status(200).json({ url: link, success, content });\n    } catch (e) {\n      console.error(e);\n      response.status(200).json({\n        url: link,\n        success: false,\n        content: null,\n      });\n    }\n    return;\n  }\n);\n\napp.post(\n  \"/process-raw-text\",\n  [verifyPayloadIntegrity],\n  async function (request, response) {\n    const { textContent, metadata } = reqBody(request);\n    try {\n      const {\n        success,\n        reason,\n        documents = [],\n      } = await processRawText(textContent, metadata);\n      response\n        .status(200)\n        .json({ filename: metadata.title, success, reason, documents });\n    } catch (e) {\n      console.error(e);\n      response.status(200).json({\n        filename: metadata?.title || \"Unknown-doc.txt\",\n        success: false,\n        reason: \"A processing error occurred.\",\n        documents: [],\n      });\n    }\n    return;\n  }\n);\n\nextensions(app);\n\napp.get(\"/accepts\", function (_, response) {\n  response.status(200).json(ACCEPTED_MIMES);\n});\n\napp.all(\"*\", function (_, response) {\n  response.sendStatus(200);\n});\n\napp\n  .listen(8888, async () => {\n    await wipeCollectorStorage();\n    console.log(`Document processor app listening on port 8888`);\n  })\n  .on(\"error\", function (_) {\n    process.once(\"SIGUSR2\", function () {\n      process.kill(process.pid, \"SIGUSR2\");\n    });\n    process.on(\"SIGINT\", function () {\n      process.kill(process.pid, \"SIGINT\");\n    });\n  });\n"
  },
  {
    "path": "collector/middleware/httpLogger.js",
    "content": "const httpLogger =\n  ({ enableTimestamps = false }) =>\n  (req, res, next) => {\n    // Capture the original res.end to log response status\n    const originalEnd = res.end;\n\n    res.end = function (chunk, encoding) {\n      // Log the request method, status code, and path\n      const statusColor = res.statusCode >= 400 ? \"\\x1b[31m\" : \"\\x1b[32m\"; // Red for errors, green for success\n      console.log(\n        `\\x1b[32m[HTTP]\\x1b[0m ${statusColor}${res.statusCode}\\x1b[0m ${\n          req.method\n        } -> ${req.path} ${\n          enableTimestamps\n            ? `@ ${new Date().toLocaleTimeString(\"en-US\", { hour12: true })}`\n            : \"\"\n        }`.trim()\n      );\n\n      // Call the original end method\n      return originalEnd.call(this, chunk, encoding);\n    };\n\n    next();\n  };\n\nmodule.exports = {\n  httpLogger,\n};\n"
  },
  {
    "path": "collector/middleware/setDataSigner.js",
    "content": "const { EncryptionWorker } = require(\"../utils/EncryptionWorker\");\nconst { CommunicationKey } = require(\"../utils/comKey\");\n\n/**\n * Express Response Object interface with defined encryptionWorker attached to locals property.\n * @typedef {import(\"express\").Response & import(\"express\").Response['locals'] & {encryptionWorker: EncryptionWorker} } ResponseWithSigner\n */\n\n// You can use this middleware to assign the EncryptionWorker to the response locals\n// property so that if can be used to encrypt/decrypt arbitrary data via response object.\n// eg: Encrypting API keys in chunk sources.\n\n// The way this functions is that the rolling RSA Communication Key is used server-side to private-key encrypt the raw\n// key of the persistent EncryptionManager credentials. Since EncryptionManager credentials do _not_ roll, we should not send them\n// even between server<>collector in plaintext because if the user configured the server/collector to be public they could technically\n// be exposing the key in transit via the X-Payload-Signer header. Even if this risk is minimal we should not do this.\n\n// This middleware uses the CommunicationKey public key to first decrypt the base64 representation of the EncryptionManager credentials\n// and then loads that in to the EncryptionWorker as a buffer so we can use the same credentials across the system. Should we ever break the\n// collector out into its own service this would still work without SSL/TLS.\n\n/**\n *\n * @param {import(\"express\").Request} request\n * @param {import(\"express\").Response} response\n * @param {import(\"express\").NextFunction} next\n */\nfunction setDataSigner(request, response, next) {\n  const comKey = new CommunicationKey();\n  const encryptedPayloadSigner = request.header(\"X-Payload-Signer\");\n  if (!encryptedPayloadSigner)\n    console.log(\n      \"Failed to find signed-payload to set encryption worker! Encryption calls will fail.\"\n    );\n\n  const decryptedPayloadSignerKey = comKey.decrypt(encryptedPayloadSigner);\n  const encryptionWorker = new EncryptionWorker(decryptedPayloadSignerKey);\n  response.locals.encryptionWorker = encryptionWorker;\n  next();\n}\n\nmodule.exports = {\n  setDataSigner,\n};\n"
  },
  {
    "path": "collector/middleware/verifyIntegrity.js",
    "content": "const { CommunicationKey } = require(\"../utils/comKey\");\nconst RuntimeSettings = require(\"../utils/runtimeSettings\");\nconst runtimeSettings = new RuntimeSettings();\n\nfunction verifyPayloadIntegrity(request, response, next) {\n  const comKey = new CommunicationKey();\n  if (process.env.NODE_ENV === \"development\") {\n    comKey.log(\"verifyPayloadIntegrity is skipped in development.\");\n    runtimeSettings.parseOptionsFromRequest(request);\n    next();\n    return;\n  }\n\n  const signature = request.header(\"X-Integrity\");\n  if (!signature)\n    return response\n      .status(400)\n      .json({ msg: \"Failed integrity signature check.\" });\n\n  const validSignedPayload = comKey.verify(signature, request.body);\n  if (!validSignedPayload)\n    return response\n      .status(400)\n      .json({ msg: \"Failed integrity signature check.\" });\n\n  runtimeSettings.parseOptionsFromRequest(request);\n  next();\n}\n\nmodule.exports = {\n  verifyPayloadIntegrity,\n};\n"
  },
  {
    "path": "collector/nodemon.json",
    "content": "{\n  \"events\": {}\n}"
  },
  {
    "path": "collector/package.json",
    "content": "{\n  \"name\": \"anything-llm-document-collector\",\n  \"version\": \"1.11.1\",\n  \"description\": \"Document collector server endpoints\",\n  \"main\": \"index.js\",\n  \"author\": \"Timothy Carambat (Mintplex Labs)\",\n  \"license\": \"MIT\",\n  \"private\": false,\n  \"engines\": {\n    \"node\": \">=18.12.1\"\n  },\n  \"scripts\": {\n    \"dev\": \"cross-env NODE_ENV=development nodemon --ignore hotdir --ignore storage --trace-warnings index.js\",\n    \"start\": \"cross-env NODE_ENV=production node index.js\",\n    \"lint\": \"eslint --fix .\",\n    \"lint:check\": \"eslint .\"\n  },\n  \"dependencies\": {\n    \"@langchain/community\": \"^0.2.23\",\n    \"@xenova/transformers\": \"^2.14.0\",\n    \"body-parser\": \"^1.20.3\",\n    \"cors\": \"^2.8.5\",\n    \"dotenv\": \"^16.0.3\",\n    \"epub2\": \"git+https://github.com/Mintplex-Labs/epub2-static.git#main\",\n    \"express\": \"^4.21.2\",\n    \"fix-path\": \"^4.0.0\",\n    \"html-to-text\": \"^9.0.5\",\n    \"ignore\": \"^5.3.0\",\n    \"js-tiktoken\": \"^1.0.8\",\n    \"langchain\": \"0.1.36\",\n    \"mammoth\": \"^1.6.0\",\n    \"mbox-parser\": \"^1.0.1\",\n    \"mime\": \"^3.0.0\",\n    \"moment\": \"^2.29.4\",\n    \"node-html-parser\": \"^6.1.13\",\n    \"node-xlsx\": \"^0.24.0\",\n    \"officeparser\": \"^4.0.5\",\n    \"openai\": \"4.95.1\",\n    \"pdf-parse\": \"^1.1.1\",\n    \"puppeteer\": \"~21.5.2\",\n    \"sharp\": \"^0.33.5\",\n    \"slugify\": \"^1.6.6\",\n    \"strip-ansi\": \"^7.1.2\",\n    \"tesseract.js\": \"^6.0.0\",\n    \"url-pattern\": \"^1.0.3\",\n    \"uuid\": \"^9.0.0\",\n    \"wavefile\": \"^11.0.0\",\n    \"winston\": \"^3.13.0\",\n    \"youtube-transcript-plus\": \"^1.1.2\",\n    \"youtubei.js\": \"^9.1.0\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.0.0\",\n    \"cross-env\": \"^7.0.3\",\n    \"eslint\": \"^9.0.0\",\n    \"eslint-config-prettier\": \"^9.0.0\",\n    \"eslint-plugin-prettier\": \"^5.0.0\",\n    \"eslint-plugin-unused-imports\": \"^4.0.0\",\n    \"globals\": \"^17.4.0\",\n    \"nodemon\": \"^2.0.22\",\n    \"prettier\": \"^2.4.1\"\n  },\n  \"resolutions\": {\n    \"string-width\": \"^4.2.3\",\n    \"wrap-ansi\": \"^7.0.0\"\n  }\n}\n"
  },
  {
    "path": "collector/processLink/convert/generic.js",
    "content": "const { v4 } = require(\"uuid\");\nconst {\n  PuppeteerWebBaseLoader,\n} = require(\"langchain/document_loaders/web/puppeteer\");\nconst { writeToServerDocuments } = require(\"../../utils/files\");\nconst { tokenizeString } = require(\"../../utils/tokenizer\");\nconst { default: slugify } = require(\"slugify\");\nconst {\n  returnResult,\n  determineContentType,\n  processAsFile,\n} = require(\"../helpers\");\nconst {\n  loadYouTubeTranscript,\n} = require(\"../../utils/extensions/YoutubeTranscript\");\nconst RuntimeSettings = require(\"../../utils/runtimeSettings\");\n\n/**\n * Scrape a generic URL and return the content in the specified format\n * @param {Object} config - The configuration object\n * @param {string} config.link - The URL to scrape\n * @param {('html' | 'text')} config.captureAs - The format to capture the page content as. Default is 'text'\n * @param {{[key: string]: string}} config.scraperHeaders - Custom headers to use when making the request\n * @param {{[key: string]: string}} config.metadata - Metadata to use when creating the document\n * @param {boolean} config.saveAsDocument - Whether to save the content as a document. Default is true\n * @returns {Promise<Object>} - The content of the page\n */\nasync function scrapeGenericUrl({\n  link,\n  captureAs = \"text\",\n  scraperHeaders = {},\n  metadata = {},\n  saveAsDocument = true,\n}) {\n  /** @type {'web' | 'file' | 'youtube'} */\n  console.log(`-- Working URL ${link} => (captureAs: ${captureAs}) --`);\n  let { contentType, processVia } = await determineContentType(link);\n  console.log(`-- URL determined to be ${contentType} (${processVia}) --`);\n\n  /**\n   * When the content is a file or a YouTube video, we can use the existing processing functions\n   * These are self-contained and will return the correct response based on the saveAsDocument flag already\n   * so we can return the content immediately.\n   */\n  if (processVia === \"file\")\n    return await processAsFile({ uri: link, saveAsDocument });\n  else if (processVia === \"youtube\")\n    return await loadYouTubeTranscript(\n      { url: link },\n      { parseOnly: saveAsDocument === false }\n    );\n\n  // Otherwise, assume the content is a webpage and scrape the content from the webpage\n  const content = await getPageContent({\n    link,\n    captureAs,\n    headers: scraperHeaders,\n  });\n  if (!content || !content.length) {\n    console.error(`Resulting URL content was empty at ${link}.`);\n    return returnResult({\n      success: false,\n      reason: `No URL content found at ${link}.`,\n      documents: [],\n      content: null,\n      saveAsDocument,\n    });\n  }\n\n  // If the captureAs is text, return the content as a string immediately\n  // so that we dont save the content as a document\n  if (!saveAsDocument)\n    return returnResult({\n      success: true,\n      content,\n      saveAsDocument,\n    });\n\n  // Save the content as a document from the URL\n  const url = new URL(link);\n  const decodedPathname = decodeURIComponent(url.pathname);\n  const filename = `${url.hostname}${decodedPathname.replace(/\\//g, \"_\")}`;\n  const data = {\n    id: v4(),\n    url: \"file://\" + slugify(filename) + \".html\",\n    title: metadata.title || slugify(filename) + \".html\",\n    docAuthor: metadata.docAuthor || \"no author found\",\n    description: metadata.description || \"No description found.\",\n    docSource: metadata.docSource || \"URL link uploaded by the user.\",\n    chunkSource: `link://${link}`,\n    published: new Date().toLocaleString(),\n    wordCount: content.split(\" \").length,\n    pageContent: content,\n    token_count_estimate: tokenizeString(content),\n  };\n\n  const document = writeToServerDocuments({\n    data,\n    filename: `url-${slugify(filename)}-${data.id}`,\n  });\n  console.log(`[SUCCESS]: URL ${link} converted & ready for embedding.\\n`);\n  return { success: true, reason: null, documents: [document] };\n}\n\n/**\n * Validate the headers object\n * - Keys & Values must be strings and not empty\n * - Assemble a new object with only the valid keys and values\n * @param {{[key: string]: string}} headers - The headers object to validate\n * @returns {{[key: string]: string}} - The validated headers object\n */\nfunction validatedHeaders(headers = {}) {\n  try {\n    if (Object.keys(headers).length === 0) return {};\n    let validHeaders = {};\n    for (const key of Object.keys(headers)) {\n      if (!key?.trim()) continue;\n      if (typeof headers[key] !== \"string\" || !headers[key]?.trim()) continue;\n      validHeaders[key] = headers[key].trim();\n    }\n    return validHeaders;\n  } catch (error) {\n    console.error(\"Error validating headers\", error);\n    return {};\n  }\n}\n\n/**\n * Get the content of a page\n * @param {Object} config - The configuration object\n * @param {string} config.link - The URL to get the content of\n * @param {('html' | 'text')} config.captureAs - The format to capture the page content as. Default is 'text'\n * @param {{[key: string]: string}} config.headers - Custom headers to use when making the request\n * @returns {Promise<string>} - The content of the page\n */\nasync function getPageContent({ link, captureAs = \"text\", headers = {} }) {\n  try {\n    let pageContents = [];\n    const runtimeSettings = new RuntimeSettings();\n\n    /** @type {import('puppeteer').PuppeteerLaunchOptions} */\n    let launchConfig = { headless: \"new\" };\n\n    /* On MacOS 15.1, the headless=new option causes the browser to crash immediately.\n     * It is not clear why this is the case, but it is reproducible. Since AnythinglLM\n     * in production runs in a container, we can disable headless mode to workaround the issue for development purposes.\n     *\n     * This may show a popup window when scraping a page in development mode.\n     * This is expected behavior if seen in development mode on MacOS 15+\n     */\n    if (\n      process.platform === \"darwin\" &&\n      process.env.NODE_ENV === \"development\"\n    ) {\n      console.log(\n        \"Darwin Development Mode: Disabling headless mode to prevent Chromium from crashing.\"\n      );\n      launchConfig.headless = \"false\";\n    }\n\n    const loader = new PuppeteerWebBaseLoader(link, {\n      launchOptions: {\n        headless: launchConfig.headless,\n        ignoreHTTPSErrors: true,\n        args: runtimeSettings.get(\"browserLaunchArgs\"),\n      },\n      gotoOptions: {\n        waitUntil: \"networkidle2\",\n      },\n      async evaluate(page, browser) {\n        const result = await page.evaluate((captureAs) => {\n          if (captureAs === \"text\") return document.body.innerText;\n          if (captureAs === \"html\") return document.documentElement.innerHTML;\n          return document.body.innerText;\n        }, captureAs);\n        await browser.close();\n        return result;\n      },\n    });\n\n    // Override scrape method if headers are available\n    let overrideHeaders = validatedHeaders(headers);\n    if (Object.keys(overrideHeaders).length > 0) {\n      loader.scrape = async function () {\n        const { launch } = await PuppeteerWebBaseLoader.imports();\n        const browser = await launch({\n          headless: \"new\",\n          defaultViewport: null,\n          ignoreDefaultArgs: [\"--disable-extensions\"],\n          ...this.options?.launchOptions,\n        });\n        const page = await browser.newPage();\n        await page.setExtraHTTPHeaders(overrideHeaders);\n\n        await page.goto(this.webPath, {\n          timeout: 180000,\n          waitUntil: \"networkidle2\",\n          ...this.options?.gotoOptions,\n        });\n\n        const bodyHTML = this.options?.evaluate\n          ? await this.options.evaluate(page, browser)\n          : await page.evaluate(() => document.body.innerHTML);\n\n        await browser.close();\n        return bodyHTML;\n      };\n    }\n\n    const docs = await loader.load();\n    for (const doc of docs) pageContents.push(doc.pageContent);\n    return pageContents.join(\" \");\n  } catch (error) {\n    console.error(\n      \"getPageContent failed to be fetched by puppeteer - falling back to fetch!\",\n      error\n    );\n  }\n\n  try {\n    const pageText = await fetch(link, {\n      method: \"GET\",\n      headers: {\n        \"Content-Type\": \"text/plain\",\n        \"User-Agent\":\n          \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36,gzip(gfe)\",\n        ...validatedHeaders(headers),\n      },\n    }).then((res) => res.text());\n    return pageText;\n  } catch (error) {\n    console.error(\"getPageContent failed to be fetched by any method.\", error);\n  }\n\n  return null;\n}\n\nmodule.exports = {\n  scrapeGenericUrl,\n};\n"
  },
  {
    "path": "collector/processLink/helpers/index.js",
    "content": "const path = require(\"path\");\nconst { validURL } = require(\"../../utils/url\");\nconst { processSingleFile } = require(\"../../processSingleFile\");\nconst { downloadURIToFile } = require(\"../../utils/downloadURIToFile\");\nconst { ACCEPTED_MIMES } = require(\"../../utils/constants\");\nconst { validYoutubeVideoUrl } = require(\"../../utils/url\");\n\n/**\n * Get the content type of a resource\n * - Sends a HEAD request to the URL and returns the Content-Type header with a 5 second timeout\n * @param {string} url - The URL to get the content type of\n * @returns {Promise<{success: boolean, reason: string|null, contentType: string|null}>} - The content type of the resource\n */\nasync function getContentTypeFromURL(url) {\n  try {\n    if (!url || typeof url !== \"string\" || !validURL(url))\n      return { success: false, reason: \"Not a valid URL.\", contentType: null };\n\n    const abortController = new AbortController();\n    const timeout = setTimeout(() => {\n      abortController.abort();\n      console.error(\"Timeout fetching content type for URL:\", url.toString());\n    }, 5_000);\n\n    const res = await fetch(url, {\n      method: \"HEAD\",\n      signal: abortController.signal,\n    }).finally(() => clearTimeout(timeout));\n\n    if (!res.ok)\n      return {\n        success: false,\n        reason: `HTTP ${res.status}: ${res.statusText}`,\n        contentType: null,\n      };\n\n    const contentType = res.headers.get(\"Content-Type\")?.toLowerCase();\n    const contentTypeWithoutCharset = contentType?.split(\";\")[0].trim();\n    if (!contentTypeWithoutCharset)\n      return {\n        success: false,\n        reason: \"No Content-Type found.\",\n        contentType: null,\n      };\n    return {\n      success: true,\n      reason: null,\n      contentType: contentTypeWithoutCharset,\n    };\n  } catch (error) {\n    return {\n      success: false,\n      reason: `Error: ${error.message}`,\n      contentType: null,\n    };\n  }\n}\n\n/**\n * Normalize the result object based on the saveAsDocument flag\n * @param {Object} result - The result object to normalize\n * @param {boolean} result.success - Whether the result is successful\n * @param {string|null} result.reason - The reason for the result\n * @param {Object[]} result.documents - The documents from the result\n * @param {string|null} result.content - The content of the result\n * @param {boolean} result.saveAsDocument - Whether to save the content as a document. Default is true\n * @returns {{success: boolean, reason: string|null, documents: Object[], content: string|null}} - The normalized result object\n */\nfunction returnResult({\n  success,\n  reason,\n  documents,\n  content,\n  saveAsDocument = true,\n} = {}) {\n  if (!saveAsDocument) {\n    return {\n      success,\n      content,\n    };\n  } else return { success, reason, documents };\n}\n\n/**\n * Determine the content type of a link - should be a URL\n * @param {string} uri - The link to determine the content type of\n * @returns {Promise<{contentType: string|null, processVia: 'web' | 'file' | 'youtube'}>} - The content type of the link\n */\nasync function determineContentType(uri) {\n  let processVia = \"web\";\n\n  // Dont check for content type if it is a YouTube video URL\n  if (validYoutubeVideoUrl(uri))\n    return { contentType: \"text/html\", processVia: \"youtube\" };\n\n  return await getContentTypeFromURL(uri)\n    .then((result) => {\n      if (!!result.reason) console.error(result.reason);\n\n      // If the content type is not text/html or text/plain, and it is in the ACCEPTED_MIMES,\n      // then we can process it as a file\n      if (\n        !!result.contentType &&\n        ![\"text/html\", \"text/plain\"].includes(result.contentType) &&\n        result.contentType in ACCEPTED_MIMES\n      )\n        processVia = \"file\";\n\n      return { contentType: result.contentType, processVia };\n    })\n    .catch((error) => {\n      console.error(\"Error getting content type from URL\", error);\n      return { contentType: null, processVia };\n    });\n}\n\n/**\n * Process a link as a file\n * @param {string} uri - The link to process as a file\n * @param {boolean} saveAsDocument - Whether to save the content as a document. Default is true\n * @returns {Promise<{success: boolean, reason: string|null, documents: Object[], content: string|null, saveAsDocument: boolean}>} - The content of the file\n */\nasync function processAsFile({ uri, saveAsDocument = true }) {\n  const fileContentResult = await downloadURIToFile(uri);\n  if (!fileContentResult.success)\n    return returnResult({\n      success: false,\n      reason: fileContentResult.reason,\n      documents: [],\n      content: null,\n      saveAsDocument,\n    });\n\n  const fileFilePath = fileContentResult.fileLocation;\n  const targetFilename = path.basename(fileFilePath);\n\n  /**\n   * If the saveAsDocument is false, we are only interested in the text content\n   * and can ignore the file as a document by using `parseOnly` in the options.\n   * This will send the file to the Direct Uploads folder instead of the Documents folder.\n   * that will be deleted by the cleanup-orphan-documents job that runs frequently. The trade off\n   * is that since it still is in FS we can debug its output or even potentially reuse it for other purposes.\n   *\n   * TODO: Improve this process via a new option that will instantly delete the file after processing\n   * if we find we dont need this file ever after processing.\n   */\n  const processSingleFileResult = await processSingleFile(targetFilename, {\n    parseOnly: saveAsDocument === false,\n  });\n  if (!processSingleFileResult.success) {\n    return returnResult({\n      success: false,\n      reason: processSingleFileResult.reason,\n      documents: [],\n      content: null,\n      saveAsDocument,\n    });\n  }\n\n  // If we intend to return only the text content, return the content from the file\n  // and then delete the file - otherwise it will be saved as a document\n  if (!saveAsDocument) {\n    return returnResult({\n      success: true,\n      content: processSingleFileResult.documents[0].pageContent,\n      saveAsDocument,\n    });\n  }\n\n  return processSingleFileResult;\n}\n\nmodule.exports = {\n  returnResult,\n  getContentTypeFromURL,\n  determineContentType,\n  processAsFile,\n};\n"
  },
  {
    "path": "collector/processLink/index.js",
    "content": "const { validURL } = require(\"../utils/url\");\nconst { scrapeGenericUrl } = require(\"./convert/generic\");\nconst { validateURL } = require(\"../utils/url\");\n\n/**\n * Process a link and return the text content. This util will save the link as a document\n * so it can be used for embedding later.\n * @param {string} link - The link to process\n * @param {{[key: string]: string}} scraperHeaders - Custom headers to apply when scraping the link\n * @param {Object} metadata - Optional metadata to attach to the document\n * @returns {Promise<{success: boolean, content: string}>} - Response from collector\n */\nasync function processLink(link, scraperHeaders = {}, metadata = {}) {\n  const validatedLink = validateURL(link);\n  if (!validURL(validatedLink))\n    return { success: false, reason: \"Not a valid URL.\" };\n  return await scrapeGenericUrl({\n    link: validatedLink,\n    captureAs: \"text\",\n    scraperHeaders,\n    metadata,\n    saveAsDocument: true,\n  });\n}\n\n/**\n * Get the text content of a link - does not save the link as a document\n * Mostly used in agentic flows/tools calls to get the text content of a link\n * @param {string} link - The link to get the text content of\n * @param {('html' | 'text' | 'json')} captureAs - The format to capture the page content as\n * @returns {Promise<{success: boolean, content: string}>} - Response from collector\n */\nasync function getLinkText(link, captureAs = \"text\") {\n  const validatedLink = validateURL(link);\n  if (!validURL(validatedLink))\n    return { success: false, reason: \"Not a valid URL.\" };\n  return await scrapeGenericUrl({\n    link: validatedLink,\n    captureAs,\n    saveAsDocument: false,\n  });\n}\n\nmodule.exports = {\n  processLink,\n  getLinkText,\n};\n"
  },
  {
    "path": "collector/processRawText/index.js",
    "content": "const { v4 } = require(\"uuid\");\nconst { writeToServerDocuments } = require(\"../utils/files\");\nconst { tokenizeString } = require(\"../utils/tokenizer\");\nconst { default: slugify } = require(\"slugify\");\n\n// Will remove the last .extension from the input\n// and stringify the input + move to lowercase.\nfunction stripAndSlug(input) {\n  if (!input.includes(\".\")) return slugify(input, { lower: true });\n  return slugify(input.split(\".\").slice(0, -1).join(\"-\"), { lower: true });\n}\n\nconst METADATA_KEYS = {\n  possible: {\n    url: ({ url, title }) => {\n      let validUrl;\n      try {\n        const u = new URL(url);\n        validUrl = [\"https:\", \"http:\"].includes(u.protocol);\n      } catch {}\n\n      if (validUrl) return `web://${url.toLowerCase()}.website`;\n      return `file://${stripAndSlug(title)}.txt`;\n    },\n    title: ({ title }) => `${stripAndSlug(title)}.txt`,\n    docAuthor: ({ docAuthor }) => {\n      return typeof docAuthor === \"string\" ? docAuthor : \"no author specified\";\n    },\n    description: ({ description }) => {\n      return typeof description === \"string\"\n        ? description\n        : \"no description found\";\n    },\n    docSource: ({ docSource }) => {\n      return typeof docSource === \"string\" ? docSource : \"no source set\";\n    },\n    chunkSource: ({ chunkSource, title }) => {\n      return typeof chunkSource === \"string\"\n        ? chunkSource\n        : `${stripAndSlug(title)}.txt`;\n    },\n    published: ({ published }) => {\n      if (isNaN(Number(published))) return new Date().toLocaleString();\n      return new Date(Number(published)).toLocaleString();\n    },\n  },\n};\n\nasync function processRawText(textContent, metadata) {\n  console.log(`-- Working Raw Text doc ${metadata.title} --`);\n  if (!textContent || textContent.length === 0) {\n    return {\n      success: false,\n      reason: \"textContent was empty - nothing to process.\",\n      documents: [],\n    };\n  }\n\n  const data = {\n    id: v4(),\n    url: METADATA_KEYS.possible.url(metadata),\n    title: METADATA_KEYS.possible.title(metadata),\n    docAuthor: METADATA_KEYS.possible.docAuthor(metadata),\n    description: METADATA_KEYS.possible.description(metadata),\n    docSource: METADATA_KEYS.possible.docSource(metadata),\n    chunkSource: METADATA_KEYS.possible.chunkSource(metadata),\n    published: METADATA_KEYS.possible.published(metadata),\n    wordCount: textContent.split(\" \").length,\n    pageContent: textContent,\n    token_count_estimate: tokenizeString(textContent),\n  };\n\n  const document = writeToServerDocuments({\n    data,\n    filename: `raw-${stripAndSlug(metadata.title)}-${data.id}`,\n  });\n  console.log(\n    `[SUCCESS]: Raw text and metadata saved & ready for embedding.\\n`\n  );\n  return { success: true, reason: null, documents: [document] };\n}\n\nmodule.exports = { processRawText };\n"
  },
  {
    "path": "collector/processSingleFile/convert/asAudio.js",
    "content": "const { v4 } = require(\"uuid\");\nconst {\n  createdDate,\n  trashFile,\n  writeToServerDocuments,\n} = require(\"../../utils/files\");\nconst { tokenizeString } = require(\"../../utils/tokenizer\");\nconst { default: slugify } = require(\"slugify\");\nconst { LocalWhisper } = require(\"../../utils/WhisperProviders/localWhisper\");\nconst { OpenAiWhisper } = require(\"../../utils/WhisperProviders/OpenAiWhisper\");\n\nconst WHISPER_PROVIDERS = {\n  openai: OpenAiWhisper,\n  local: LocalWhisper,\n};\n\nasync function asAudio({\n  fullFilePath = \"\",\n  filename = \"\",\n  options = {},\n  metadata = {},\n}) {\n  const WhisperProvider = WHISPER_PROVIDERS.hasOwnProperty(\n    options?.whisperProvider\n  )\n    ? WHISPER_PROVIDERS[options?.whisperProvider]\n    : WHISPER_PROVIDERS.local;\n\n  console.log(`-- Working ${filename} --`);\n  const whisper = new WhisperProvider({ options });\n  const { content, error } = await whisper.processFile(fullFilePath, filename);\n\n  if (!!error) {\n    console.error(`Error encountered for parsing of ${filename}.`);\n    trashFile(fullFilePath);\n    return {\n      success: false,\n      reason: error,\n      documents: [],\n    };\n  }\n\n  if (!content?.length) {\n    console.error(`Resulting text content was empty for ${filename}.`);\n    trashFile(fullFilePath);\n    return {\n      success: false,\n      reason: `No text content found in ${filename}.`,\n      documents: [],\n    };\n  }\n\n  const data = {\n    id: v4(),\n    url: \"file://\" + fullFilePath,\n    title: metadata.title || filename,\n    docAuthor: metadata.docAuthor || \"no author found\",\n    description: metadata.description || \"No description found.\",\n    docSource: metadata.docSource || \"audio file uploaded by the user.\",\n    chunkSource: metadata.chunkSource || \"\",\n    published: createdDate(fullFilePath),\n    wordCount: content.split(\" \").length,\n    pageContent: content,\n    token_count_estimate: tokenizeString(content),\n  };\n\n  const document = writeToServerDocuments({\n    data,\n    filename: `${slugify(filename)}-${data.id}`,\n    options: { parseOnly: options.parseOnly },\n  });\n  trashFile(fullFilePath);\n  console.log(\n    `[SUCCESS]: ${filename} transcribed, converted & ready for embedding.\\n`\n  );\n  return { success: true, reason: null, documents: [document] };\n}\n\nmodule.exports = asAudio;\n"
  },
  {
    "path": "collector/processSingleFile/convert/asDocx.js",
    "content": "const { v4 } = require(\"uuid\");\nconst { DocxLoader } = require(\"langchain/document_loaders/fs/docx\");\nconst {\n  createdDate,\n  trashFile,\n  writeToServerDocuments,\n} = require(\"../../utils/files\");\nconst { tokenizeString } = require(\"../../utils/tokenizer\");\nconst { default: slugify } = require(\"slugify\");\n\nasync function asDocX({\n  fullFilePath = \"\",\n  filename = \"\",\n  options = {},\n  metadata = {},\n}) {\n  const loader = new DocxLoader(fullFilePath);\n\n  console.log(`-- Working ${filename} --`);\n  let pageContent = [];\n  const docs = await loader.load();\n  for (const doc of docs) {\n    console.log(`-- Parsing content from docx page --`);\n    if (!doc.pageContent.length) continue;\n    pageContent.push(doc.pageContent);\n  }\n\n  if (!pageContent.length) {\n    console.error(`Resulting text content was empty for ${filename}.`);\n    trashFile(fullFilePath);\n    return {\n      success: false,\n      reason: `No text content found in ${filename}.`,\n      documents: [],\n    };\n  }\n\n  const content = pageContent.join(\"\");\n  const data = {\n    id: v4(),\n    url: \"file://\" + fullFilePath,\n    title: metadata.title || filename,\n    docAuthor: metadata.docAuthor || \"no author found\",\n    description: metadata.description || \"No description found.\",\n    docSource: metadata.docSource || \"docx file uploaded by the user.\",\n    chunkSource: metadata.chunkSource || \"\",\n    published: createdDate(fullFilePath),\n    wordCount: content.split(\" \").length,\n    pageContent: content,\n    token_count_estimate: tokenizeString(content),\n  };\n\n  const document = writeToServerDocuments({\n    data,\n    filename: `${slugify(filename)}-${data.id}`,\n    options: { parseOnly: options.parseOnly },\n  });\n  trashFile(fullFilePath);\n  console.log(`[SUCCESS]: ${filename} converted & ready for embedding.\\n`);\n  return { success: true, reason: null, documents: [document] };\n}\n\nmodule.exports = asDocX;\n"
  },
  {
    "path": "collector/processSingleFile/convert/asEPub.js",
    "content": "const { v4 } = require(\"uuid\");\nconst { EPubLoader } = require(\"langchain/document_loaders/fs/epub\");\nconst { tokenizeString } = require(\"../../utils/tokenizer\");\nconst {\n  createdDate,\n  trashFile,\n  writeToServerDocuments,\n} = require(\"../../utils/files\");\nconst { default: slugify } = require(\"slugify\");\n\nasync function asEPub({\n  fullFilePath = \"\",\n  filename = \"\",\n  options = {},\n  metadata = {},\n}) {\n  let content = \"\";\n  try {\n    const loader = new EPubLoader(fullFilePath, { splitChapters: false });\n    const docs = await loader.load();\n    docs.forEach((doc) => (content += doc.pageContent));\n  } catch (err) {\n    console.error(\"Could not read epub file!\", err);\n  }\n\n  if (!content?.length) {\n    console.error(`Resulting text content was empty for ${filename}.`);\n    trashFile(fullFilePath);\n    return {\n      success: false,\n      reason: `No text content found in ${filename}.`,\n      documents: [],\n    };\n  }\n\n  console.log(`-- Working ${filename} --`);\n  const data = {\n    id: v4(),\n    url: \"file://\" + fullFilePath,\n    title: metadata.title || filename,\n    docAuthor: metadata.docAuthor || \"Unknown\",\n    description: metadata.description || \"Unknown\",\n    docSource: metadata.docSource || \"epub file uploaded by the user.\",\n    chunkSource: metadata.chunkSource || \"\",\n    published: createdDate(fullFilePath),\n    wordCount: content.split(\" \").length,\n    pageContent: content,\n    token_count_estimate: tokenizeString(content),\n  };\n\n  const document = writeToServerDocuments({\n    data,\n    filename: `${slugify(filename)}-${data.id}`,\n    options: { parseOnly: options.parseOnly },\n  });\n  trashFile(fullFilePath);\n  console.log(`[SUCCESS]: ${filename} converted & ready for embedding.\\n`);\n  return { success: true, reason: null, documents: [document] };\n}\n\nmodule.exports = asEPub;\n"
  },
  {
    "path": "collector/processSingleFile/convert/asImage.js",
    "content": "const { v4 } = require(\"uuid\");\nconst { tokenizeString } = require(\"../../utils/tokenizer\");\nconst {\n  createdDate,\n  trashFile,\n  writeToServerDocuments,\n} = require(\"../../utils/files\");\nconst OCRLoader = require(\"../../utils/OCRLoader\");\nconst { default: slugify } = require(\"slugify\");\n\nasync function asImage({\n  fullFilePath = \"\",\n  filename = \"\",\n  options = {},\n  metadata = {},\n}) {\n  let content = await new OCRLoader({\n    targetLanguages: options?.ocr?.langList,\n  }).ocrImage(fullFilePath);\n\n  if (!content?.length) {\n    console.error(`Resulting text content was empty for ${filename}.`);\n    trashFile(fullFilePath);\n    return {\n      success: false,\n      reason: `No text content found in ${filename}.`,\n      documents: [],\n    };\n  }\n\n  console.log(`-- Working ${filename} --`);\n  const data = {\n    id: v4(),\n    url: \"file://\" + fullFilePath,\n    title: metadata.title || filename,\n    docAuthor: metadata.docAuthor || \"Unknown\",\n    description: metadata.description || \"Unknown\",\n    docSource: metadata.docSource || \"image file uploaded by the user.\",\n    chunkSource: metadata.chunkSource || \"\",\n    published: createdDate(fullFilePath),\n    wordCount: content.split(\" \").length,\n    pageContent: content,\n    token_count_estimate: tokenizeString(content),\n  };\n\n  const document = writeToServerDocuments({\n    data,\n    filename: `${slugify(filename)}-${data.id}`,\n    options: { parseOnly: options.parseOnly },\n  });\n  trashFile(fullFilePath);\n  console.log(`[SUCCESS]: ${filename} converted & ready for embedding.\\n`);\n  return { success: true, reason: null, documents: [document] };\n}\n\nmodule.exports = asImage;\n"
  },
  {
    "path": "collector/processSingleFile/convert/asMbox.js",
    "content": "const { v4 } = require(\"uuid\");\nconst fs = require(\"fs\");\nconst { mboxParser } = require(\"mbox-parser\");\nconst {\n  createdDate,\n  trashFile,\n  writeToServerDocuments,\n} = require(\"../../utils/files\");\nconst { tokenizeString } = require(\"../../utils/tokenizer\");\nconst { default: slugify } = require(\"slugify\");\n\nasync function asMbox({\n  fullFilePath = \"\",\n  filename = \"\",\n  options = {},\n  metadata = {},\n}) {\n  console.log(`-- Working ${filename} --`);\n\n  const mails = await mboxParser(fs.createReadStream(fullFilePath))\n    .then((mails) => mails)\n    .catch((error) => {\n      console.log(`Could not parse mail items`, error);\n      return [];\n    });\n\n  if (!mails.length) {\n    console.error(`Resulting mail items was empty for ${filename}.`);\n    trashFile(fullFilePath);\n    return {\n      success: false,\n      reason: `No mail items found in ${filename}.`,\n      documents: [],\n    };\n  }\n\n  let item = 1;\n  const documents = [];\n  for (const mail of mails) {\n    if (!mail.hasOwnProperty(\"text\")) continue;\n\n    const content = mail.text;\n    if (!content) continue;\n    console.log(\n      `-- Working on message \"${mail.subject || \"Unknown subject\"}\" --`\n    );\n\n    const data = {\n      id: v4(),\n      url: \"file://\" + fullFilePath,\n      title:\n        metadata.title ||\n        (mail?.subject\n          ? slugify(mail?.subject?.replace(\".\", \"\")) + \".mbox\"\n          : `msg_${item}-${filename}`),\n      docAuthor: metadata.docAuthor || mail?.from?.text,\n      description: metadata.description || \"No description found.\",\n      docSource:\n        metadata.docSource || \"Mbox message file uploaded by the user.\",\n      chunkSource: metadata.chunkSource || \"\",\n      published: createdDate(fullFilePath),\n      wordCount: content.split(\" \").length,\n      pageContent: content,\n      token_count_estimate: tokenizeString(content),\n    };\n\n    item++;\n    const document = writeToServerDocuments({\n      data,\n      filename: `${slugify(filename)}-${data.id}-msg-${item}`,\n      options: { parseOnly: options.parseOnly },\n    });\n    documents.push(document);\n  }\n\n  trashFile(fullFilePath);\n  console.log(\n    `[SUCCESS]: ${filename} messages converted & ready for embedding.\\n`\n  );\n  return { success: true, reason: null, documents };\n}\n\nmodule.exports = asMbox;\n"
  },
  {
    "path": "collector/processSingleFile/convert/asOfficeMime.js",
    "content": "const { v4 } = require(\"uuid\");\nconst officeParser = require(\"officeparser\");\nconst {\n  createdDate,\n  trashFile,\n  writeToServerDocuments,\n} = require(\"../../utils/files\");\nconst { tokenizeString } = require(\"../../utils/tokenizer\");\nconst { default: slugify } = require(\"slugify\");\n\nasync function asOfficeMime({\n  fullFilePath = \"\",\n  filename = \"\",\n  options = {},\n  metadata = {},\n}) {\n  console.log(`-- Working ${filename} --`);\n  let content = \"\";\n  try {\n    content = await officeParser.parseOfficeAsync(fullFilePath);\n  } catch (error) {\n    console.error(`Could not parse office or office-like file`, error);\n  }\n\n  if (!content.length) {\n    console.error(`Resulting text content was empty for ${filename}.`);\n    trashFile(fullFilePath);\n    return {\n      success: false,\n      reason: `No text content found in ${filename}.`,\n      documents: [],\n    };\n  }\n\n  const data = {\n    id: v4(),\n    url: \"file://\" + fullFilePath,\n    title: metadata.title || filename,\n    docAuthor: metadata.docAuthor || \"no author found\",\n    description: metadata.description || \"No description found.\",\n    docSource: metadata.docSource || \"Office file uploaded by the user.\",\n    chunkSource: metadata.chunkSource || \"\",\n    published: createdDate(fullFilePath),\n    wordCount: content.split(\" \").length,\n    pageContent: content,\n    token_count_estimate: tokenizeString(content),\n  };\n\n  const document = writeToServerDocuments({\n    data,\n    filename: `${slugify(filename)}-${data.id}`,\n    options: { parseOnly: options.parseOnly },\n  });\n  trashFile(fullFilePath);\n  console.log(`[SUCCESS]: ${filename} converted & ready for embedding.\\n`);\n  return { success: true, reason: null, documents: [document] };\n}\n\nmodule.exports = asOfficeMime;\n"
  },
  {
    "path": "collector/processSingleFile/convert/asPDF/PDFLoader/index.js",
    "content": "const fs = require(\"fs\").promises;\n\nclass PDFLoader {\n  constructor(filePath, { splitPages = true } = {}) {\n    this.filePath = filePath;\n    this.splitPages = splitPages;\n  }\n\n  async load() {\n    const buffer = await fs.readFile(this.filePath);\n    const { getDocument, version } = await this.getPdfJS();\n\n    const pdf = await getDocument({\n      data: new Uint8Array(buffer),\n      useWorkerFetch: false,\n      isEvalSupported: false,\n      useSystemFonts: true,\n    }).promise;\n\n    const meta = await pdf.getMetadata().catch(() => null);\n    const documents = [];\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      let lastY;\n      const textItems = [];\n      for (const item of content.items) {\n        if (\"str\" in item) {\n          if (lastY === item.transform[5] || !lastY) {\n            textItems.push(item.str);\n          } else {\n            textItems.push(`\\n${item.str}`);\n          }\n          lastY = item.transform[5];\n        }\n      }\n\n      const text = textItems.join(\"\");\n      documents.push({\n        pageContent: text.trim(),\n        metadata: {\n          source: this.filePath,\n          pdf: {\n            version,\n            info: meta?.info,\n            metadata: meta?.metadata,\n            totalPages: pdf.numPages,\n          },\n          loc: { pageNumber: i },\n        },\n      });\n    }\n\n    if (this.splitPages) {\n      return documents;\n    }\n\n    if (documents.length === 0) {\n      return [];\n    }\n\n    return [\n      {\n        pageContent: documents.map((doc) => doc.pageContent).join(\"\\n\\n\"),\n        metadata: {\n          source: this.filePath,\n          pdf: {\n            version,\n            info: meta?.info,\n            metadata: meta?.metadata,\n            totalPages: pdf.numPages,\n          },\n        },\n      },\n    ];\n  }\n\n  async getPdfJS() {\n    try {\n      const pdfjs = await import(\"pdf-parse/lib/pdf.js/v1.10.100/build/pdf.js\");\n      return { getDocument: pdfjs.getDocument, version: pdfjs.version };\n    } catch (e) {\n      console.error(e);\n      throw new Error(\n        \"Failed to load pdf-parse. Please install it with eg. `npm install pdf-parse`.\"\n      );\n    }\n  }\n}\n\nmodule.exports = PDFLoader;\n"
  },
  {
    "path": "collector/processSingleFile/convert/asPDF/index.js",
    "content": "const { v4 } = require(\"uuid\");\nconst {\n  createdDate,\n  trashFile,\n  writeToServerDocuments,\n} = require(\"../../../utils/files\");\nconst { tokenizeString } = require(\"../../../utils/tokenizer\");\nconst { default: slugify } = require(\"slugify\");\nconst PDFLoader = require(\"./PDFLoader\");\nconst OCRLoader = require(\"../../../utils/OCRLoader\");\n\nasync function asPdf({\n  fullFilePath = \"\",\n  filename = \"\",\n  options = {},\n  metadata = {},\n}) {\n  const pdfLoader = new PDFLoader(fullFilePath, {\n    splitPages: true,\n  });\n\n  console.log(`-- Working ${filename} --`);\n  const pageContent = [];\n  let docs = await pdfLoader.load();\n\n  if (docs.length === 0) {\n    console.log(\n      `[asPDF] No text content found for ${filename}. Will attempt OCR parse.`\n    );\n    docs = await new OCRLoader({\n      targetLanguages: options?.ocr?.langList,\n    }).ocrPDF(fullFilePath);\n  }\n\n  for (const doc of docs) {\n    console.log(\n      `-- Parsing content from pg ${\n        doc.metadata?.loc?.pageNumber || \"unknown\"\n      } --`\n    );\n    if (!doc.pageContent || !doc.pageContent.length) continue;\n    pageContent.push(doc.pageContent);\n  }\n\n  if (!pageContent.length) {\n    console.error(`[asPDF] Resulting text content was empty for ${filename}.`);\n    trashFile(fullFilePath);\n    return {\n      success: false,\n      reason: `No text content found in ${filename}.`,\n      documents: [],\n    };\n  }\n\n  const content = pageContent.join(\"\");\n  const data = {\n    id: v4(),\n    url: \"file://\" + fullFilePath,\n    title: metadata.title || filename,\n    docAuthor:\n      metadata.docAuthor ||\n      docs[0]?.metadata?.pdf?.info?.Creator ||\n      \"no author found\",\n    description:\n      metadata.description ||\n      docs[0]?.metadata?.pdf?.info?.Title ||\n      \"No description found.\",\n    docSource: metadata.docSource || \"pdf file uploaded by the user.\",\n    chunkSource: metadata.chunkSource || \"\",\n    published: createdDate(fullFilePath),\n    wordCount: content.split(\" \").length,\n    pageContent: content,\n    token_count_estimate: tokenizeString(content),\n  };\n\n  const document = writeToServerDocuments({\n    data,\n    filename: `${slugify(filename)}-${data.id}`,\n    options: { parseOnly: options.parseOnly },\n  });\n  trashFile(fullFilePath);\n  console.log(`[SUCCESS]: ${filename} converted & ready for embedding.\\n`);\n  return { success: true, reason: null, documents: [document] };\n}\n\nmodule.exports = asPdf;\n"
  },
  {
    "path": "collector/processSingleFile/convert/asTxt.js",
    "content": "const { v4 } = require(\"uuid\");\nconst fs = require(\"fs\");\nconst { tokenizeString } = require(\"../../utils/tokenizer\");\nconst {\n  createdDate,\n  trashFile,\n  writeToServerDocuments,\n} = require(\"../../utils/files\");\nconst { default: slugify } = require(\"slugify\");\n\nasync function asTxt({\n  fullFilePath = \"\",\n  filename = \"\",\n  options = {},\n  metadata = {},\n}) {\n  let content = \"\";\n  try {\n    content = fs.readFileSync(fullFilePath, \"utf8\");\n  } catch (err) {\n    console.error(\"Could not read file!\", err);\n  }\n\n  if (!content?.length) {\n    console.error(`Resulting text content was empty for ${filename}.`);\n    trashFile(fullFilePath);\n    return {\n      success: false,\n      reason: `No text content found in ${filename}.`,\n      documents: [],\n    };\n  }\n\n  console.log(`-- Working ${filename} --`);\n  const data = {\n    id: v4(),\n    url: \"file://\" + fullFilePath,\n    title: metadata.title || filename,\n    docAuthor: metadata.docAuthor || \"Unknown\",\n    description: metadata.description || \"Unknown\",\n    docSource: metadata.docSource || \"a text file uploaded by the user.\",\n    chunkSource: metadata.chunkSource || \"\",\n    published: createdDate(fullFilePath),\n    wordCount: content.split(\" \").length,\n    pageContent: content,\n    token_count_estimate: tokenizeString(content),\n  };\n\n  const document = writeToServerDocuments({\n    data,\n    filename: `${slugify(filename)}-${data.id}`,\n    options: { parseOnly: options.parseOnly },\n  });\n  trashFile(fullFilePath);\n  console.log(`[SUCCESS]: ${filename} converted & ready for embedding.\\n`);\n  return { success: true, reason: null, documents: [document] };\n}\n\nmodule.exports = asTxt;\n"
  },
  {
    "path": "collector/processSingleFile/convert/asXlsx.js",
    "content": "const { v4 } = require(\"uuid\");\nconst xlsx = require(\"node-xlsx\").default;\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst {\n  createdDate,\n  trashFile,\n  writeToServerDocuments,\n  documentsFolder,\n} = require(\"../../utils/files\");\nconst { tokenizeString } = require(\"../../utils/tokenizer\");\nconst { default: slugify } = require(\"slugify\");\n\nfunction convertToCSV(data) {\n  return data\n    .map((row) =>\n      row\n        .map((cell) => {\n          if (cell === null || cell === undefined) return \"\";\n          if (typeof cell === \"string\" && cell.includes(\",\"))\n            return `\"${cell}\"`;\n          return cell;\n        })\n        .join(\",\")\n    )\n    .join(\"\\n\");\n}\n\nasync function asXlsx({\n  fullFilePath = \"\",\n  filename = \"\",\n  options = {},\n  metadata = {},\n}) {\n  const documents = [];\n\n  try {\n    const workSheetsFromFile = xlsx.parse(fullFilePath);\n\n    if (options.parseOnly) {\n      const allSheetContents = [];\n      let totalWordCount = 0;\n      const sheetNames = [];\n\n      for (const sheet of workSheetsFromFile) {\n        const processed = processSheet(sheet);\n        if (!processed) continue;\n\n        const { name, content, wordCount } = processed;\n        sheetNames.push(name);\n        allSheetContents.push(`\\nSheet: ${name}\\n${content}`);\n        totalWordCount += wordCount;\n      }\n\n      if (allSheetContents.length === 0) {\n        console.log(`No valid sheets found in ${filename}.`);\n        return {\n          success: false,\n          reason: `No valid sheets found in ${filename}.`,\n          documents: [],\n        };\n      }\n\n      const combinedContent = allSheetContents.join(\"\\n\");\n      const sheetListText =\n        sheetNames.length > 1\n          ? ` (Sheets: ${sheetNames.join(\", \")})`\n          : ` (Sheet: ${sheetNames[0]})`;\n\n      const combinedData = {\n        id: v4(),\n        url: `file://${fullFilePath}`,\n        title: metadata.title || `${filename}${sheetListText}`,\n        docAuthor: metadata.docAuthor || \"Unknown\",\n        description:\n          metadata.description ||\n          `Spreadsheet data from ${filename} containing ${sheetNames.length} ${\n            sheetNames.length === 1 ? \"sheet\" : \"sheets\"\n          }`,\n        docSource: metadata.docSource || \"an xlsx file uploaded by the user.\",\n        chunkSource: metadata.chunkSource || \"\",\n        published: createdDate(fullFilePath),\n        wordCount: totalWordCount,\n        pageContent: combinedContent,\n        token_count_estimate: tokenizeString(combinedContent),\n      };\n\n      const document = writeToServerDocuments({\n        data: combinedData,\n        filename: `${slugify(path.basename(filename))}-${combinedData.id}`,\n        destinationOverride: null,\n        options: { parseOnly: true },\n      });\n      documents.push(document);\n      console.log(`[SUCCESS]: ${filename} converted & ready for embedding.`);\n    } else {\n      const folderName = slugify(\n        `${path.basename(filename)}-${v4().slice(0, 4)}`,\n        {\n          lower: true,\n          trim: true,\n        }\n      );\n      const outFolderPath = path.resolve(documentsFolder, folderName);\n      if (!fs.existsSync(outFolderPath))\n        fs.mkdirSync(outFolderPath, { recursive: true });\n\n      for (const sheet of workSheetsFromFile) {\n        const processed = processSheet(sheet);\n        if (!processed) continue;\n\n        const { name, content, wordCount } = processed;\n        const sheetData = {\n          id: v4(),\n          url: `file://${path.join(outFolderPath, `${slugify(name)}.csv`)}`,\n          title: metadata.title || `${filename} - Sheet:${name}`,\n          docAuthor: metadata.docAuthor || \"Unknown\",\n          description:\n            metadata.description || `Spreadsheet data from sheet: ${name}`,\n          docSource: metadata.docSource || \"an xlsx file uploaded by the user.\",\n          chunkSource: metadata.chunkSource || \"\",\n          published: createdDate(fullFilePath),\n          wordCount: wordCount,\n          pageContent: content,\n          token_count_estimate: tokenizeString(content),\n        };\n\n        const document = writeToServerDocuments({\n          data: sheetData,\n          filename: `sheet-${slugify(name)}`,\n          destinationOverride: outFolderPath,\n          options: { parseOnly: options.parseOnly },\n        });\n        documents.push(document);\n        console.log(\n          `[SUCCESS]: Sheet \"${name}\" converted & ready for embedding.`\n        );\n      }\n    }\n  } catch (err) {\n    console.error(\"Could not process xlsx file!\", err);\n    return {\n      success: false,\n      reason: `Error processing ${filename}: ${err.message}`,\n      documents: [],\n    };\n  } finally {\n    trashFile(fullFilePath);\n  }\n\n  if (documents.length === 0) {\n    console.error(`No valid sheets found in ${filename}.`);\n    return {\n      success: false,\n      reason: `No valid sheets found in ${filename}.`,\n      documents: [],\n    };\n  }\n\n  console.log(\n    `[SUCCESS]: ${filename} fully processed. Created ${documents.length} document(s).\\n`\n  );\n  return { success: true, reason: null, documents };\n}\n\n/**\n * Processes a single sheet and returns its content and metadata\n * @param {{name: string, data: Array<Array<string|number|null|undefined>>}} sheet - Parsed sheet with name and 2D array of cell values\n * @returns {{name: string, content: string, wordCount: number}|null} - Object with name, CSV content, and word count, or null if sheet is empty\n */\nfunction processSheet(sheet) {\n  try {\n    const { name, data } = sheet;\n    const content = convertToCSV(data);\n\n    if (!content?.length) {\n      console.log(`Sheet \"${name}\" is empty. Skipping.`);\n      return null;\n    }\n\n    console.log(`-- Processing sheet: ${name} --`);\n    return {\n      name,\n      content,\n      wordCount: content.split(/\\s+/).length,\n    };\n  } catch (err) {\n    console.error(`Error processing sheet \"${sheet.name}\":`, err);\n    return null;\n  }\n}\n\nmodule.exports = asXlsx;\n"
  },
  {
    "path": "collector/processSingleFile/index.js",
    "content": "const path = require(\"path\");\nconst fs = require(\"fs\");\nconst {\n  WATCH_DIRECTORY,\n  SUPPORTED_FILETYPE_CONVERTERS,\n} = require(\"../utils/constants\");\nconst {\n  trashFile,\n  isTextType,\n  normalizePath,\n  isWithin,\n} = require(\"../utils/files\");\nconst RESERVED_FILES = [\"__HOTDIR__.md\"];\n\n/**\n * Process a single file and return the documents\n * @param {string} targetFilename - The filename to process\n * @param {Object} options - The options for the file processing\n * @param {boolean} options.parseOnly - If true, the file will not be saved as a document even when `writeToServerDocuments` is called in the handler. Must be explicitly set to true to use.\n * @param {Object} metadata - The metadata for the file processing\n * @returns {Promise<{success: boolean, reason: string, documents: Object[]}>} - The documents from the file processing\n */\nasync function processSingleFile(targetFilename, options = {}, metadata = {}) {\n  const fullFilePath = path.resolve(\n    WATCH_DIRECTORY,\n    normalizePath(targetFilename)\n  );\n  if (!isWithin(path.resolve(WATCH_DIRECTORY), fullFilePath))\n    return {\n      success: false,\n      reason: \"Filename is a not a valid path to process.\",\n      documents: [],\n    };\n\n  if (RESERVED_FILES.includes(targetFilename))\n    return {\n      success: false,\n      reason: \"Filename is a reserved filename and cannot be processed.\",\n      documents: [],\n    };\n  if (!fs.existsSync(fullFilePath))\n    return {\n      success: false,\n      reason: \"File does not exist in upload directory.\",\n      documents: [],\n    };\n\n  const fileExtension = path.extname(fullFilePath).toLowerCase();\n  if (fullFilePath.includes(\".\") && !fileExtension) {\n    return {\n      success: false,\n      reason: `No file extension found. This file cannot be processed.`,\n      documents: [],\n    };\n  }\n\n  let processFileAs = fileExtension;\n  if (!SUPPORTED_FILETYPE_CONVERTERS.hasOwnProperty(fileExtension)) {\n    if (isTextType(fullFilePath)) {\n      console.log(\n        `\\x1b[33m[Collector]\\x1b[0m The provided filetype of ${fileExtension} does not have a preset and will be processed as .txt.`\n      );\n      processFileAs = \".txt\";\n    } else {\n      trashFile(fullFilePath);\n      return {\n        success: false,\n        reason: `File extension ${fileExtension} not supported for parsing and cannot be assumed as text file type.`,\n        documents: [],\n      };\n    }\n  }\n\n  const FileTypeProcessor = require(SUPPORTED_FILETYPE_CONVERTERS[\n    processFileAs\n  ]);\n  return await FileTypeProcessor({\n    fullFilePath,\n    filename: targetFilename,\n    options,\n    metadata,\n  });\n}\n\nmodule.exports = {\n  processSingleFile,\n};\n"
  },
  {
    "path": "collector/storage/.gitignore",
    "content": "tmp/*\n!tmp/.placeholder"
  },
  {
    "path": "collector/storage/tmp/.placeholder",
    "content": ""
  },
  {
    "path": "collector/utils/EncryptionWorker/index.js",
    "content": "const crypto = require(\"crypto\");\n\n// Differs from EncryptionManager in that is does not set or define the keys that will be used\n// to encrypt or read data and it must be told the key (as base64 string) explicitly that will be used and is provided to\n// the class on creation. This key should be the same `key` that is used by the EncryptionManager class.\nclass EncryptionWorker {\n  constructor(presetKeyBase64 = \"\") {\n    this.key = Buffer.from(presetKeyBase64, \"base64\");\n    this.algorithm = \"aes-256-cbc\";\n    this.separator = \":\";\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[EncryptionManager]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Give a chunk source, parse its payload query param and expand that object back into the URL\n   * as additional query params\n   * @param {string} chunkSource\n   * @returns {URL} Javascript URL object with query params decrypted from payload query param.\n   */\n  expandPayload(chunkSource = \"\") {\n    try {\n      const url = new URL(chunkSource);\n      if (!url.searchParams.has(\"payload\")) return url;\n\n      const decryptedPayload = this.decrypt(url.searchParams.get(\"payload\"));\n      const encodedParams = JSON.parse(decryptedPayload);\n      url.searchParams.delete(\"payload\"); // remove payload prop\n\n      // Add all query params needed to replay as query params\n      Object.entries(encodedParams).forEach(([key, value]) =>\n        url.searchParams.append(key, value)\n      );\n      return url;\n    } catch (e) {\n      console.error(e);\n    }\n    return new URL(chunkSource);\n  }\n\n  encrypt(plainTextString = null) {\n    try {\n      if (!plainTextString)\n        throw new Error(\"Empty string is not valid for this method.\");\n      const iv = crypto.randomBytes(16);\n      const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);\n      const encrypted = cipher.update(plainTextString, \"utf8\", \"hex\");\n      return [\n        encrypted + cipher.final(\"hex\"),\n        Buffer.from(iv).toString(\"hex\"),\n      ].join(this.separator);\n    } catch (e) {\n      this.log(e);\n      return null;\n    }\n  }\n\n  decrypt(encryptedString) {\n    try {\n      const [encrypted, iv] = encryptedString.split(this.separator);\n      if (!iv) throw new Error(\"IV not found\");\n      const decipher = crypto.createDecipheriv(\n        this.algorithm,\n        this.key,\n        Buffer.from(iv, \"hex\")\n      );\n      return decipher.update(encrypted, \"hex\", \"utf8\") + decipher.final(\"utf8\");\n    } catch (e) {\n      this.log(e);\n      return null;\n    }\n  }\n}\n\nmodule.exports = { EncryptionWorker };\n"
  },
  {
    "path": "collector/utils/OCRLoader/index.js",
    "content": "const fs = require(\"fs\");\nconst os = require(\"os\");\nconst path = require(\"path\");\nconst { VALID_LANGUAGE_CODES } = require(\"./validLangs\");\n\nclass OCRLoader {\n  /**\n   * The language code(s) to use for the OCR.\n   * @type {string[]}\n   */\n  language;\n  /**\n   * The cache directory for the OCR.\n   * @type {string}\n   */\n  cacheDir;\n\n  /**\n   * The constructor for the OCRLoader.\n   * @param {Object} options - The options for the OCRLoader.\n   * @param {string} options.targetLanguages - The target languages to use for the OCR as a comma separated string. eg: \"eng,deu,...\"\n   */\n  constructor({ targetLanguages = \"eng\" } = {}) {\n    this.language = this.parseLanguages(targetLanguages);\n    this.cacheDir = path.resolve(\n      process.env.STORAGE_DIR\n        ? path.resolve(process.env.STORAGE_DIR, `models`, `tesseract`)\n        : path.resolve(__dirname, `../../../server/storage/models/tesseract`)\n    );\n\n    // Ensure the cache directory exists or else Tesseract will persist the cache in the default location.\n    if (!fs.existsSync(this.cacheDir))\n      fs.mkdirSync(this.cacheDir, { recursive: true });\n    this.log(\n      `OCRLoader initialized with language support for:`,\n      this.language.map((lang) => VALID_LANGUAGE_CODES[lang]).join(\", \")\n    );\n  }\n\n  /**\n   * Parses the language code from a provided comma separated string of language codes.\n   * @param {string} language - The language code to parse.\n   * @returns {string[]} The parsed language code.\n   */\n  parseLanguages(language = null) {\n    try {\n      if (!language || typeof language !== \"string\") return [\"eng\"];\n      const langList = language\n        .split(\",\")\n        .map((lang) => (lang.trim() !== \"\" ? lang.trim() : null))\n        .filter(Boolean)\n        .filter((lang) => VALID_LANGUAGE_CODES.hasOwnProperty(lang));\n      if (langList.length === 0) return [\"eng\"];\n      return langList;\n    } catch (e) {\n      this.log(`Error parsing languages: ${e.message}`, e.stack);\n      return [\"eng\"];\n    }\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[OCRLoader]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Loads a PDF file and returns an array of documents.\n   * This function is reserved to parsing for SCANNED documents - digital documents are not supported in this function\n   * @returns {Promise<{pageContent: string, metadata: object}[]>} An array of documents with page content and metadata.\n   */\n  async ocrPDF(\n    filePath,\n    { maxExecutionTime = 300_000, batchSize = 10, maxWorkers = null } = {}\n  ) {\n    if (\n      !filePath ||\n      !fs.existsSync(filePath) ||\n      !fs.statSync(filePath).isFile()\n    ) {\n      this.log(`File ${filePath} does not exist. Skipping OCR.`);\n      return [];\n    }\n\n    const documentTitle = path.basename(filePath);\n    this.log(`Starting OCR of ${documentTitle}`);\n    const pdfjs = await import(\"pdf-parse/lib/pdf.js/v2.0.550/build/pdf.js\");\n    let buffer = fs.readFileSync(filePath);\n\n    const pdfDocument = await pdfjs.getDocument({ data: buffer });\n\n    const documents = [];\n    const meta = await pdfDocument.getMetadata().catch(() => null);\n    const metadata = {\n      source: filePath,\n      pdf: {\n        version: \"v2.0.550\",\n        info: meta?.info,\n        metadata: meta?.metadata,\n        totalPages: pdfDocument.numPages,\n      },\n    };\n\n    const pdfSharp = new PDFSharp({\n      validOps: [\n        pdfjs.OPS.paintJpegXObject,\n        pdfjs.OPS.paintImageXObject,\n        pdfjs.OPS.paintInlineImageXObject,\n      ],\n    });\n    await pdfSharp.init();\n\n    const { createWorker, OEM } = require(\"tesseract.js\");\n    const BATCH_SIZE = batchSize;\n    const MAX_EXECUTION_TIME = maxExecutionTime;\n    const NUM_WORKERS = maxWorkers ?? Math.min(os.cpus().length, 4);\n    const totalPages = pdfDocument.numPages;\n    const workerPool = await Promise.all(\n      Array(NUM_WORKERS)\n        .fill(0)\n        .map(() =>\n          createWorker(this.language, OEM.LSTM_ONLY, {\n            cachePath: this.cacheDir,\n          })\n        )\n    );\n\n    const startTime = Date.now();\n    try {\n      this.log(\"Bootstrapping OCR completed successfully!\", {\n        MAX_EXECUTION_TIME_MS: MAX_EXECUTION_TIME,\n        BATCH_SIZE,\n        MAX_CONCURRENT_WORKERS: NUM_WORKERS,\n        TOTAL_PAGES: totalPages,\n      });\n      const timeoutPromise = new Promise((_, reject) => {\n        setTimeout(() => {\n          reject(\n            new Error(\n              `OCR job took too long to complete (${\n                MAX_EXECUTION_TIME / 1000\n              } seconds)`\n            )\n          );\n        }, MAX_EXECUTION_TIME);\n      });\n\n      const processPages = async () => {\n        for (\n          let startPage = 1;\n          startPage <= totalPages;\n          startPage += BATCH_SIZE\n        ) {\n          const endPage = Math.min(startPage + BATCH_SIZE - 1, totalPages);\n          const pageNumbers = Array.from(\n            { length: endPage - startPage + 1 },\n            (_, i) => startPage + i\n          );\n          this.log(`Working on pages ${startPage} - ${endPage}`);\n\n          const pageQueue = [...pageNumbers];\n          const results = [];\n          const workerPromises = workerPool.map(async (worker, workerIndex) => {\n            while (pageQueue.length > 0) {\n              const pageNum = pageQueue.shift();\n              this.log(\n                `\\x1b[34m[Worker ${\n                  workerIndex + 1\n                }]\\x1b[0m assigned pg${pageNum}`\n              );\n              const page = await pdfDocument.getPage(pageNum);\n              const imageBuffer = await pdfSharp.pageToBuffer({ page });\n              if (!imageBuffer) continue;\n              const { data } = await worker.recognize(imageBuffer, {}, \"text\");\n              this.log(\n                `✅ \\x1b[34m[Worker ${\n                  workerIndex + 1\n                }]\\x1b[0m completed pg${pageNum}`\n              );\n              results.push({\n                pageContent: data.text,\n                metadata: {\n                  ...metadata,\n                  loc: { pageNumber: pageNum },\n                },\n              });\n            }\n          });\n\n          await Promise.all(workerPromises);\n          documents.push(\n            ...results.sort(\n              (a, b) => a.metadata.loc.pageNumber - b.metadata.loc.pageNumber\n            )\n          );\n        }\n        return documents;\n      };\n\n      await Promise.race([timeoutPromise, processPages()]);\n    } catch (e) {\n      this.log(`Error: ${e.message}`, e.stack);\n    } finally {\n      global.Image = undefined;\n      await Promise.all(workerPool.map((worker) => worker.terminate()));\n    }\n\n    this.log(`Completed OCR of ${documentTitle}!`, {\n      documentsParsed: documents.length,\n      totalPages: totalPages,\n      executionTime: `${((Date.now() - startTime) / 1000).toFixed(2)}s`,\n    });\n    return documents;\n  }\n\n  /**\n   * Loads an image file and returns the OCRed text.\n   * @param {string} filePath - The path to the image file.\n   * @param {Object} options - The options for the OCR.\n   * @param {number} options.maxExecutionTime - The maximum execution time of the OCR in milliseconds.\n   * @returns {Promise<string>} The OCRed text.\n   */\n  async ocrImage(filePath, { maxExecutionTime = 300_000 } = {}) {\n    let content = \"\";\n    let worker = null;\n    if (\n      !filePath ||\n      !fs.existsSync(filePath) ||\n      !fs.statSync(filePath).isFile()\n    ) {\n      this.log(`File ${filePath} does not exist. Skipping OCR.`);\n      return null;\n    }\n\n    const documentTitle = path.basename(filePath);\n    try {\n      this.log(`Starting OCR of ${documentTitle}`);\n      const startTime = Date.now();\n      const { createWorker, OEM } = require(\"tesseract.js\");\n      worker = await createWorker(this.language, OEM.LSTM_ONLY, {\n        cachePath: this.cacheDir,\n      });\n\n      // Race the timeout with the OCR\n      const timeoutPromise = new Promise((_, reject) => {\n        setTimeout(() => {\n          reject(\n            new Error(\n              `OCR job took too long to complete (${\n                maxExecutionTime / 1000\n              } seconds)`\n            )\n          );\n        }, maxExecutionTime);\n      });\n\n      const processImage = async () => {\n        const { data } = await worker.recognize(filePath, {}, \"text\");\n        content = data.text;\n      };\n\n      await Promise.race([timeoutPromise, processImage()]);\n      this.log(`Completed OCR of ${documentTitle}!`, {\n        executionTime: `${((Date.now() - startTime) / 1000).toFixed(2)}s`,\n      });\n\n      return content;\n    } catch (e) {\n      this.log(`Error: ${e.message}`);\n      return null;\n    } finally {\n      //eslint-disable-next-line\n      if (!worker) return;\n      await worker.terminate();\n    }\n  }\n}\n\n/**\n * Converts a PDF page to a buffer using Sharp.\n * @param {Object} options - The options for the Sharp PDF page object.\n * @param {Object} options.page - The PDFJS page proxy object.\n * @returns {Promise<Buffer>} The buffer of the page.\n */\nclass PDFSharp {\n  constructor({ validOps = [] } = {}) {\n    this.sharp = null;\n    this.validOps = validOps;\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[PDFSharp]\\x1b[0m ${text}`, ...args);\n  }\n\n  async init() {\n    this.sharp = (await import(\"sharp\")).default;\n  }\n\n  /**\n   * Converts a PDF page to a buffer.\n   * @param {Object} options - The options for the Sharp PDF page object.\n   * @param {Object} options.page - The PDFJS page proxy object.\n   * @returns {Promise<Buffer>} The buffer of the page.\n   */\n  async pageToBuffer({ page }) {\n    if (!this.sharp) await this.init();\n    try {\n      this.log(`Converting page ${page.pageNumber} to image...`);\n      const ops = await page.getOperatorList();\n      const pageImages = ops.fnArray.length;\n\n      for (let i = 0; i < pageImages; i++) {\n        try {\n          if (!this.validOps.includes(ops.fnArray[i])) continue;\n\n          const name = ops.argsArray[i][0];\n          const img = await page.objs.get(name);\n          const { width, height } = img;\n          const size = img.data.length;\n          const channels = size / width / height;\n          const targetDPI = 70;\n          const targetWidth = Math.floor(width * (targetDPI / 72));\n          const targetHeight = Math.floor(height * (targetDPI / 72));\n\n          const image = this.sharp(img.data, {\n            raw: { width, height, channels },\n            density: targetDPI,\n          })\n            .resize({\n              width: targetWidth,\n              height: targetHeight,\n              fit: \"fill\",\n            })\n            .withMetadata({\n              density: targetDPI,\n              resolution: targetDPI,\n            })\n            .png();\n\n          // For debugging purposes\n          // await image.toFile(path.resolve(__dirname, `../../storage/`, `pg${page.pageNumber}.png`));\n          return await image.toBuffer();\n        } catch (error) {\n          this.log(`Iteration error: ${error.message}`, error.stack);\n          continue;\n        }\n      }\n      this.log(`No valid images found on page ${page.pageNumber}`);\n      return null;\n    } catch (error) {\n      this.log(`Error: ${error.message}`, error.stack);\n      return null;\n    }\n  }\n}\n\nmodule.exports = OCRLoader;\n"
  },
  {
    "path": "collector/utils/OCRLoader/validLangs.js",
    "content": "/*\n\nTo get the list of valid language codes - do the following:\nOpen the following URL in your browser: https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html\n\nCheck this element is the proper table tbody with all the codes via console:\ndocument.getElementsByTagName('table').item(0).children.item(1)\n\nNow, copy the following code and paste it into the console:\nfunction parseLangs() {\nlet langs = {};\n  Array.from(document.getElementsByTagName('table').item(0).children.item(1).children).forEach((el) => {\n    const [codeEl, languageEl, ...rest] = el.children\n    const code = codeEl.innerText.trim()\n    const language = languageEl.innerText.trim()\n    if (!!code && !!language) langs[code] = language\n  })\n  return langs;\n}\n\nnow, run the function:\ncopy(parseLangs())\n*/\n\nconst VALID_LANGUAGE_CODES = {\n  afr: \"Afrikaans\",\n  amh: \"Amharic\",\n  ara: \"Arabic\",\n  asm: \"Assamese\",\n  aze: \"Azerbaijani\",\n  aze_cyrl: \"Azerbaijani - Cyrilic\",\n  bel: \"Belarusian\",\n  ben: \"Bengali\",\n  bod: \"Tibetan\",\n  bos: \"Bosnian\",\n  bre: \"Breton\",\n  bul: \"Bulgarian\",\n  cat: \"Catalan; Valencian\",\n  ceb: \"Cebuano\",\n  ces: \"Czech\",\n  chi_sim: \"Chinese - Simplified\",\n  chi_tra: \"Chinese - Traditional\",\n  chr: \"Cherokee\",\n  cos: \"Corsican\",\n  cym: \"Welsh\",\n  dan: \"Danish\",\n  dan_frak: \"Danish - Fraktur (contrib)\",\n  deu: \"German\",\n  deu_frak: \"German - Fraktur (contrib)\",\n  deu_latf: \"German (Fraktur Latin)\",\n  dzo: \"Dzongkha\",\n  ell: \"Greek, Modern (1453-)\",\n  eng: \"English\",\n  enm: \"English, Middle (1100-1500)\",\n  epo: \"Esperanto\",\n  equ: \"Math / equation detection module\",\n  est: \"Estonian\",\n  eus: \"Basque\",\n  fao: \"Faroese\",\n  fas: \"Persian\",\n  fil: \"Filipino (old - Tagalog)\",\n  fin: \"Finnish\",\n  fra: \"French\",\n  frk: \"German - Fraktur (now deu_latf)\",\n  frm: \"French, Middle (ca.1400-1600)\",\n  fry: \"Western Frisian\",\n  gla: \"Scottish Gaelic\",\n  gle: \"Irish\",\n  glg: \"Galician\",\n  grc: \"Greek, Ancient (to 1453) (contrib)\",\n  guj: \"Gujarati\",\n  hat: \"Haitian; Haitian Creole\",\n  heb: \"Hebrew\",\n  hin: \"Hindi\",\n  hrv: \"Croatian\",\n  hun: \"Hungarian\",\n  hye: \"Armenian\",\n  iku: \"Inuktitut\",\n  ind: \"Indonesian\",\n  isl: \"Icelandic\",\n  ita: \"Italian\",\n  ita_old: \"Italian - Old\",\n  jav: \"Javanese\",\n  jpn: \"Japanese\",\n  kan: \"Kannada\",\n  kat: \"Georgian\",\n  kat_old: \"Georgian - Old\",\n  kaz: \"Kazakh\",\n  khm: \"Central Khmer\",\n  kir: \"Kirghiz; Kyrgyz\",\n  kmr: \"Kurmanji (Kurdish - Latin Script)\",\n  kor: \"Korean\",\n  kor_vert: \"Korean (vertical)\",\n  kur: \"Kurdish (Arabic Script)\",\n  lao: \"Lao\",\n  lat: \"Latin\",\n  lav: \"Latvian\",\n  lit: \"Lithuanian\",\n  ltz: \"Luxembourgish\",\n  mal: \"Malayalam\",\n  mar: \"Marathi\",\n  mkd: \"Macedonian\",\n  mlt: \"Maltese\",\n  mon: \"Mongolian\",\n  mri: \"Maori\",\n  msa: \"Malay\",\n  mya: \"Burmese\",\n  nep: \"Nepali\",\n  nld: \"Dutch; Flemish\",\n  nor: \"Norwegian\",\n  oci: \"Occitan (post 1500)\",\n  ori: \"Oriya\",\n  osd: \"Orientation and script detection module\",\n  pan: \"Panjabi; Punjabi\",\n  pol: \"Polish\",\n  por: \"Portuguese\",\n  pus: \"Pushto; Pashto\",\n  que: \"Quechua\",\n  ron: \"Romanian; Moldavian; Moldovan\",\n  rus: \"Russian\",\n  san: \"Sanskrit\",\n  sin: \"Sinhala; Sinhalese\",\n  slk: \"Slovak\",\n  slk_frak: \"Slovak - Fraktur (contrib)\",\n  slv: \"Slovenian\",\n  snd: \"Sindhi\",\n  spa: \"Spanish; Castilian\",\n  spa_old: \"Spanish; Castilian - Old\",\n  sqi: \"Albanian\",\n  srp: \"Serbian\",\n  srp_latn: \"Serbian - Latin\",\n  sun: \"Sundanese\",\n  swa: \"Swahili\",\n  swe: \"Swedish\",\n  syr: \"Syriac\",\n  tam: \"Tamil\",\n  tat: \"Tatar\",\n  tel: \"Telugu\",\n  tgk: \"Tajik\",\n  tgl: \"Tagalog (new - Filipino)\",\n  tha: \"Thai\",\n  tir: \"Tigrinya\",\n  ton: \"Tonga\",\n  tur: \"Turkish\",\n  uig: \"Uighur; Uyghur\",\n  ukr: \"Ukrainian\",\n  urd: \"Urdu\",\n  uzb: \"Uzbek\",\n  uzb_cyrl: \"Uzbek - Cyrilic\",\n  vie: \"Vietnamese\",\n  yid: \"Yiddish\",\n  yor: \"Yoruba\",\n};\n\nmodule.exports.VALID_LANGUAGE_CODES = VALID_LANGUAGE_CODES;\n"
  },
  {
    "path": "collector/utils/WhisperProviders/OpenAiWhisper.js",
    "content": "const fs = require(\"fs\");\n\nclass OpenAiWhisper {\n  constructor({ options }) {\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    if (!options.openAiKey) throw new Error(\"No OpenAI API key was set.\");\n\n    this.openai = new OpenAIApi({\n      apiKey: options.openAiKey,\n    });\n    this.model = \"whisper-1\";\n    this.temperature = 0;\n    this.#log(\"Initialized.\");\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[32m[OpenAiWhisper]\\x1b[0m ${text}`, ...args);\n  }\n\n  async processFile(fullFilePath) {\n    return await this.openai.audio.transcriptions\n      .create({\n        file: fs.createReadStream(fullFilePath),\n        model: this.model,\n        temperature: this.temperature,\n      })\n      .then((response) => {\n        if (!response) {\n          return {\n            content: \"\",\n            error: \"No content was able to be transcribed.\",\n          };\n        }\n\n        return { content: response.text, error: null };\n      })\n      .catch((error) => {\n        this.#log(\n          `Could not get any response from openai whisper`,\n          error.message\n        );\n        return { content: \"\", error: error.message };\n      });\n  }\n}\n\nmodule.exports = {\n  OpenAiWhisper,\n};\n"
  },
  {
    "path": "collector/utils/WhisperProviders/ffmpeg/index.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { execSync, spawnSync } = require(\"child_process\");\nconst { patchShellEnvironmentPath } = require(\"../../shell\");\n/**\n * Custom FFMPEG wrapper class for audio file conversion.\n * Replaces deprecated fluent-ffmpeg package.\n * Locates ffmpeg binary and converts audio files to required\n * WAV format (16k hz mono 32f) for Whisper transcription.\n *\n * @class FFMPEGWrapper\n */\nclass FFMPEGWrapper {\n  static _instance;\n\n  constructor() {\n    if (FFMPEGWrapper._instance) return FFMPEGWrapper._instance;\n    FFMPEGWrapper._instance = this;\n    this._ffmpegPath = null;\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[35m[FFMPEG]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Locates ffmpeg binary.\n   * Uses fix-path on non-Windows platforms to ensure we can find ffmpeg.\n   *\n   * @returns {Promise<string>} Path to ffmpeg binary\n   * @throws {Error}\n   */\n  async ffmpegPath() {\n    if (this._ffmpegPath) return this._ffmpegPath;\n    await patchShellEnvironmentPath();\n\n    try {\n      const which = process.platform === \"win32\" ? \"where\" : \"which\";\n      const result = execSync(`${which} ffmpeg`, { encoding: \"utf8\" }).trim();\n      const candidatePath = result?.split(\"\\n\")?.[0]?.trim();\n      if (!candidatePath) throw new Error(\"FFMPEG candidate path not found.\");\n      if (!this.isValidFFMPEG(candidatePath))\n        throw new Error(\"FFMPEG candidate path is not valid ffmpeg binary.\");\n\n      this.log(`Found FFMPEG binary at ${candidatePath}`);\n      this._ffmpegPath = candidatePath;\n      return this._ffmpegPath;\n    } catch (error) {\n      this.log(error.message);\n    }\n\n    throw new Error(\"FFMPEG binary not found.\");\n  }\n\n  /**\n   * Validates that path points to a valid ffmpeg binary.\n   * Runs ffmpeg -version command.\n   *\n   * @param {string} pathToTest - Path of ffmpeg binary\n   * @returns {boolean}\n   */\n  isValidFFMPEG(pathToTest) {\n    try {\n      if (!pathToTest || !fs.existsSync(pathToTest)) return false;\n      execSync(`\"${pathToTest}\" -version`, { encoding: \"utf8\", stdio: \"pipe\" });\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  /**\n   * Converts audio file to WAV format with required parameters for Whisper.\n   * Output: 16k hz, mono, 32bit float.\n   *\n   * @param {string} inputPath - Input path for audio file (any format supported by ffmpeg)\n   * @param {string} outputPath - Output path for converted file\n   * @returns {Promise<boolean>}\n   * @throws {Error} If ffmpeg binary cannot be found or conversion fails\n   */\n  async convertAudioToWav(inputPath, outputPath) {\n    if (!fs.existsSync(inputPath))\n      throw new Error(`Input file ${inputPath} does not exist.`);\n    const outputDir = path.dirname(outputPath);\n    if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n    this.log(`Converting ${path.basename(inputPath)} to WAV format...`);\n    // Convert to 16k hz mono 32f\n    const result = spawnSync(\n      await this.ffmpegPath(),\n      [\n        \"-i\",\n        inputPath,\n        \"-ar\",\n        \"16000\",\n        \"-ac\",\n        \"1\",\n        \"-acodec\",\n        \"pcm_f32le\",\n        \"-y\",\n        outputPath,\n      ],\n      { encoding: \"utf8\" }\n    );\n\n    // ffmpeg writes progress to stderr\n    if (result.stderr) this.log(result.stderr.trim());\n    if (result.status !== 0) throw new Error(`FFMPEG conversion failed`);\n    this.log(`Conversion complete: ${path.basename(outputPath)}`);\n    return true;\n  }\n}\n\nmodule.exports = { FFMPEGWrapper };\n"
  },
  {
    "path": "collector/utils/WhisperProviders/localWhisper.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { v4 } = require(\"uuid\");\nconst defaultWhisper = \"Xenova/whisper-small\"; // Model Card: https://huggingface.co/Xenova/whisper-small\nconst fileSize = {\n  \"Xenova/whisper-small\": \"250mb\",\n  \"Xenova/whisper-large\": \"1.56GB\",\n};\n\nclass LocalWhisper {\n  constructor({ options }) {\n    this.model = options?.WhisperModelPref ?? defaultWhisper;\n    this.fileSize = fileSize[this.model];\n    this.cacheDir = path.resolve(\n      process.env.STORAGE_DIR\n        ? path.resolve(process.env.STORAGE_DIR, `models`)\n        : path.resolve(__dirname, `../../../server/storage/models`)\n    );\n\n    this.modelPath = path.resolve(this.cacheDir, ...this.model.split(\"/\"));\n    // Make directory when it does not exist in existing installations\n    if (!fs.existsSync(this.cacheDir))\n      fs.mkdirSync(this.cacheDir, { recursive: true });\n\n    this.#log(\"Initialized.\");\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[32m[LocalWhisper]\\x1b[0m ${text}`, ...args);\n  }\n\n  #validateAudioFile(wavFile) {\n    const sampleRate = wavFile.fmt.sampleRate;\n    const duration = wavFile.data.samples / sampleRate;\n\n    // Most speech recognition systems expect minimum 8kHz\n    // But we'll set it lower to be safe\n    if (sampleRate < 4000) {\n      // 4kHz minimum\n      throw new Error(\n        \"Audio file sample rate is too low for accurate transcription. Minimum required is 4kHz.\"\n      );\n    }\n\n    // Typical audio file duration limits\n    const MAX_DURATION_SECONDS = 4 * 60 * 60; // 4 hours\n    if (duration > MAX_DURATION_SECONDS) {\n      throw new Error(\"Audio file duration exceeds maximum limit of 4 hours.\");\n    }\n\n    // Check final sample count after upsampling to prevent memory issues\n    const targetSampleRate = 16000;\n    const upsampledSamples = duration * targetSampleRate;\n    const MAX_SAMPLES = 230_400_000; // ~4 hours at 16kHz\n\n    if (upsampledSamples > MAX_SAMPLES) {\n      throw new Error(\"Audio file exceeds maximum allowed length.\");\n    }\n\n    return true;\n  }\n\n  async #convertToWavAudioData(sourcePath) {\n    try {\n      let buffer;\n      const wavefile = require(\"wavefile\");\n      const { FFMPEGWrapper } = require(\"./ffmpeg\");\n      const ffmpeg = new FFMPEGWrapper();\n      const outFolder = path.resolve(__dirname, `../../storage/tmp`);\n      if (!fs.existsSync(outFolder))\n        fs.mkdirSync(outFolder, { recursive: true });\n\n      const outputFile = path.resolve(outFolder, `${v4()}.wav`);\n      const success = await ffmpeg.convertAudioToWav(sourcePath, outputFile);\n      if (!success)\n        throw new Error(\n          \"[Conversion Failed]: Could not convert file to .wav format!\"\n        );\n\n      buffer = fs.readFileSync(outputFile);\n      fs.rmSync(outputFile);\n\n      const wavFile = new wavefile.WaveFile(buffer);\n      try {\n        this.#validateAudioFile(wavFile);\n      } catch (error) {\n        this.#log(`Audio validation failed: ${error.message}`);\n        throw new Error(`Invalid audio file: ${error.message}`);\n      }\n\n      // Although we use ffmpeg to convert to the correct format (16k hz 32f),\n      // different versions of ffmpeg produce different results based on the\n      // environment. To ensure consistency, we convert to the correct format again.\n      wavFile.toBitDepth(\"32f\");\n      wavFile.toSampleRate(16000);\n\n      let audioData = wavFile.getSamples();\n      if (Array.isArray(audioData)) {\n        if (audioData.length > 1) {\n          const SCALING_FACTOR = Math.sqrt(2);\n\n          // Merge channels into first channel to save memory\n          for (let i = 0; i < audioData[0].length; ++i) {\n            audioData[0][i] =\n              (SCALING_FACTOR * (audioData[0][i] + audioData[1][i])) / 2;\n          }\n        }\n        audioData = audioData[0];\n      }\n\n      return audioData;\n    } catch (error) {\n      console.error(`convertToWavAudioData`, error);\n      return null;\n    }\n  }\n\n  async client() {\n    if (!fs.existsSync(this.modelPath)) {\n      this.#log(\n        `The native whisper model has never been run and will be downloaded right now. Subsequent runs will be faster. (~${this.fileSize})`\n      );\n    }\n\n    try {\n      // Convert ESM to CommonJS via import so we can load this library.\n      const pipeline = (...args) =>\n        import(\"@xenova/transformers\").then(({ pipeline }) => {\n          return pipeline(...args);\n        });\n      return await pipeline(\"automatic-speech-recognition\", this.model, {\n        cache_dir: this.cacheDir,\n        ...(!fs.existsSync(this.modelPath)\n          ? {\n              // Show download progress if we need to download any files\n              progress_callback: (data) => {\n                if (!data.hasOwnProperty(\"progress\")) return;\n                console.log(\n                  `\\x1b[34m[ONNXWhisper - Downloading Model Files]\\x1b[0m ${\n                    data.file\n                  } ${~~data?.progress}%`\n                );\n              },\n            }\n          : {}),\n      });\n    } catch (error) {\n      let errMsg = error.message;\n      if (errMsg.includes(\"Could not locate file\")) {\n        errMsg =\n          \"The native whisper model failed to download from the huggingface.co CDN. Your internet connection may be unstable or blocked by Huggingface.co - you will need to download the model manually and place it in the storage/models folder to use local Whisper transcription.\";\n      }\n\n      this.#log(\n        `Failed to load the native whisper model: ${errMsg}`,\n        error.stack\n      );\n      throw new Error(errMsg);\n    }\n  }\n\n  async processFile(fullFilePath, filename) {\n    try {\n      const audioDataPromise = new Promise((resolve) =>\n        this.#convertToWavAudioData(fullFilePath).then((audioData) =>\n          resolve(audioData)\n        )\n      );\n      const [audioData, transcriber] = await Promise.all([\n        audioDataPromise,\n        this.client(),\n      ]);\n\n      if (!audioData) {\n        this.#log(`Failed to parse content from ${filename}.`);\n        return {\n          content: null,\n          error: `Failed to parse content from ${filename}.`,\n        };\n      }\n\n      this.#log(`Transcribing audio data to text...`);\n      const { text } = await transcriber(audioData, {\n        chunk_length_s: 30,\n        stride_length_s: 5,\n      });\n\n      return { content: text, error: null };\n    } catch (error) {\n      return { content: null, error: error.message };\n    }\n  }\n}\n\nmodule.exports = {\n  LocalWhisper,\n};\n"
  },
  {
    "path": "collector/utils/comKey/index.js",
    "content": "const crypto = require(\"crypto\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst keyPath =\n  process.env.NODE_ENV === \"development\"\n    ? path.resolve(__dirname, `../../../server/storage/comkey`)\n    : path.resolve(\n        process.env.STORAGE_DIR ??\n          path.resolve(__dirname, `../../../server/storage`),\n        `comkey`\n      );\n\nclass CommunicationKey {\n  #pubKeyName = \"ipc-pub.pem\";\n  #storageLoc = keyPath;\n\n  constructor() {}\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[CommunicationKeyVerify]\\x1b[0m ${text}`, ...args);\n  }\n\n  #readPublicKey() {\n    return fs.readFileSync(path.resolve(this.#storageLoc, this.#pubKeyName));\n  }\n\n  // Given a signed payload from private key from /app/server/ this signature should\n  // decode to match the textData provided. This class does verification only in collector.\n  // Note: The textData is typically the JSON stringified body sent to the document processor API.\n  verify(signature = \"\", textData = \"\") {\n    try {\n      let data = textData;\n      if (typeof textData !== \"string\") data = JSON.stringify(data);\n      return crypto.verify(\n        \"RSA-SHA256\",\n        Buffer.from(data),\n        this.#readPublicKey(),\n        Buffer.from(signature, \"hex\")\n      );\n    } catch {}\n    return false;\n  }\n\n  // Use the rolling public-key to decrypt arbitrary data that was encrypted via the private key on the server side CommunicationKey class\n  // that we know was done with the same key-pair and the given input is in base64 format already.\n  // Returns plaintext string of the data that was encrypted.\n  decrypt(base64String = \"\") {\n    return crypto\n      .publicDecrypt(this.#readPublicKey(), Buffer.from(base64String, \"base64\"))\n      .toString();\n  }\n}\n\nmodule.exports = { CommunicationKey };\n"
  },
  {
    "path": "collector/utils/constants.js",
    "content": "const WATCH_DIRECTORY = require(\"path\").resolve(__dirname, \"../hotdir\");\n\nconst ACCEPTED_MIMES = {\n  \"text/plain\": [\".txt\", \".md\", \".org\", \".adoc\", \".rst\"],\n  \"text/html\": [\".html\"],\n  \"text/csv\": [\".csv\"],\n  \"application/json\": [\".json\"],\n  // TODO: Create asDoc.js that works for standard MS Word files.\n  // \"application/msword\": [\".doc\"],\n\n  \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": [\n    \".docx\",\n  ],\n  \"application/vnd.openxmlformats-officedocument.presentationml.presentation\": [\n    \".pptx\",\n  ],\n\n  \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\": [\n    \".xlsx\",\n  ],\n\n  \"application/vnd.oasis.opendocument.text\": [\".odt\"],\n  \"application/vnd.oasis.opendocument.presentation\": [\".odp\"],\n\n  \"application/pdf\": [\".pdf\"],\n  \"application/mbox\": [\".mbox\"],\n\n  \"audio/wav\": [\".wav\"],\n  \"audio/mpeg\": [\".mp3\"],\n\n  \"video/mp4\": [\".mp4\"],\n  \"video/mpeg\": [\".mpeg\"],\n  \"application/epub+zip\": [\".epub\"],\n  \"image/png\": [\".png\"],\n  \"image/jpeg\": [\".jpg\"],\n  \"image/jpg\": [\".jpg\"],\n  \"image/webp\": [\".webp\"],\n};\n\nconst SUPPORTED_FILETYPE_CONVERTERS = {\n  \".txt\": \"./convert/asTxt.js\",\n  \".md\": \"./convert/asTxt.js\",\n  \".org\": \"./convert/asTxt.js\",\n  \".adoc\": \"./convert/asTxt.js\",\n  \".rst\": \"./convert/asTxt.js\",\n  \".csv\": \"./convert/asTxt.js\",\n  \".json\": \"./convert/asTxt.js\",\n\n  \".html\": \"./convert/asTxt.js\",\n  \".pdf\": \"./convert/asPDF/index.js\",\n\n  \".docx\": \"./convert/asDocx.js\",\n  // TODO: Create asDoc.js that works for standard MS Word files.\n  // \".doc\": \"./convert/asDoc.js\",\n\n  \".pptx\": \"./convert/asOfficeMime.js\",\n\n  \".odt\": \"./convert/asOfficeMime.js\",\n  \".odp\": \"./convert/asOfficeMime.js\",\n\n  \".xlsx\": \"./convert/asXlsx.js\",\n\n  \".mbox\": \"./convert/asMbox.js\",\n\n  \".epub\": \"./convert/asEPub.js\",\n\n  \".mp3\": \"./convert/asAudio.js\",\n  \".wav\": \"./convert/asAudio.js\",\n  \".mp4\": \"./convert/asAudio.js\",\n  \".mpeg\": \"./convert/asAudio.js\",\n\n  \".png\": \"./convert/asImage.js\",\n  \".jpg\": \"./convert/asImage.js\",\n  \".jpeg\": \"./convert/asImage.js\",\n  \".webp\": \"./convert/asImage.js\",\n};\n\nmodule.exports = {\n  SUPPORTED_FILETYPE_CONVERTERS,\n  WATCH_DIRECTORY,\n  ACCEPTED_MIMES,\n};\n"
  },
  {
    "path": "collector/utils/downloadURIToFile/index.js",
    "content": "const { WATCH_DIRECTORY } = require(\"../constants\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { pipeline } = require(\"stream/promises\");\nconst { validURL } = require(\"../url\");\nconst { default: slugify } = require(\"slugify\");\n\n/**\n * Download a file to the hotdir\n * @param {string} url - The URL of the file to download\n * @param {number} maxTimeout - The maximum timeout in milliseconds\n * @returns {Promise<{success: boolean, fileLocation: string|null, reason: string|null}>} - The path to the downloaded file\n */\nasync function downloadURIToFile(url, maxTimeout = 10_000) {\n  if (!url || typeof url !== \"string\" || !validURL(url))\n    return { success: false, reason: \"Not a valid URL.\", fileLocation: null };\n\n  try {\n    const abortController = new AbortController();\n    const timeout = setTimeout(() => {\n      abortController.abort();\n      console.error(\n        `Timeout ${maxTimeout}ms reached while downloading file for URL:`,\n        url.toString()\n      );\n    }, maxTimeout);\n\n    const res = await fetch(url, { signal: abortController.signal })\n      .then((res) => {\n        if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);\n        return res;\n      })\n      .finally(() => clearTimeout(timeout));\n\n    const urlObj = new URL(url);\n    const filename = `${urlObj.hostname}-${slugify(\n      urlObj.pathname.replace(/\\//g, \"-\"),\n      { lower: true }\n    )}`;\n    const localFilePath = path.join(WATCH_DIRECTORY, filename);\n    const writeStream = fs.createWriteStream(localFilePath);\n    await pipeline(res.body, writeStream);\n\n    console.log(`[SUCCESS]: File ${localFilePath} downloaded to hotdir.`);\n    return { success: true, fileLocation: localFilePath, reason: null };\n  } catch (error) {\n    console.error(`Error writing to hotdir: ${error} for URL: ${url}`);\n    return { success: false, reason: error.message, fileLocation: null };\n  }\n}\n\nmodule.exports = {\n  downloadURIToFile,\n};\n"
  },
  {
    "path": "collector/utils/extensions/Confluence/ConfluenceLoader/index.js",
    "content": "/*\n * This is a custom implementation of the Confluence langchain loader. There was an issue where\n * code blocks were not being extracted. This is a temporary fix until this issue is resolved.*/\n\nconst { htmlToText } = require(\"html-to-text\");\n\nclass ConfluencePagesLoader {\n  constructor({\n    baseUrl,\n    spaceKey,\n    username,\n    accessToken,\n    limit = 25,\n    expand = \"body.storage,version\",\n    personalAccessToken,\n    cloud = true,\n    bypassSSL = false,\n  }) {\n    this.baseUrl = baseUrl;\n    this.spaceKey = spaceKey;\n    this.username = username;\n    this.accessToken = accessToken;\n    this.limit = limit;\n    this.expand = expand;\n    this.personalAccessToken = personalAccessToken;\n    this.cloud = cloud;\n    this.bypassSSL = bypassSSL;\n    this.log(\"Initialized Confluence Loader\");\n    if (this.bypassSSL)\n      this.log(\"!!SSL bypass is enabled!! Use at your own risk!!\");\n  }\n\n  log(message, ...args) {\n    console.log(`\\x1b[36m[Confluence Loader]\\x1b[0m ${message}`, ...args);\n  }\n\n  get authorizationHeader() {\n    if (this.personalAccessToken) {\n      return `Bearer ${this.personalAccessToken}`;\n    } else if (this.username && this.accessToken) {\n      const authToken = Buffer.from(\n        `${this.username}:${this.accessToken}`\n      ).toString(\"base64\");\n      return `Basic ${authToken}`;\n    }\n    return undefined;\n  }\n\n  async load(options) {\n    try {\n      const pages = await this.fetchAllPagesInSpace(\n        options?.start,\n        options?.limit\n      );\n      return pages.map((page) => this.createDocumentFromPage(page));\n    } catch (error) {\n      this.log(\"Error:\", error);\n      return [];\n    }\n  }\n\n  async fetchConfluenceData(url) {\n    try {\n      const initialHeaders = {\n        \"Content-Type\": \"application/json\",\n        Accept: \"application/json\",\n      };\n      const authHeader = this.authorizationHeader;\n      if (authHeader) initialHeaders.Authorization = authHeader;\n\n      // If SSL bypass is enabled, set the NODE_TLS_REJECT_UNAUTHORIZED environment variable\n      if (this.bypassSSL) process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\";\n      const response = await fetch(url, { headers: initialHeaders });\n      if (!response.ok) {\n        throw new Error(\n          `Failed to fetch ${url} from Confluence: ${response.status}`\n        );\n      }\n      return await response.json();\n    } catch (error) {\n      this.log(\"Error:\", error);\n      throw new Error(error.message);\n    } finally {\n      if (this.bypassSSL) process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"1\";\n    }\n  }\n\n  // https://developer.atlassian.com/cloud/confluence/rest/v2/intro/#auth\n  async fetchAllPagesInSpace(start = 0, limit = this.limit) {\n    const url = `${this.baseUrl}${\n      this.cloud ? \"/wiki\" : \"\"\n    }/rest/api/content?spaceKey=${\n      this.spaceKey\n    }&limit=${limit}&start=${start}&expand=${this.expand}`;\n    const data = await this.fetchConfluenceData(url);\n    if (data.size === 0) {\n      return [];\n    }\n    const nextPageStart = start + data.size;\n    const nextPageResults = await this.fetchAllPagesInSpace(\n      nextPageStart,\n      limit\n    );\n    return data.results.concat(nextPageResults);\n  }\n\n  createDocumentFromPage(page) {\n    // Function to extract code blocks\n    const extractCodeBlocks = (content) => {\n      const codeBlockRegex =\n        /<ac:structured-macro ac:name=\"code\"[^>]*>[\\s\\S]*?<ac:plain-text-body><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/ac:plain-text-body>[\\s\\S]*?<\\/ac:structured-macro>/g;\n      const languageRegex =\n        /<ac:parameter ac:name=\"language\">(.*?)<\\/ac:parameter>/;\n\n      return content.replace(codeBlockRegex, (match) => {\n        const language = match.match(languageRegex)?.[1] || \"\";\n        const code =\n          match.match(\n            /<ac:plain-text-body><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/ac:plain-text-body>/\n          )?.[1] || \"\";\n        return `\\n\\`\\`\\`${language}\\n${code.trim()}\\n\\`\\`\\`\\n`;\n      });\n    };\n\n    const contentWithCodeBlocks = extractCodeBlocks(page.body.storage.value);\n    const plainTextContent = htmlToText(contentWithCodeBlocks, {\n      wordwrap: false,\n      preserveNewlines: true,\n    });\n    const textWithPreservedStructure = plainTextContent.replace(\n      /\\n{3,}/g,\n      \"\\n\\n\"\n    );\n    const pageUrl = `${this.baseUrl}${this.cloud ? \"/wiki\" : \"\"}/spaces/${\n      this.spaceKey\n    }/pages/${page.id}`;\n\n    return {\n      pageContent: textWithPreservedStructure,\n      metadata: {\n        id: page.id,\n        status: page.status,\n        title: page.title,\n        type: page.type,\n        url: pageUrl,\n        version: page.version?.number,\n        updated_by: page.version?.by?.displayName,\n        updated_at: page.version?.when,\n      },\n    };\n  }\n}\n\nmodule.exports = { ConfluencePagesLoader };\n"
  },
  {
    "path": "collector/utils/extensions/Confluence/index.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { default: slugify } = require(\"slugify\");\nconst { v4 } = require(\"uuid\");\nconst { writeToServerDocuments, sanitizeFileName } = require(\"../../files\");\nconst { tokenizeString } = require(\"../../tokenizer\");\nconst { ConfluencePagesLoader } = require(\"./ConfluenceLoader\");\n\n/**\n * Load Confluence documents from a spaceID and Confluence credentials\n * @param {object} args - forwarded request body params\n * @param {import(\"../../../middleware/setDataSigner\").ResponseWithSigner} response - Express response object with encryptionWorker\n * @returns\n */\nasync function loadConfluence(\n  {\n    baseUrl = null,\n    spaceKey = null,\n    username = null,\n    accessToken = null,\n    cloud = true,\n    personalAccessToken = null,\n    bypassSSL = false,\n  },\n  response\n) {\n  if (!personalAccessToken && (!username || !accessToken)) {\n    return {\n      success: false,\n      reason:\n        \"You need either a personal access token (PAT), or a username and access token to use the Confluence connector.\",\n    };\n  }\n\n  if (!baseUrl || !validBaseUrl(baseUrl)) {\n    return {\n      success: false,\n      reason: \"Provided base URL is not a valid URL.\",\n    };\n  }\n\n  if (!spaceKey) {\n    return {\n      success: false,\n      reason: \"You need to provide a Confluence space key.\",\n    };\n  }\n\n  const { origin, hostname } = new URL(baseUrl);\n  console.log(`-- Working Confluence ${origin} --`);\n  const loader = new ConfluencePagesLoader({\n    baseUrl: origin, // Use the origin to avoid issues with subdomains, ports, protocols, etc.\n    spaceKey,\n    username,\n    accessToken,\n    cloud,\n    personalAccessToken,\n    bypassSSL,\n  });\n\n  const { docs, error } = await loader\n    .load()\n    .then((docs) => {\n      return { docs, error: null };\n    })\n    .catch((e) => {\n      return {\n        docs: [],\n        error: e.message?.split(\"Error:\")?.[1] || e.message,\n      };\n    });\n\n  if (!docs.length || !!error) {\n    return {\n      success: false,\n      reason: error ?? \"No pages found for that Confluence space.\",\n    };\n  }\n  const outFolder = slugify(\n    `confluence-${hostname}-${v4().slice(0, 4)}`\n  ).toLowerCase();\n\n  const outFolderPath =\n    process.env.NODE_ENV === \"development\"\n      ? path.resolve(\n          __dirname,\n          `../../../../server/storage/documents/${outFolder}`\n        )\n      : path.resolve(process.env.STORAGE_DIR, `documents/${outFolder}`);\n\n  if (!fs.existsSync(outFolderPath))\n    fs.mkdirSync(outFolderPath, { recursive: true });\n\n  docs.forEach((doc) => {\n    if (!doc.pageContent) return;\n\n    const data = {\n      id: v4(),\n      url: doc.metadata.url + \".page\",\n      title: doc.metadata.title || doc.metadata.source,\n      docAuthor: origin,\n      description: doc.metadata.title,\n      docSource: `${origin} Confluence`,\n      chunkSource: generateChunkSource(\n        {\n          doc,\n          baseUrl: origin,\n          spaceKey,\n          accessToken,\n          username,\n          cloud,\n          bypassSSL,\n        },\n        response.locals.encryptionWorker\n      ),\n      published: new Date().toLocaleString(),\n      wordCount: doc.pageContent.split(\" \").length,\n      pageContent: doc.pageContent,\n      token_count_estimate: tokenizeString(doc.pageContent),\n    };\n\n    console.log(\n      `[Confluence Loader]: Saving ${doc.metadata.title} to ${outFolder}`\n    );\n\n    const fileName = sanitizeFileName(\n      `${slugify(doc.metadata.title)}-${data.id}`\n    );\n    writeToServerDocuments({\n      data,\n      filename: fileName,\n      destinationOverride: outFolderPath,\n    });\n  });\n\n  return {\n    success: true,\n    reason: null,\n    data: {\n      spaceKey,\n      destination: outFolder,\n    },\n  };\n}\n\n/**\n * Gets the page content from a specific Confluence page, not all pages in a workspace.\n * @returns\n */\nasync function fetchConfluencePage({\n  pageUrl,\n  baseUrl,\n  spaceKey,\n  username,\n  accessToken,\n  cloud = true,\n  bypassSSL = false,\n}) {\n  if (!pageUrl || !baseUrl || !spaceKey || !username || !accessToken) {\n    return {\n      success: false,\n      content: null,\n      reason:\n        \"You need either a username and access token, or a personal access token (PAT), to use the Confluence connector.\",\n    };\n  }\n\n  if (!validBaseUrl(baseUrl)) {\n    return {\n      success: false,\n      content: null,\n      reason: \"Provided base URL is not a valid URL.\",\n    };\n  }\n\n  if (!spaceKey) {\n    return {\n      success: false,\n      content: null,\n      reason: \"You need to provide a Confluence space key.\",\n    };\n  }\n\n  console.log(`-- Working Confluence Page ${pageUrl} --`);\n  const loader = new ConfluencePagesLoader({\n    baseUrl, // Should be the origin of the baseUrl\n    spaceKey,\n    username,\n    accessToken,\n    cloud,\n    bypassSSL,\n  });\n\n  const { docs, error } = await loader\n    .load()\n    .then((docs) => {\n      return { docs, error: null };\n    })\n    .catch((e) => {\n      return {\n        docs: [],\n        error: e.message?.split(\"Error:\")?.[1] || e.message,\n      };\n    });\n\n  if (!docs.length || !!error) {\n    return {\n      success: false,\n      reason: error ?? \"No pages found for that Confluence space.\",\n      content: null,\n    };\n  }\n\n  const targetDocument = docs.find(\n    (doc) => doc.pageContent && doc.metadata.url === pageUrl\n  );\n  if (!targetDocument) {\n    return {\n      success: false,\n      reason: \"Target page could not be found in Confluence space.\",\n      content: null,\n    };\n  }\n\n  return {\n    success: true,\n    reason: null,\n    content: targetDocument.pageContent,\n  };\n}\n\n/**\n * Validates if the provided baseUrl is a valid URL at all.\n * @param {string} baseUrl\n * @returns {boolean}\n */\nfunction validBaseUrl(baseUrl) {\n  try {\n    new URL(baseUrl);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n/**\n * Generate the full chunkSource for a specific Confluence page so that we can resync it later.\n * This data is encrypted into a single `payload` query param so we can replay credentials later\n * since this was encrypted with the systems persistent password and salt.\n * @param {object} chunkSourceInformation\n * @param {import(\"../../EncryptionWorker\").EncryptionWorker} encryptionWorker\n * @returns {string}\n */\nfunction generateChunkSource(\n  { doc, baseUrl, spaceKey, accessToken, username, cloud, bypassSSL },\n  encryptionWorker\n) {\n  const payload = {\n    baseUrl,\n    spaceKey,\n    token: accessToken,\n    username,\n    cloud,\n    bypassSSL,\n  };\n  return `confluence://${doc.metadata.url}?payload=${encryptionWorker.encrypt(\n    JSON.stringify(payload)\n  )}`;\n}\n\nmodule.exports = {\n  loadConfluence,\n  fetchConfluencePage,\n};\n"
  },
  {
    "path": "collector/utils/extensions/DrupalWiki/DrupalWiki/index.js",
    "content": "/**\n * Copyright 2024\n *\n * Authors:\n *  - Eugen Mayer (KontextWork)\n */\n\nconst { htmlToText } = require(\"html-to-text\");\nconst { tokenizeString } = require(\"../../../tokenizer\");\nconst {\n  sanitizeFileName,\n  writeToServerDocuments,\n  documentsFolder,\n  normalizePath,\n  isWithin,\n} = require(\"../../../files\");\nconst { default: slugify } = require(\"slugify\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { processSingleFile } = require(\"../../../../processSingleFile\");\nconst {\n  WATCH_DIRECTORY,\n  SUPPORTED_FILETYPE_CONVERTERS,\n} = require(\"../../../constants\");\n\nclass Page {\n  /**\n   *\n   * @param {number }id\n   * @param {string }title\n   * @param {string} created\n   * @param {string} type\n   * @param {string} processedBody\n   * @param {string} url\n   * @param {number} spaceId\n   */\n  constructor({ id, title, created, type, processedBody, url, spaceId }) {\n    this.id = id;\n    this.title = title;\n    this.url = url;\n    this.created = created;\n    this.type = type;\n    this.processedBody = processedBody;\n    this.spaceId = spaceId;\n  }\n}\n\nclass DrupalWiki {\n  /**\n   *\n   * @param baseUrl\n   * @param spaceId\n   * @param accessToken\n   */\n  constructor({ baseUrl, accessToken }) {\n    this.baseUrl = baseUrl;\n    this.accessToken = accessToken;\n    this.storagePath = this.#prepareStoragePath(baseUrl);\n  }\n\n  /**\n   * Load all pages for the given space, fetching storing each page one by one\n   * to minimize the memory usage\n   *\n   * @param {number} spaceId\n   * @param {import(\"../../EncryptionWorker\").EncryptionWorker} encryptionWorker\n   * @returns {Promise<void>}\n   */\n  async loadAndStoreAllPagesForSpace(spaceId, encryptionWorker) {\n    const pageIndex = await this.#getPageIndexForSpace(spaceId);\n    for (const pageId of pageIndex) {\n      try {\n        const page = await this.loadPage(pageId);\n\n        // Pages with an empty body will lead to embedding issues / exceptions\n        if (page.processedBody.trim() !== \"\") {\n          this.#storePage(page, encryptionWorker);\n          await this.#downloadAndProcessAttachments(page.id);\n        } else {\n          console.log(`Skipping page (${page.id}) since it has no content`);\n        }\n      } catch (e) {\n        console.error(\n          `Could not process DrupalWiki page ${pageId} (skipping and continuing): `\n        );\n        console.error(e);\n      }\n    }\n  }\n\n  /**\n   * @param {number} pageId\n   * @returns {Promise<Page>}\n   */\n  async loadPage(pageId) {\n    return this.#fetchPage(pageId);\n  }\n\n  /**\n   * Fetches the page ids for the configured space\n   * @param {number} spaceId\n   * @returns{Promise<number[]>} array of pageIds\n   */\n  async #getPageIndexForSpace(spaceId) {\n    // errors on fetching the pageIndex is fatal, no error handling\n    let hasNext = true;\n    let pageIds = [];\n    let pageNr = 0;\n    do {\n      let { isLast, pageIdsForPage } = await this.#getPagesForSpacePaginated(\n        spaceId,\n        pageNr\n      );\n      hasNext = !isLast;\n      pageNr++;\n      if (pageIdsForPage.length) {\n        pageIds = pageIds.concat(pageIdsForPage);\n      }\n    } while (hasNext);\n\n    return pageIds;\n  }\n\n  /**\n   *\n   * @param {number} pageNr\n   * @param {number} spaceId\n   * @returns {Promise<{isLast,pageIds}>}\n   */\n  async #getPagesForSpacePaginated(spaceId, pageNr) {\n    /*\n     * {\n     *   content: Page[],\n     *   last: boolean,\n     *   pageable: {\n     *     pageNumber: number\n     *   }\n     * }\n     */\n    const data = await this._doFetch(\n      `${this.baseUrl}/api/rest/scope/api/page?size=100&space=${spaceId}&page=${pageNr}`\n    );\n\n    const pageIds = data.content.map((page) => {\n      return Number(page.id);\n    });\n\n    return {\n      isLast: data.last,\n      pageIdsForPage: pageIds,\n    };\n  }\n\n  /**\n   * @param pageId\n   * @returns {Promise<Page>}\n   */\n  async #fetchPage(pageId) {\n    const data = await this._doFetch(\n      `${this.baseUrl}/api/rest/scope/api/page/${pageId}`\n    );\n    const url = `${this.baseUrl}/node/${data.id}`;\n    return new Page({\n      id: data.id,\n      title: data.title,\n      created: data.lastModified,\n      type: data.type,\n      processedBody: this.#processPageBody({\n        body: data.body,\n        title: data.title,\n        lastModified: data.lastModified,\n        url: url,\n      }),\n      url: url,\n    });\n  }\n\n  /**\n   * @param {Page} page\n   * @param {import(\"../../EncryptionWorker\").EncryptionWorker} encryptionWorker\n   */\n  #storePage(page, encryptionWorker) {\n    const { hostname } = new URL(this.baseUrl);\n\n    // This UUID will ensure that re-importing the same page without any changes will not\n    // show up (deduplication).\n    const targetUUID = `${hostname}.${page.spaceId}.${page.id}.${page.created}`;\n    const wordCount = page.processedBody.split(\" \").length;\n    const data = {\n      id: targetUUID,\n      url: `drupalwiki://${page.url}`,\n      title: page.title,\n      docAuthor: this.baseUrl,\n      description: page.title,\n      docSource: `${this.baseUrl} DrupalWiki`,\n      chunkSource: this.#generateChunkSource(page.id, encryptionWorker),\n      published: new Date().toLocaleString(),\n      wordCount: wordCount,\n      pageContent: page.processedBody,\n      token_count_estimate: tokenizeString(page.processedBody),\n    };\n\n    const fileName = sanitizeFileName(`${slugify(page.title)}-${data.id}`);\n    console.log(\n      `[DrupalWiki Loader]: Saving page '${page.title}' (${page.id}) to '${this.storagePath}/${fileName}'`\n    );\n    writeToServerDocuments({\n      data,\n      filename: fileName,\n      destinationOverride: this.storagePath,\n    });\n  }\n\n  /**\n   * Generate the full chunkSource for a specific Confluence page so that we can resync it later.\n   * This data is encrypted into a single `payload` query param so we can replay credentials later\n   * since this was encrypted with the systems persistent password and salt.\n   * @param {number} pageId\n   * @param {import(\"../../EncryptionWorker\").EncryptionWorker} encryptionWorker\n   * @returns {string}\n   */\n  #generateChunkSource(pageId, encryptionWorker) {\n    const payload = {\n      baseUrl: this.baseUrl,\n      pageId: pageId,\n      accessToken: this.accessToken,\n    };\n    return `drupalwiki://${\n      this.baseUrl\n    }/node/${pageId}?payload=${encryptionWorker.encrypt(\n      JSON.stringify(payload)\n    )}`;\n  }\n\n  async _doFetch(url) {\n    const response = await fetch(url, {\n      headers: this.#getHeaders(),\n    });\n    if (!response.ok) {\n      throw new Error(`Failed to fetch ${url}: ${response.status}`);\n    }\n    return response.json();\n  }\n\n  #getHeaders() {\n    return {\n      \"Content-Type\": \"application/json\",\n      Accept: \"application/json\",\n      Authorization: `Bearer ${this.accessToken}`,\n    };\n  }\n\n  #prepareStoragePath(baseUrl) {\n    const { hostname } = new URL(baseUrl);\n    const subFolder = slugify(`drupalwiki-${hostname}`).toLowerCase();\n    const outFolder = path.resolve(documentsFolder, subFolder);\n    if (!fs.existsSync(outFolder)) fs.mkdirSync(outFolder, { recursive: true });\n    return outFolder;\n  }\n\n  /**\n   * @param {string} body\n   * @param {string} url\n   * @param {string} title\n   * @param {string} lastModified\n   * @returns {string}\n   * @private\n   */\n  #processPageBody({ body, title }) {\n    const textContent = body.trim() !== \"\" ? body : title;\n\n    const plainTextContent = htmlToText(textContent, {\n      wordwrap: false,\n      preserveNewlines: true,\n      selectors: [\n        {\n          selector: \"table\",\n          format: \"dataTable\",\n          options: {\n            colSpacing: 3,\n            rowSpacing: 1,\n            uppercaseHeaderCells: true,\n            maxColumnWidth: Infinity,\n          },\n        },\n      ],\n    });\n\n    const plainBody = plainTextContent.replace(/\\n{3,}/g, \"\\n\\n\");\n    return plainBody;\n  }\n\n  async #downloadAndProcessAttachments(pageId) {\n    try {\n      const data = await this._doFetch(\n        `${this.baseUrl}/api/rest/scope/api/attachment?pageId=${pageId}&size=2000`\n      );\n\n      const extensionsList = Object.keys(SUPPORTED_FILETYPE_CONVERTERS);\n      for (const attachment of data.content || data) {\n        const { fileName, id: attachId } = attachment;\n        const lowerName = fileName.toLowerCase();\n        if (!extensionsList.some((ext) => lowerName.endsWith(ext))) {\n          continue;\n        }\n\n        const downloadUrl = `${this.baseUrl}/api/rest/scope/api/attachment/${attachId}/download`;\n        const attachmentResponse = await fetch(downloadUrl, {\n          headers: this.#getHeaders(),\n        });\n        if (!attachmentResponse.ok) {\n          console.log(`Skipping attachment: ${fileName} - Download failed`);\n          continue;\n        }\n\n        const buffer = await attachmentResponse.arrayBuffer();\n        const localFilePath = normalizePath(\n          sanitizeFileName(path.resolve(WATCH_DIRECTORY, fileName))\n        );\n        if (!isWithin(path.resolve(WATCH_DIRECTORY), localFilePath)) {\n          console.error(\n            `[DrupalWiki Loader]: File name ${localFilePath} is not within the storage path ${path.resolve(\n              WATCH_DIRECTORY\n            )}`\n          );\n          continue;\n        }\n\n        require(\"fs\").writeFileSync(localFilePath, Buffer.from(buffer));\n        await processSingleFile(localFilePath);\n      }\n    } catch (err) {\n      console.error(`Fetching/processing attachments failed:`, err);\n    }\n  }\n}\n\nmodule.exports = { DrupalWiki };\n"
  },
  {
    "path": "collector/utils/extensions/DrupalWiki/index.js",
    "content": "/**\n * Copyright 2024\n *\n * Authors:\n *  - Eugen Mayer (KontextWork)\n */\n\nconst { DrupalWiki } = require(\"./DrupalWiki\");\nconst { validBaseUrl } = require(\"../../../utils/http\");\n\nasync function loadAndStoreSpaces(\n  { baseUrl = null, spaceIds = null, accessToken = null },\n  response\n) {\n  if (!baseUrl) {\n    return {\n      success: false,\n      reason:\n        \"Please provide your baseUrl like https://mywiki.drupal-wiki.net.\",\n    };\n  } else if (!validBaseUrl(baseUrl)) {\n    return {\n      success: false,\n      reason: \"Provided base URL is not a valid URL.\",\n    };\n  }\n\n  if (!spaceIds) {\n    return {\n      success: false,\n      reason:\n        \"Please provide a list of spaceIds like 21,56,67 you want to extract\",\n    };\n  }\n\n  if (!accessToken) {\n    return {\n      success: false,\n      reason: \"Please provide a REST API-Token.\",\n    };\n  }\n\n  console.log(`-- Working Drupal Wiki ${baseUrl} for spaceIds: ${spaceIds} --`);\n  const drupalWiki = new DrupalWiki({ baseUrl, accessToken });\n\n  const encryptionWorker = response.locals.encryptionWorker;\n  const spaceIdsArr = spaceIds.split(\",\").map((idStr) => {\n    return Number(idStr.trim());\n  });\n\n  for (const spaceId of spaceIdsArr) {\n    try {\n      await drupalWiki.loadAndStoreAllPagesForSpace(spaceId, encryptionWorker);\n      console.log(`--- Finished space ${spaceId} ---`);\n    } catch (e) {\n      console.error(e);\n      return {\n        success: false,\n        reason: e.message,\n        data: {},\n      };\n    }\n  }\n  console.log(`-- Finished all spaces--`);\n\n  return {\n    success: true,\n    reason: null,\n    data: {\n      spaceIds,\n      destination: drupalWiki.storagePath,\n    },\n  };\n}\n\n/**\n * Gets the page content from a specific Confluence page, not all pages in a workspace.\n * @returns\n */\nasync function loadPage({ baseUrl, pageId, accessToken }) {\n  console.log(`-- Working Drupal Wiki Page ${pageId} of ${baseUrl} --`);\n  const drupalWiki = new DrupalWiki({ baseUrl, accessToken });\n  try {\n    const page = await drupalWiki.loadPage(pageId);\n    return {\n      success: true,\n      reason: null,\n      content: page.processedBody,\n    };\n  } catch {\n    return {\n      success: false,\n      reason: `Failed (re)-fetching DrupalWiki page ${pageId} form ${baseUrl}}`,\n      content: null,\n    };\n  }\n}\n\nmodule.exports = {\n  loadAndStoreSpaces,\n  loadPage,\n};\n"
  },
  {
    "path": "collector/utils/extensions/ObsidianVault/index.js",
    "content": "const { v4 } = require(\"uuid\");\nconst { default: slugify } = require(\"slugify\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst {\n  writeToServerDocuments,\n  sanitizeFileName,\n  documentsFolder,\n} = require(\"../../files\");\n\nfunction parseObsidianVaultPath(files = []) {\n  const possiblePaths = new Set();\n  files.forEach(\n    (file) => file?.path && possiblePaths.add(file.path.split(\"/\")[0])\n  );\n\n  switch (possiblePaths.size) {\n    case 0:\n      return null;\n    case 1:\n      // The user specified a vault properly - so all files are in the same folder.\n      return possiblePaths.values().next().value;\n    default:\n      return null;\n  }\n}\n\nasync function loadObsidianVault({ files = [] }) {\n  if (!files || files?.length === 0)\n    return { success: false, error: \"No files provided\" };\n  const vaultName = parseObsidianVaultPath(files);\n  const folderUUId = v4().slice(0, 4);\n  const outFolder = vaultName\n    ? slugify(`obsidian-vault-${vaultName}-${folderUUId}`).toLowerCase()\n    : slugify(`obsidian-${folderUUId}`).toLowerCase();\n  const outFolderPath = path.resolve(documentsFolder, outFolder);\n  if (!fs.existsSync(outFolderPath))\n    fs.mkdirSync(outFolderPath, { recursive: true });\n\n  console.log(\n    `Processing ${files.length} files from Obsidian Vault ${\n      vaultName ? `\"${vaultName}\"` : \"\"\n    }`\n  );\n  const results = [];\n  for (const file of files) {\n    try {\n      const fullPageContent = file?.content;\n      // If the file has no content or is just whitespace, skip it.\n      if (!fullPageContent || fullPageContent.trim() === \"\") continue;\n\n      const data = {\n        id: v4(),\n        url: `obsidian://${file.path}`,\n        title: file.name,\n        docAuthor: \"Obsidian Vault\",\n        description: file.name,\n        docSource: \"Obsidian Vault\",\n        chunkSource: `obsidian://${file.path}`,\n        published: new Date().toLocaleString(),\n        wordCount: fullPageContent.split(\" \").length,\n        pageContent: fullPageContent,\n        token_count_estimate: fullPageContent.length / 4, // rough estimate\n      };\n\n      const targetFileName = sanitizeFileName(\n        `${slugify(file.name)}-${data.id}`\n      );\n      writeToServerDocuments({\n        data,\n        filename: targetFileName,\n        destinationOverride: outFolderPath,\n      });\n      results.push({ file: file.path, status: \"success\" });\n    } catch (e) {\n      console.error(`Failed to process ${file.path}:`, e);\n      results.push({ file: file.path, status: \"failed\", reason: e.message });\n    }\n  }\n\n  return {\n    success: true,\n    data: {\n      processed: results.filter((r) => r.status === \"success\").length,\n      failed: results.filter((r) => r.status === \"failed\").length,\n      total: files.length,\n      results,\n      destination: path.basename(outFolderPath),\n    },\n  };\n}\n\nmodule.exports = {\n  loadObsidianVault,\n};\n"
  },
  {
    "path": "collector/utils/extensions/PaperlessNgx/PaperlessNgxLoader/index.js",
    "content": "const { htmlToText } = require(\"html-to-text\");\nconst pdf = require(\"pdf-parse\");\n\nclass PaperlessNgxLoader {\n  constructor({ baseUrl, apiToken }) {\n    this.baseUrl = new URL(baseUrl).origin;\n    this.apiToken = apiToken;\n    this.baseHeaders = {\n      Authorization: `Token ${this.apiToken}`,\n    };\n  }\n\n  async load() {\n    try {\n      const documents = await this.fetchAllDocuments();\n      return documents.map((doc) => this.createDocumentFromPage(doc));\n    } catch (error) {\n      console.error(\"Error:\", error);\n      throw error;\n    }\n  }\n\n  /**\n   * Fetches all documents from Paperless-ngx\n   * @returns {Promise<{{[key: string]: any, content: string}[]}>} The documents with their content\n   */\n  async fetchAllDocuments() {\n    try {\n      const documents = [];\n      let nextUrl = `${this.baseUrl}/api/documents/`;\n      let page = 1;\n\n      while (nextUrl) {\n        console.log(`Fetching documents page ${page} from Paperless-ngx`);\n        try {\n          const data = await fetch(nextUrl, {\n            headers: {\n              \"Content-Type\": \"application/json\",\n              ...this.baseHeaders,\n            },\n          }).then((res) => {\n            if (!res.ok)\n              throw new Error(\n                `Failed to fetch documents from Paperless-ngx: ${res.status}`\n              );\n            return res.json();\n          });\n\n          const validResults = data.results.filter((doc) => doc?.id);\n          if (!validResults.length) break;\n\n          documents.push(...validResults);\n\n          if (data.next === nextUrl) break;\n          nextUrl = data.next || null;\n          page++;\n        } catch (error) {\n          console.error(\n            `Error fetching page ${page} from Paperless-ngx:`,\n            error\n          );\n          break;\n        }\n      }\n\n      console.log(\n        `Fetched ${documents.length} documents from Paperless-ngx (Pages: ${\n          page - 1\n        })`\n      );\n\n      const documentsWithContent = await Promise.all(\n        documents.map(async (doc) => {\n          const content = await this.fetchDocumentContent(doc.id);\n          return { ...doc, content };\n        })\n      );\n\n      return documentsWithContent.filter((doc) => !!doc.content);\n    } catch (error) {\n      throw new Error(\n        `Failed to fetch documents from Paperless-ngx: ${error.message}`\n      );\n    }\n  }\n\n  /**\n   * Fetches the content of a document from Paperless-ngx\n   * @param {string} documentId - The ID of the document to fetch\n   * @returns {Promise<string>} The content of the document\n   */\n  async fetchDocumentContent(documentId) {\n    try {\n      const response = await fetch(\n        `${this.baseUrl}/api/documents/${documentId}/download/`,\n        {\n          headers: this.baseHeaders,\n        }\n      );\n\n      if (!response.ok)\n        throw new Error(`Failed to fetch document content: ${response.status}`);\n\n      const contentType = response.headers.get(\"content-type\");\n      switch (contentType) {\n        case \"text/plain\":\n          return await response.text();\n        case \"application/pdf\":\n          const buffer = await response.arrayBuffer();\n          return await this.parsePdfContent(buffer);\n        default:\n          return await response.text();\n      }\n    } catch (error) {\n      console.error(\n        `Failed to fetch content for document ${documentId}:`,\n        error\n      );\n      return \"\";\n    }\n  }\n\n  async parsePdfContent(buffer) {\n    try {\n      const data = await pdf(Buffer.from(buffer));\n      return data.text;\n    } catch (error) {\n      console.error(\"Failed to parse PDF content:\", error);\n      return \"\";\n    }\n  }\n\n  createDocumentFromPage(doc) {\n    const content = doc.content || \"\";\n    const plainTextContent = htmlToText(content, {\n      wordwrap: false,\n      preserveNewlines: true,\n    });\n\n    return {\n      pageContent: plainTextContent,\n      metadata: {\n        id: doc.id,\n        title: doc.original_file_name,\n        created: doc.created,\n        modified: doc.modified,\n        added: doc.added,\n        tags: doc.tags,\n        correspondent: doc.correspondent,\n        documentType: doc.document_type,\n        url: `${this.baseUrl}/documents/${doc.id}`,\n      },\n    };\n  }\n}\n\nmodule.exports = PaperlessNgxLoader;\n"
  },
  {
    "path": "collector/utils/extensions/PaperlessNgx/index.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { default: slugify } = require(\"slugify\");\nconst { v4 } = require(\"uuid\");\nconst {\n  writeToServerDocuments,\n  sanitizeFileName,\n  documentsFolder,\n} = require(\"../../files\");\nconst { tokenizeString } = require(\"../../tokenizer\");\nconst { validBaseUrl } = require(\"../../http\");\nconst PaperlessNgxLoader = require(\"./PaperlessNgxLoader\");\n\n/**\n * Load documents from a Paperless-ngx instance\n * @param {object} args - forwarded request body params\n * @param {import(\"../../../middleware/setDataSigner\").ResponseWithSigner} response - Express response object with encryptionWorker\n * @returns\n */\nasync function loadPaperlessNgx({ baseUrl = null, apiToken = null }, response) {\n  if (!baseUrl || !validBaseUrl(baseUrl)) {\n    return {\n      success: false,\n      reason: \"Provided base URL is not a valid URL.\",\n    };\n  }\n\n  if (!apiToken) {\n    return {\n      success: false,\n      reason:\n        \"You need to provide an API token to use the Paperless-ngx connector.\",\n    };\n  }\n\n  const { origin, hostname } = new URL(baseUrl);\n  console.log(`-- Working Paperless-ngx ${origin} --`);\n  const loader = new PaperlessNgxLoader({\n    baseUrl: origin,\n    apiToken,\n  });\n\n  const { docs, error } = await loader\n    .load()\n    .then((docs) => ({ docs, error: null }))\n    .catch((e) => ({\n      docs: [],\n      error: e.message?.split(\"Error:\")?.[1] || e.message,\n    }));\n\n  if (!docs.length || !!error) {\n    return {\n      success: false,\n      reason:\n        error ?? \"No parseable documents found in that Paperless-ngx instance.\",\n      data: null,\n    };\n  }\n\n  const outFolder = slugify(\n    `paperless-${hostname}-${v4().slice(0, 4)}`\n  ).toLowerCase();\n  const outFolderPath = path.resolve(documentsFolder, outFolder);\n  if (!fs.existsSync(outFolderPath))\n    fs.mkdirSync(outFolderPath, { recursive: true });\n\n  docs.forEach((doc) => {\n    if (!doc.pageContent) return;\n\n    const data = {\n      id: v4(),\n      url: doc.metadata.url,\n      title: doc.metadata.title,\n      docAuthor: doc.metadata.correspondent || \"Unknown\",\n      description: `A document from the Paperless-ngx instance at ${origin}`,\n      docSource: `paperless-ngx`,\n      chunkSource: generateChunkSource(\n        { doc, baseUrl: origin, apiToken },\n        response.locals.encryptionWorker\n      ),\n      published: doc.metadata.created,\n      wordCount: doc.pageContent.split(\" \").length,\n      pageContent: doc.pageContent,\n      token_count_estimate: tokenizeString(doc.pageContent),\n    };\n\n    console.log(\n      `[Paperless-ngx Loader]: Saving ${doc.metadata.title} to ${outFolder}`\n    );\n    const fileName = sanitizeFileName(\n      `${slugify(doc.metadata.title)}-${data.id}`\n    );\n    writeToServerDocuments({\n      data,\n      filename: fileName,\n      destinationOverride: outFolderPath,\n    });\n  });\n\n  return {\n    success: true,\n    reason: null,\n    data: {\n      files: docs.length,\n      destination: outFolder,\n    },\n  };\n}\n\n/**\n * Generate the full chunkSource for a specific Paperless-ngx document so that we can resync it later.\n * @param {object} chunkSourceInformation\n * @param {import(\"../../EncryptionWorker\").EncryptionWorker} encryptionWorker\n * @returns {string}\n */\nfunction generateChunkSource({ doc, baseUrl, apiToken }, encryptionWorker) {\n  const payload = {\n    baseUrl,\n    token: apiToken,\n  };\n  return `paperless-ngx://${doc.metadata.id}?payload=${encryptionWorker.encrypt(\n    JSON.stringify(payload)\n  )}`;\n}\n\nmodule.exports = {\n  loadPaperlessNgx,\n};\n"
  },
  {
    "path": "collector/utils/extensions/RepoLoader/GithubRepo/RepoLoader/index.js",
    "content": "/**\n * @typedef {Object} RepoLoaderArgs\n * @property {string} repo - The GitHub repository URL.\n * @property {string} [branch] - The branch to load from (optional).\n * @property {string} [accessToken] - GitHub access token for authentication (optional).\n * @property {string[]} [ignorePaths] - Array of paths to ignore when loading (optional).\n */\n\n/**\n * @class\n * @classdesc Loads and manages GitHub repository content.\n */\nclass GitHubRepoLoader {\n  /**\n   * Creates an instance of RepoLoader.\n   * @param {RepoLoaderArgs} [args] - The configuration options.\n   * @returns {GitHubRepoLoader}\n   */\n  constructor(args = {}) {\n    this.ready = false;\n    this.repo = this.#processRepoUrl(args?.repo);\n    this.branch = args?.branch;\n    this.accessToken = args?.accessToken || null;\n    this.ignorePaths = args?.ignorePaths || [];\n\n    this.author = null;\n    this.project = null;\n    this.branches = [];\n  }\n\n  /**\n   * Processes a repository URL to ensure it is in the correct format\n   * - remove the .git suffix if present\n   * - ensure the url is valid\n   * @param {string} repoUrl - The repository URL to process.\n   * @returns {string|null} The processed repository URL, or null if the URL is invalid.\n   */\n  #processRepoUrl(repoUrl) {\n    if (!repoUrl) return repoUrl;\n    try {\n      const url = new URL(repoUrl);\n      if (url.pathname.endsWith(\".git\"))\n        url.pathname = url.pathname.slice(0, -4);\n      return url.toString();\n    } catch (e) {\n      console.error(\n        `[GitHub Loader]: Error processing repository URL ${this.repo}: ${e.message}`\n      );\n      return repoUrl;\n    }\n  }\n\n  /**\n   * Validates the GitHub URL format.\n   * - ensure the url is valid\n   * - ensure the hostname is github.com\n   * - ensure the pathname is in the format of github.com/{author}/{project}\n   * - sets the author and project properties of class instance\n   * @returns {boolean} True if the URL is valid, false otherwise.\n   */\n  #validGithubUrl() {\n    try {\n      const url = new URL(this.repo);\n\n      // Not a github url at all.\n      if (url.hostname !== \"github.com\") {\n        console.log(\n          `[GitHub Loader]: Invalid GitHub URL provided! Hostname must be 'github.com'. Got ${url.hostname}`\n        );\n        return false;\n      }\n\n      // Assume the url is in the format of github.com/{author}/{project}\n      // Remove the first slash from the pathname so we can split it properly.\n      const [author, project, ..._rest] = url.pathname.slice(1).split(\"/\");\n      if (!author || !project) {\n        console.log(\n          `[GitHub Loader]: Invalid GitHub URL provided! URL must be in the format of 'github.com/{author}/{project}'. Got ${url.pathname}`\n        );\n        return false;\n      }\n\n      this.author = author;\n      this.project = project;\n      return true;\n    } catch (e) {\n      console.log(\n        `[GitHub Loader]: Invalid GitHub URL provided! Error: ${e.message}`\n      );\n      return false;\n    }\n  }\n\n  // Ensure the branch provided actually exists\n  // and if it does not or has not been set auto-assign to primary branch.\n  async #validBranch() {\n    await this.getRepoBranches();\n    if (!!this.branch && this.branches.includes(this.branch)) return;\n\n    console.log(\n      \"[GitHub Loader]: Branch not set! Auto-assigning to a default branch.\"\n    );\n    this.branch = this.branches.includes(\"main\") ? \"main\" : \"master\";\n    console.log(`[GitHub Loader]: Branch auto-assigned to ${this.branch}.`);\n    return;\n  }\n\n  async #validateAccessToken() {\n    if (!this.accessToken) return;\n    const valid = await fetch(\"https://api.github.com/octocat\", {\n      method: \"GET\",\n      headers: {\n        Authorization: `Bearer ${this.accessToken}`,\n        \"X-GitHub-Api-Version\": \"2022-11-28\",\n      },\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(res.statusText);\n        return res.ok;\n      })\n      .catch((e) => {\n        console.error(\n          \"Invalid GitHub Access Token provided! Access token will not be used\",\n          e.message\n        );\n        return false;\n      });\n\n    if (!valid) this.accessToken = null;\n    return;\n  }\n\n  /**\n   * Initializes the RepoLoader instance.\n   * @returns {Promise<RepoLoader>} The initialized RepoLoader instance.\n   */\n  async init() {\n    if (!this.#validGithubUrl()) return;\n    await this.#validBranch();\n    await this.#validateAccessToken();\n    this.ready = true;\n    return this;\n  }\n\n  /**\n   * Recursively loads the repository content.\n   * @returns {Promise<Array<Object>>} An array of loaded documents.\n   * @throws {Error} If the RepoLoader is not in a ready state.\n   */\n  async recursiveLoader() {\n    if (!this.ready) throw new Error(\"[GitHub Loader]: not in ready state!\");\n    const {\n      GithubRepoLoader: LCGithubLoader,\n    } = require(\"@langchain/community/document_loaders/web/github\");\n\n    if (this.accessToken)\n      console.log(\n        `[GitHub Loader]: Access token set! Recursive loading enabled!`\n      );\n\n    const loader = new LCGithubLoader(this.repo, {\n      branch: this.branch,\n      recursive: !!this.accessToken, // Recursive will hit rate limits.\n      maxConcurrency: 5,\n      unknown: \"warn\",\n      accessToken: this.accessToken,\n      ignorePaths: this.ignorePaths,\n      verbose: true,\n    });\n\n    const docs = await loader.load();\n    return docs;\n  }\n\n  // Sort branches to always show either main or master at the top of the result.\n  #branchPrefSort(branches = []) {\n    const preferredSort = [\"main\", \"master\"];\n    return branches.reduce((acc, branch) => {\n      if (preferredSort.includes(branch)) return [branch, ...acc];\n      return [...acc, branch];\n    }, []);\n  }\n\n  /**\n   * Retrieves all branches for the repository.\n   * @returns {Promise<string[]>} An array of branch names.\n   */\n  async getRepoBranches() {\n    if (!this.#validGithubUrl() || !this.author || !this.project) return [];\n    await this.#validateAccessToken(); // Ensure API access token is valid for pre-flight\n\n    let page = 0;\n    let polling = true;\n    const branches = [];\n\n    while (polling) {\n      console.log(`Fetching page ${page} of branches for ${this.project}`);\n      await fetch(\n        `https://api.github.com/repos/${this.author}/${this.project}/branches?per_page=100&page=${page}`,\n        {\n          method: \"GET\",\n          headers: {\n            ...(this.accessToken\n              ? { Authorization: `Bearer ${this.accessToken}` }\n              : {}),\n            \"X-GitHub-Api-Version\": \"2022-11-28\",\n          },\n        }\n      )\n        .then((res) => {\n          if (res.ok) return res.json();\n          throw new Error(`Invalid request to Github API: ${res.statusText}`);\n        })\n        .then((branchObjects) => {\n          polling = branchObjects.length > 0;\n          branches.push(branchObjects.map((branch) => branch.name));\n          page++;\n        })\n        .catch((err) => {\n          polling = false;\n          console.log(`RepoLoader.branches`, err);\n        });\n    }\n\n    this.branches = [...new Set(branches.flat())];\n    return this.#branchPrefSort(this.branches);\n  }\n\n  /**\n   * Fetches the content of a single file from the repository.\n   * @param {string} sourceFilePath - The path to the file in the repository.\n   * @returns {Promise<string|null>} The content of the file, or null if fetching fails.\n   */\n  async fetchSingleFile(sourceFilePath) {\n    try {\n      return fetch(\n        `https://api.github.com/repos/${this.author}/${this.project}/contents/${sourceFilePath}?ref=${this.branch}`,\n        {\n          method: \"GET\",\n          headers: {\n            Accept: \"application/vnd.github+json\",\n            \"X-GitHub-Api-Version\": \"2022-11-28\",\n            ...(!!this.accessToken\n              ? { Authorization: `Bearer ${this.accessToken}` }\n              : {}),\n          },\n        }\n      )\n        .then((res) => {\n          if (res.ok) return res.json();\n          throw new Error(`Failed to fetch from Github API: ${res.statusText}`);\n        })\n        .then((json) => {\n          if (json.hasOwnProperty(\"status\") || !json.hasOwnProperty(\"content\"))\n            throw new Error(json?.message || \"missing content\");\n          return atob(json.content);\n        });\n    } catch (e) {\n      console.error(`RepoLoader.fetchSingleFile`, e);\n      return null;\n    }\n  }\n}\n\nmodule.exports = GitHubRepoLoader;\n"
  },
  {
    "path": "collector/utils/extensions/RepoLoader/GithubRepo/index.js",
    "content": "const RepoLoader = require(\"./RepoLoader\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { default: slugify } = require(\"slugify\");\nconst { v4 } = require(\"uuid\");\nconst { writeToServerDocuments } = require(\"../../../files\");\nconst { tokenizeString } = require(\"../../../tokenizer\");\n\n/**\n * Load in a GitHub Repo recursively or just the top level if no PAT is provided\n * @param {object} args - forwarded request body params\n * @param {import(\"../../../middleware/setDataSigner\").ResponseWithSigner} response - Express response object with encryptionWorker\n * @returns\n */\nasync function loadGithubRepo(args, response) {\n  const repo = new RepoLoader(args);\n  await repo.init();\n\n  if (!repo.ready)\n    return {\n      success: false,\n      reason: \"Could not prepare GitHub repo for loading! Check URL\",\n    };\n\n  console.log(\n    `-- Working GitHub ${repo.author}/${repo.project}:${repo.branch} --`\n  );\n  const docs = await repo.recursiveLoader();\n  if (!docs.length) {\n    return {\n      success: false,\n      reason: \"No files were found for those settings.\",\n    };\n  }\n\n  console.log(`[GitHub Loader]: Found ${docs.length} source files. Saving...`);\n  const outFolder = slugify(\n    `${repo.author}-${repo.project}-${repo.branch}-${v4().slice(0, 4)}`\n  ).toLowerCase();\n\n  const outFolderPath =\n    process.env.NODE_ENV === \"development\"\n      ? path.resolve(\n          __dirname,\n          `../../../../../server/storage/documents/${outFolder}`\n        )\n      : path.resolve(process.env.STORAGE_DIR, `documents/${outFolder}`);\n\n  if (!fs.existsSync(outFolderPath))\n    fs.mkdirSync(outFolderPath, { recursive: true });\n\n  for (const doc of docs) {\n    if (!doc.pageContent) continue;\n    const data = {\n      id: v4(),\n      url: \"github://\" + doc.metadata.source,\n      title: doc.metadata.source,\n      docAuthor: repo.author,\n      description: \"No description found.\",\n      docSource: doc.metadata.source,\n      chunkSource: generateChunkSource(\n        repo,\n        doc,\n        response.locals.encryptionWorker\n      ),\n      published: new Date().toLocaleString(),\n      wordCount: doc.pageContent.split(\" \").length,\n      pageContent: doc.pageContent,\n      token_count_estimate: tokenizeString(doc.pageContent),\n    };\n    console.log(\n      `[GitHub Loader]: Saving ${doc.metadata.source} to ${outFolder}`\n    );\n    writeToServerDocuments({\n      data,\n      filename: `${slugify(doc.metadata.source)}-${data.id}`,\n      destinationOverride: outFolderPath,\n    });\n  }\n\n  return {\n    success: true,\n    reason: null,\n    data: {\n      author: repo.author,\n      repo: repo.project,\n      branch: repo.branch,\n      files: docs.length,\n      destination: outFolder,\n    },\n  };\n}\n\n/**\n * Gets the page content from a specific source file in a give GitHub Repo, not all items in a repo.\n * @returns\n */\nasync function fetchGithubFile({\n  repoUrl,\n  branch,\n  accessToken = null,\n  sourceFilePath,\n}) {\n  const repo = new RepoLoader({\n    repo: repoUrl,\n    branch,\n    accessToken,\n  });\n  await repo.init();\n\n  if (!repo.ready)\n    return {\n      success: false,\n      content: null,\n      reason: \"Could not prepare GitHub repo for loading! Check URL or PAT.\",\n    };\n\n  console.log(\n    `-- Working GitHub ${repo.author}/${repo.project}:${repo.branch} file:${sourceFilePath} --`\n  );\n  const fileContent = await repo.fetchSingleFile(sourceFilePath);\n  if (!fileContent) {\n    return {\n      success: false,\n      reason: \"Target file returned a null content response.\",\n      content: null,\n    };\n  }\n\n  return {\n    success: true,\n    reason: null,\n    content: fileContent,\n  };\n}\n\n/**\n * Generate the full chunkSource for a specific file so that we can resync it later.\n * This data is encrypted into a single `payload` query param so we can replay credentials later\n * since this was encrypted with the systems persistent password and salt.\n * @param {RepoLoader} repo\n * @param {import(\"@langchain/core/documents\").Document} doc\n * @param {import(\"../../EncryptionWorker\").EncryptionWorker} encryptionWorker\n * @returns {string}\n */\nfunction generateChunkSource(repo, doc, encryptionWorker) {\n  const payload = {\n    owner: repo.author,\n    project: repo.project,\n    branch: repo.branch,\n    path: doc.metadata.source,\n    pat: !!repo.accessToken ? repo.accessToken : null,\n  };\n  return `github://${repo.repo}?payload=${encryptionWorker.encrypt(\n    JSON.stringify(payload)\n  )}`;\n}\n\nmodule.exports = { loadGithubRepo, fetchGithubFile };\n"
  },
  {
    "path": "collector/utils/extensions/RepoLoader/GitlabRepo/RepoLoader/index.js",
    "content": "const ignore = require(\"ignore\");\nconst MAX_RETRIES = 3;\n\n/**\n * @typedef {Object} RepoLoaderArgs\n * @property {string} repo - The GitLab repository URL.\n * @property {string} [branch] - The branch to load from (optional).\n * @property {string} [accessToken] - GitLab access token for authentication (optional).\n * @property {string[]} [ignorePaths] - Array of paths to ignore when loading (optional).\n * @property {boolean} [fetchIssues] - Should issues be fetched (optional).\n * @property {boolean} [fetchWikis] - Should wiki be fetched (optional).\n */\n\n/**\n * @typedef {Object} FileTreeObject\n * @property {string} id - The file object ID.\n * @property {string} name - name of file.\n * @property {('blob'|'tree')} type - type of file object.\n * @property {string} path - path + name of file.\n * @property {string} mode - Linux permission code.\n */\n\n/**\n * @class\n * @classdesc Loads and manages GitLab repository content.\n */\nclass GitLabRepoLoader {\n  /**\n   * Creates an instance of RepoLoader.\n   * @param {RepoLoaderArgs} [args] - The configuration options.\n   * @returns {GitLabRepoLoader}\n   */\n  constructor(args = {}) {\n    this.ready = false;\n    this.repo = args?.repo;\n    this.branch = args?.branch;\n    this.accessToken = args?.accessToken || null;\n    this.ignorePaths = args?.ignorePaths || [];\n    this.ignoreFilter = ignore().add(this.ignorePaths);\n    this.withIssues = args?.fetchIssues || false;\n    this.withWikis = args?.fetchWikis || false;\n\n    this.projectId = null;\n    this.apiBase = \"https://gitlab.com\";\n    this.author = null;\n    this.project = null;\n    this.branches = [];\n  }\n\n  #wait(ms) {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n  }\n\n  #validGitlabUrl() {\n    const validPatterns = [\n      //eslint-disable-next-line\n      /https:\\/\\/gitlab\\.com\\/(?<author>[^\\/]+)\\/(?<project>.*)/,\n      // This should even match the regular hosted URL, but we may want to know\n      // if this was a hosted GitLab (above) or a self-hosted (below) instance\n      // since the API interface could be different.\n      //eslint-disable-next-line\n      /(http|https):\\/\\/[^\\/]+\\/(?<author>[^\\/]+)\\/(?<project>.*)/,\n    ];\n\n    const match = validPatterns\n      .find((pattern) => this.repo.match(pattern)?.groups)\n      ?.exec(this.repo);\n    if (!match?.groups) return false;\n\n    const { author, project } = match.groups;\n    this.projectId = encodeURIComponent(`${author}/${project}`);\n    this.apiBase = new URL(this.repo).origin;\n    this.author = author;\n    this.project = project;\n    return true;\n  }\n\n  async #validBranch() {\n    await this.getRepoBranches();\n    if (!!this.branch && this.branches.includes(this.branch)) return;\n\n    console.log(\n      \"[Gitlab Loader]: Branch not set! Auto-assigning to a default branch.\"\n    );\n    this.branch = this.branches.includes(\"main\") ? \"main\" : \"master\";\n    console.log(`[Gitlab Loader]: Branch auto-assigned to ${this.branch}.`);\n    return;\n  }\n\n  async #validateAccessToken() {\n    if (!this.accessToken) return;\n    try {\n      await fetch(`${this.apiBase}/api/v4/user`, {\n        method: \"GET\",\n        headers: this.accessToken ? { \"PRIVATE-TOKEN\": this.accessToken } : {},\n      }).then((res) => res.ok);\n    } catch (e) {\n      console.error(\n        \"Invalid Gitlab Access Token provided! Access token will not be used\",\n        e.message\n      );\n      this.accessToken = null;\n    }\n  }\n\n  /**\n   * Initializes the RepoLoader instance.\n   * @returns {Promise<RepoLoader>} The initialized RepoLoader instance.\n   */\n  async init() {\n    if (!this.#validGitlabUrl()) return;\n    await this.#validBranch();\n    await this.#validateAccessToken();\n    this.ready = true;\n    return this;\n  }\n\n  /**\n   * Recursively loads the repository content.\n   * @returns {Promise<Array<Object>>} An array of loaded documents.\n   * @throws {Error} If the RepoLoader is not in a ready state.\n   */\n  async recursiveLoader() {\n    if (!this.ready) throw new Error(\"[Gitlab Loader]: not in ready state!\");\n\n    if (this.accessToken)\n      console.log(\n        `[Gitlab Loader]: Access token set! Recursive loading enabled for ${this.repo}!`\n      );\n\n    const docs = [];\n\n    console.log(`[Gitlab Loader]: Fetching files.`);\n\n    const files = await this.fetchFilesRecursive();\n\n    console.log(`[Gitlab Loader]: Fetched ${files.length} files.`);\n\n    for (const file of files) {\n      if (this.ignoreFilter.ignores(file.path)) continue;\n\n      docs.push({\n        pageContent: file.content,\n        metadata: {\n          source: file.path,\n          url: `${this.repo}/-/blob/${this.branch}/${file.path}`,\n        },\n      });\n    }\n\n    if (this.withIssues) {\n      console.log(`[Gitlab Loader]: Fetching issues.`);\n      const issues = await this.fetchIssues();\n      console.log(\n        `[Gitlab Loader]: Fetched ${issues.length} issues with discussions.`\n      );\n      docs.push(\n        ...issues.map((issue) => ({\n          issue,\n          metadata: {\n            source: `issue-${this.repo}-${issue.iid}`,\n            url: issue.web_url,\n          },\n        }))\n      );\n    }\n\n    if (this.withWikis) {\n      console.log(`[Gitlab Loader]: Fetching wiki.`);\n      const wiki = await this.fetchWiki();\n      console.log(`[Gitlab Loader]: Fetched ${wiki.length} wiki pages.`);\n      docs.push(\n        ...wiki.map((wiki) => ({\n          wiki,\n          metadata: {\n            source: `wiki-${this.repo}-${wiki.slug}`,\n            url: `${this.repo}/-/wikis/${wiki.slug}`,\n          },\n        }))\n      );\n    }\n\n    return docs;\n  }\n\n  #branchPrefSort(branches = []) {\n    const preferredSort = [\"main\", \"master\"];\n    return branches.reduce((acc, branch) => {\n      if (preferredSort.includes(branch)) return [branch, ...acc];\n      return [...acc, branch];\n    }, []);\n  }\n\n  /**\n   * Retrieves all branches for the repository.\n   * @returns {Promise<string[]>} An array of branch names.\n   */\n  async getRepoBranches() {\n    if (!this.#validGitlabUrl() || !this.projectId) return [];\n    await this.#validateAccessToken();\n    this.branches = [];\n\n    const branchesRequestData = {\n      endpoint: `/api/v4/projects/${this.projectId}/repository/branches`,\n    };\n\n    let branchesPage = [];\n    while ((branchesPage = await this.fetchNextPage(branchesRequestData))) {\n      if (!Array.isArray(branchesPage) || !branchesPage?.length) break;\n      this.branches.push(...branchesPage.map((branch) => branch.name));\n    }\n    return this.#branchPrefSort(this.branches);\n  }\n\n  /**\n   * Returns list of all file objects from tree API for GitLab\n   * @returns {Promise<FileTreeObject[]>}\n   */\n  async fetchFilesRecursive() {\n    const files = [];\n    const filesRequestData = {\n      endpoint: `/api/v4/projects/${this.projectId}/repository/tree`,\n      queryParams: {\n        ref: this.branch,\n        recursive: true,\n      },\n    };\n\n    let filesPage = null;\n    let pagePromises = [];\n    while ((filesPage = await this.fetchNextPage(filesRequestData))) {\n      if (!Array.isArray(filesPage) || !filesPage?.length) break;\n      // Fetch all the files that are not ignored in parallel.\n      pagePromises = filesPage\n        .filter((file) => {\n          if (file.type !== \"blob\") return false;\n          return !this.ignoreFilter.ignores(file.path);\n        })\n        .map(async (file) => {\n          const content = await this.fetchSingleFileContents(file.path);\n          if (!content) return null;\n          return {\n            path: file.path,\n            content,\n          };\n        });\n\n      const pageFiles = await Promise.all(pagePromises);\n\n      files.push(...pageFiles.filter((item) => item !== null));\n      console.log(`Fetched ${files.length} files.`);\n    }\n    console.log(`Total files fetched: ${files.length}`);\n    return files;\n  }\n\n  /**\n   * Fetches all issues from the repository.\n   * @returns {Promise<Issue[]>} An array of issue objects.\n   */\n  async fetchIssues() {\n    const issues = [];\n    const issuesRequestData = {\n      endpoint: `/api/v4/projects/${this.projectId}/issues`,\n    };\n\n    let issuesPage = null;\n    let pagePromises = [];\n    while ((issuesPage = await this.fetchNextPage(issuesRequestData))) {\n      if (!Array.isArray(issuesPage) || !issuesPage?.length) break;\n      // Fetch all the issues in parallel.\n      pagePromises = issuesPage.map(async (issue) => {\n        const discussionsRequestData = {\n          endpoint: `/api/v4/projects/${this.projectId}/issues/${issue.iid}/discussions`,\n        };\n        let discussionPage = null;\n        const discussions = [];\n\n        while (\n          (discussionPage = await this.fetchNextPage(discussionsRequestData))\n        ) {\n          if (!Array.isArray(discussionPage) || !discussionPage?.length) break;\n          discussions.push(\n            ...discussionPage.map(({ notes }) =>\n              notes.map(\n                ({ body, author, created_at }) =>\n                  `${author.username} at ${created_at}:\n${body}`\n              )\n            )\n          );\n        }\n        const result = {\n          ...issue,\n          discussions,\n        };\n        return result;\n      });\n\n      const pageIssues = await Promise.all(pagePromises);\n\n      issues.push(...pageIssues);\n      console.log(`Fetched ${issues.length} issues.`);\n    }\n    console.log(`Total issues fetched: ${issues.length}`);\n    return issues;\n  }\n\n  /**\n   * Fetches all wiki pages from the repository.\n   * @returns {Promise<WikiPage[]>} An array of wiki page objects.\n   */\n  async fetchWiki() {\n    const wikiRequestData = {\n      endpoint: `/api/v4/projects/${this.projectId}/wikis`,\n      queryParams: {\n        with_content: \"1\",\n      },\n    };\n\n    const wikiPages = await this.fetchNextPage(wikiRequestData);\n    if (!Array.isArray(wikiPages)) return [];\n    console.log(`Total wiki pages fetched: ${wikiPages.length}`);\n    return wikiPages;\n  }\n\n  /**\n   * Fetches the content of a single file from the repository.\n   * @param {string} sourceFilePath - The path to the file in the repository.\n   * @returns {Promise<string|null>} The content of the file, or null if fetching fails.\n   */\n  async fetchSingleFileContents(sourceFilePath, retries = 0) {\n    try {\n      const url = `${this.apiBase}/api/v4/projects/${\n        this.projectId\n      }/repository/files/${encodeURIComponent(sourceFilePath)}/raw?ref=${\n        this.branch\n      }`;\n      const response = await fetch(url, {\n        method: \"GET\",\n        headers: this.accessToken ? { \"PRIVATE-TOKEN\": this.accessToken } : {},\n      });\n\n      if (response.status === 429) {\n        if (retries >= MAX_RETRIES) {\n          console.warn(\n            `[Gitlab Loader]: Rate limit persists for ${sourceFilePath} after ${retries} retries. Skipping.`\n          );\n          return null;\n        }\n        const retryAfter = Number(response.headers.get(\"retry-after\")) || 60;\n        console.warn(\n          `[Gitlab Loader]: Rate limit hit fetching ${sourceFilePath}. Waiting ${retryAfter}s...`\n        );\n        await this.#wait(retryAfter * 1000);\n        return this.fetchSingleFileContents(sourceFilePath, retries + 1);\n      }\n\n      if (!response.ok)\n        throw new Error(`Failed to fetch single file ${sourceFilePath}`);\n\n      return await response.text();\n    } catch (e) {\n      console.error(`RepoLoader.fetchSingleFileContents`, e);\n      return null;\n    }\n  }\n\n  /**\n   * Fetches the next page of data from the API.\n   * @param {Object} requestData - The request data.\n   * @returns {Promise<Array<Object>|null>} The next page of data, or null if no more pages.\n   */\n  async fetchNextPage(requestData, retries = 0) {\n    try {\n      if (requestData.page === -1) return null;\n      if (!requestData.page) requestData.page = 1;\n\n      const { endpoint, perPage = 100, queryParams = {} } = requestData;\n      const params = new URLSearchParams({\n        ...queryParams,\n        per_page: perPage,\n        page: requestData.page,\n      });\n      const url = `${this.apiBase}${endpoint}?${params.toString()}`;\n\n      const response = await fetch(url, {\n        method: \"GET\",\n        headers: this.accessToken ? { \"PRIVATE-TOKEN\": this.accessToken } : {},\n      });\n\n      if (response.status === 429) {\n        if (retries >= MAX_RETRIES) {\n          console.warn(\n            `[Gitlab Loader]: Rate limit persists for ${endpoint} after ${retries} retries. Skipping.`\n          );\n          return null;\n        }\n        const retryAfter = Number(response.headers.get(\"retry-after\")) || 60;\n        console.warn(\n          `[Gitlab Loader]: Rate limit hit for ${endpoint}. Waiting ${retryAfter}s before retrying...`\n        );\n        await this.#wait(retryAfter * 1000);\n        return this.fetchNextPage(requestData, retries + 1);\n      }\n\n      if (response.status === 401) {\n        console.warn(\n          `[Gitlab Loader]: Unauthorized request for ${endpoint}. Skipping.`\n        );\n        return null;\n      }\n\n      if (!response.ok) {\n        console.warn(\n          `[Gitlab Loader]: Unexpected status ${response.status} for ${endpoint}. Skipping.`\n        );\n        return null;\n      }\n\n      const data = await response.json();\n      if (!Array.isArray(data)) {\n        console.warn(`Unexpected response format for ${endpoint}:`, data);\n        return [];\n      }\n\n      // GitLab omits x-total-pages for large repos, so use x-next-page\n      // as the sole pagination signal — it's empty on the last page.\n      const nextPage = response.headers.get(\"x-next-page\");\n      const totalPages = response.headers.get(\"x-total-pages\");\n      console.log(\n        `Gitlab RepoLoader: fetched ${endpoint} page ${requestData.page}${\n          totalPages ? `/${totalPages}` : \"\"\n        } with ${data.length} records.`\n      );\n\n      requestData.page = nextPage?.trim() ? Number(nextPage) : -1;\n\n      return data;\n    } catch (e) {\n      console.error(`RepoLoader.fetchNextPage`, e);\n      return null;\n    }\n  }\n}\n\nmodule.exports = GitLabRepoLoader;\n"
  },
  {
    "path": "collector/utils/extensions/RepoLoader/GitlabRepo/index.js",
    "content": "const RepoLoader = require(\"./RepoLoader\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { default: slugify } = require(\"slugify\");\nconst { v4 } = require(\"uuid\");\nconst { sanitizeFileName, writeToServerDocuments } = require(\"../../../files\");\nconst { tokenizeString } = require(\"../../../tokenizer\");\n\n/**\n * Load in a Gitlab Repo recursively or just the top level if no PAT is provided\n * @param {object} args - forwarded request body params\n * @param {import(\"../../../middleware/setDataSigner\").ResponseWithSigner} response - Express response object with encryptionWorker\n * @returns\n */\nasync function loadGitlabRepo(args, response) {\n  const repo = new RepoLoader(args);\n  await repo.init();\n\n  if (!repo.ready)\n    return {\n      success: false,\n      reason: \"Could not prepare Gitlab repo for loading! Check URL\",\n    };\n\n  console.log(\n    `-- Working GitLab ${repo.author}/${repo.project}:${repo.branch} --`\n  );\n  const docs = await repo.recursiveLoader();\n  if (!docs.length) {\n    return {\n      success: false,\n      reason: \"No files were found for those settings.\",\n    };\n  }\n\n  console.log(`[GitLab Loader]: Found ${docs.length} source files. Saving...`);\n  const outFolder = slugify(\n    `${repo.author}-${repo.project}-${repo.branch}-${v4().slice(0, 4)}`\n  ).toLowerCase();\n\n  const outFolderPath =\n    process.env.NODE_ENV === \"development\"\n      ? path.resolve(\n          __dirname,\n          `../../../../../server/storage/documents/${outFolder}`\n        )\n      : path.resolve(process.env.STORAGE_DIR, `documents/${outFolder}`);\n\n  if (!fs.existsSync(outFolderPath))\n    fs.mkdirSync(outFolderPath, { recursive: true });\n\n  for (const doc of docs) {\n    if (!doc.metadata || (!doc.pageContent && !doc.issue && !doc.wiki))\n      continue;\n    let pageContent = null;\n\n    const data = {\n      id: v4(),\n      url: \"gitlab://\" + doc.metadata.source,\n      docSource: doc.metadata.source,\n      chunkSource: generateChunkSource(\n        repo,\n        doc,\n        response.locals.encryptionWorker\n      ),\n      published: new Date().toLocaleString(),\n    };\n\n    if (doc.pageContent) {\n      pageContent = doc.pageContent;\n\n      data.title = doc.metadata.source;\n      data.docAuthor = repo.author;\n      data.description = \"No description found.\";\n    } else if (doc.issue) {\n      pageContent = issueToMarkdown(doc.issue);\n\n      data.title = `Issue ${doc.issue.iid}: ${doc.issue.title}`;\n      data.docAuthor = doc.issue.author.username;\n      data.description = doc.issue.description;\n    } else if (doc.wiki) {\n      pageContent = doc.wiki.content;\n      data.title = doc.wiki.title;\n      data.docAuthor = repo.author;\n      data.description =\n        doc.wiki.format === \"markdown\"\n          ? \"GitLab Wiki Page (Markdown)\"\n          : \"GitLab Wiki Page\";\n    } else {\n      continue;\n    }\n\n    data.wordCount = pageContent.split(\" \").length;\n    data.token_count_estimate = tokenizeString(pageContent);\n    data.pageContent = pageContent;\n\n    console.log(\n      `[GitLab Loader]: Saving ${doc.metadata.source} to ${outFolder}`\n    );\n\n    writeToServerDocuments({\n      data,\n      filename: sanitizeFileName(`${slugify(doc.metadata.source)}-${data.id}`),\n      destinationOverride: outFolderPath,\n    });\n  }\n\n  return {\n    success: true,\n    reason: null,\n    data: {\n      author: repo.author,\n      repo: repo.project,\n      projectId: repo.projectId,\n      branch: repo.branch,\n      files: docs.length,\n      destination: outFolder,\n    },\n  };\n}\n\nasync function fetchGitlabFile({\n  repoUrl,\n  branch,\n  accessToken = null,\n  sourceFilePath,\n}) {\n  const repo = new RepoLoader({\n    repo: repoUrl,\n    branch,\n    accessToken,\n  });\n  await repo.init();\n\n  if (!repo.ready)\n    return {\n      success: false,\n      content: null,\n      reason: \"Could not prepare GitLab repo for loading! Check URL or PAT.\",\n    };\n  console.log(\n    `-- Working GitLab ${repo.author}/${repo.project}:${repo.branch} file:${sourceFilePath} --`\n  );\n  const fileContent = await repo.fetchSingleFile(sourceFilePath);\n  if (!fileContent) {\n    return {\n      success: false,\n      reason: \"Target file returned a null content response.\",\n      content: null,\n    };\n  }\n\n  return {\n    success: true,\n    reason: null,\n    content: fileContent,\n  };\n}\n\nfunction generateChunkSource(repo, doc, encryptionWorker) {\n  const payload = {\n    projectId: decodeURIComponent(repo.projectId),\n    branch: repo.branch,\n    path: doc.metadata.source,\n    pat: !!repo.accessToken ? repo.accessToken : null,\n  };\n  return `gitlab://${repo.repo}?payload=${encryptionWorker.encrypt(\n    JSON.stringify(payload)\n  )}`;\n}\n\nfunction issueToMarkdown(issue) {\n  const metadata = {};\n\n  const userFields = [\"author\", \"assignees\", \"closed_by\"];\n  const userToUsername = ({ username }) => username;\n  for (const userField of userFields) {\n    if (issue[userField]) {\n      if (Array.isArray(issue[userField])) {\n        metadata[userField] = issue[userField].map(userToUsername);\n      } else {\n        metadata[userField] = userToUsername(issue[userField]);\n      }\n    }\n  }\n\n  const singleValueFields = [\n    \"web_url\",\n    \"state\",\n    \"created_at\",\n    \"updated_at\",\n    \"closed_at\",\n    \"due_date\",\n    \"type\",\n    \"merge_request_count\",\n    \"upvotes\",\n    \"downvotes\",\n    \"labels\",\n    \"has_tasks\",\n    \"task_status\",\n    \"confidential\",\n    \"severity\",\n  ];\n\n  for (const singleValueField of singleValueFields) {\n    metadata[singleValueField] = issue[singleValueField];\n  }\n\n  if (issue.milestone) {\n    metadata.milestone = `${issue.milestone.title} (${issue.milestone.id})`;\n  }\n\n  if (issue.time_stats) {\n    const timeFields = [\"time_estimate\", \"total_time_spent\"];\n    for (const timeField of timeFields) {\n      const fieldName = `human_${timeField}`;\n      if (issue?.time_stats[fieldName]) {\n        metadata[timeField] = issue.time_stats[fieldName];\n      }\n    }\n  }\n\n  const metadataString = Object.entries(metadata)\n    .map(([name, value]) => {\n      if (!value || value?.length < 1) {\n        return null;\n      }\n      let result = `- ${name.replace(\"_\", \" \")}:`;\n\n      if (!Array.isArray(value)) {\n        result += ` ${value}`;\n      } else {\n        result += \"\\n\" + value.map((s) => `  - ${s}`).join(\"\\n\");\n      }\n\n      return result;\n    })\n    .filter((item) => item != null)\n    .join(\"\\n\");\n\n  let markdown = `# ${issue.title} (${issue.iid})\n\n${issue.description}\n\n## Metadata\n\n${metadataString}`;\n\n  if (issue.discussions.length > 0) {\n    markdown += `\n\n## Activity\n\n${issue.discussions.join(\"\\n\\n\")}\n`;\n  }\n\n  return markdown;\n}\n\nmodule.exports = { loadGitlabRepo, fetchGitlabFile };\n"
  },
  {
    "path": "collector/utils/extensions/RepoLoader/index.js",
    "content": "/**\n * Dynamically load the correct repository loader from a specific platform\n * by default will return GitHub.\n * @param {('github'|'gitlab')} platform\n * @returns {import(\"./GithubRepo/RepoLoader\")|import(\"./GitlabRepo/RepoLoader\")} the repo loader class for provider\n */\nfunction resolveRepoLoader(platform = \"github\") {\n  switch (platform) {\n    case \"github\":\n      console.log(`Loading GitHub RepoLoader...`);\n      return require(\"./GithubRepo/RepoLoader\");\n    case \"gitlab\":\n      console.log(`Loading GitLab RepoLoader...`);\n      return require(\"./GitlabRepo/RepoLoader\");\n    default:\n      console.log(`Loading GitHub RepoLoader...`);\n      return require(\"./GithubRepo/RepoLoader\");\n  }\n}\n\n/**\n * Dynamically load the correct repository loader function from a specific platform\n * by default will return Github.\n * @param {('github'|'gitlab')} platform\n * @returns {import(\"./GithubRepo\")['fetchGithubFile'] | import(\"./GitlabRepo\")['fetchGitlabFile']} the repo loader class for provider\n */\nfunction resolveRepoLoaderFunction(platform = \"github\") {\n  switch (platform) {\n    case \"github\":\n      console.log(`Loading GitHub loader function...`);\n      return require(\"./GithubRepo\").loadGithubRepo;\n    case \"gitlab\":\n      console.log(`Loading GitLab loader function...`);\n      return require(\"./GitlabRepo\").loadGitlabRepo;\n    default:\n      console.log(`Loading GitHub loader function...`);\n      return require(\"./GithubRepo\").loadGithubRepo;\n  }\n}\n\nmodule.exports = { resolveRepoLoader, resolveRepoLoaderFunction };\n"
  },
  {
    "path": "collector/utils/extensions/WebsiteDepth/index.js",
    "content": "const { v4 } = require(\"uuid\");\nconst {\n  PuppeteerWebBaseLoader,\n} = require(\"langchain/document_loaders/web/puppeteer\");\nconst { default: slugify } = require(\"slugify\");\nconst { parse } = require(\"node-html-parser\");\nconst { writeToServerDocuments, documentsFolder } = require(\"../../files\");\nconst { tokenizeString } = require(\"../../tokenizer\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst RuntimeSettings = require(\"../../runtimeSettings\");\n\nasync function discoverLinks(startUrl, maxDepth = 1, maxLinks = 20) {\n  const baseUrl = new URL(startUrl);\n  const discoveredLinks = new Set([startUrl]);\n  let queue = [[startUrl, 0]]; // [url, currentDepth]\n  const scrapedUrls = new Set();\n\n  for (let currentDepth = 0; currentDepth < maxDepth; currentDepth++) {\n    const levelSize = queue.length;\n    const nextQueue = [];\n\n    for (let i = 0; i < levelSize && discoveredLinks.size < maxLinks; i++) {\n      const [currentUrl, urlDepth] = queue[i];\n\n      if (!scrapedUrls.has(currentUrl)) {\n        scrapedUrls.add(currentUrl);\n        const newLinks = await getPageLinks(currentUrl, baseUrl);\n\n        for (const link of newLinks) {\n          if (!discoveredLinks.has(link) && discoveredLinks.size < maxLinks) {\n            discoveredLinks.add(link);\n            if (urlDepth + 1 < maxDepth) {\n              nextQueue.push([link, urlDepth + 1]);\n            }\n          }\n        }\n      }\n    }\n\n    queue = nextQueue;\n    if (queue.length === 0 || discoveredLinks.size >= maxLinks) break;\n  }\n\n  return Array.from(discoveredLinks);\n}\n\nasync function getPageLinks(url, baseUrl) {\n  try {\n    const runtimeSettings = new RuntimeSettings();\n    /** @type {import('puppeteer').PuppeteerLaunchOptions} */\n    let launchConfig = { headless: \"new\" };\n\n    /* On MacOS 15.1, the headless=new option causes the browser to crash immediately.\n     * It is not clear why this is the case, but it is reproducible. Since AnythinglLM\n     * in production runs in a container, we can disable headless mode to workaround the issue for development purposes.\n     *\n     * This may show a popup window when scraping a page in development mode.\n     * This is expected behavior if seen in development mode on MacOS 15+\n     */\n    if (\n      process.platform === \"darwin\" &&\n      process.env.NODE_ENV === \"development\"\n    ) {\n      console.log(\n        \"Darwin Development Mode: Disabling headless mode to prevent Chromium from crashing.\"\n      );\n      launchConfig.headless = \"false\";\n    }\n\n    const loader = new PuppeteerWebBaseLoader(url, {\n      launchOptions: {\n        headless: launchConfig.headless,\n        ignoreHTTPSErrors: true,\n        args: runtimeSettings.get(\"browserLaunchArgs\"),\n      },\n      gotoOptions: { waitUntil: \"networkidle2\" },\n    });\n    const docs = await loader.load();\n    const html = docs[0].pageContent;\n    const links = extractLinks(html, baseUrl);\n    return links;\n  } catch (error) {\n    console.error(`Failed to get page links from ${url}.`, error);\n    return [];\n  }\n}\n\nfunction extractLinks(html, baseUrl) {\n  const root = parse(html);\n  const links = root.querySelectorAll(\"a\");\n  const extractedLinks = new Set();\n\n  for (const link of links) {\n    const href = link.getAttribute(\"href\");\n    if (href) {\n      const absoluteUrl = new URL(href, baseUrl.href).href;\n      if (\n        absoluteUrl.startsWith(\n          baseUrl.origin + baseUrl.pathname.split(\"/\").slice(0, -1).join(\"/\")\n        )\n      ) {\n        extractedLinks.add(absoluteUrl);\n      }\n    }\n  }\n\n  return Array.from(extractedLinks);\n}\n\nasync function bulkScrapePages(links, outFolderPath) {\n  const runtimeSettings = new RuntimeSettings();\n  /** @type {import('puppeteer').PuppeteerLaunchOptions} */\n  let launchConfig = { headless: \"new\" };\n\n  /* On MacOS 15.1, the headless=new option causes the browser to crash immediately.\n   * It is not clear why this is the case, but it is reproducible. Since AnythinglLM\n   * in production runs in a container, we can disable headless mode to workaround the issue for development purposes.\n   *\n   * This may show a popup window when scraping a page in development mode.\n   * This is expected behavior if seen in development mode on MacOS 15+\n   */\n  if (process.platform === \"darwin\" && process.env.NODE_ENV === \"development\") {\n    console.log(\n      \"Darwin Development Mode: Disabling headless mode to prevent Chromium from crashing.\"\n    );\n    launchConfig.headless = \"false\";\n  }\n\n  const scrapedData = [];\n\n  for (let i = 0; i < links.length; i++) {\n    const link = links[i];\n    console.log(`Scraping ${i + 1}/${links.length}: ${link}`);\n\n    try {\n      const loader = new PuppeteerWebBaseLoader(link, {\n        launchOptions: {\n          headless: launchConfig.headless,\n          ignoreHTTPSErrors: true,\n          args: runtimeSettings.get(\"browserLaunchArgs\"),\n        },\n        gotoOptions: { waitUntil: \"networkidle2\" },\n        async evaluate(page, browser) {\n          const result = await page.evaluate(() => document.body.innerText);\n          await browser.close();\n          return result;\n        },\n      });\n      const docs = await loader.load();\n      const content = docs[0].pageContent;\n\n      if (!content.length) {\n        console.warn(`Empty content for ${link}. Skipping.`);\n        continue;\n      }\n\n      const url = new URL(link);\n      const decodedPathname = decodeURIComponent(url.pathname);\n      const filename = `${url.hostname}${decodedPathname.replace(/\\//g, \"_\")}`;\n\n      const data = {\n        id: v4(),\n        url: \"file://\" + slugify(filename) + \".html\",\n        title: slugify(filename) + \".html\",\n        docAuthor: \"no author found\",\n        description: \"No description found.\",\n        docSource: \"URL link uploaded by the user.\",\n        chunkSource: `link://${link}`,\n        published: new Date().toLocaleString(),\n        wordCount: content.split(\" \").length,\n        pageContent: content,\n        token_count_estimate: tokenizeString(content),\n      };\n\n      writeToServerDocuments({\n        data,\n        filename: data.title,\n        destinationOverride: outFolderPath,\n      });\n      scrapedData.push(data);\n\n      console.log(`Successfully scraped ${link}.`);\n    } catch (error) {\n      console.error(`Failed to scrape ${link}.`, error);\n    }\n  }\n\n  return scrapedData;\n}\n\nasync function websiteScraper(startUrl, depth = 1, maxLinks = 20) {\n  const websiteName = new URL(startUrl).hostname;\n  const outFolder = slugify(\n    `${slugify(websiteName)}-${v4().slice(0, 4)}`\n  ).toLowerCase();\n  const outFolderPath = path.resolve(documentsFolder, outFolder);\n  console.log(\"Discovering links...\");\n  const linksToScrape = await discoverLinks(startUrl, depth, maxLinks);\n  console.log(`Found ${linksToScrape.length} links to scrape.`);\n\n  if (!fs.existsSync(outFolderPath))\n    fs.mkdirSync(outFolderPath, { recursive: true });\n  console.log(\"Starting bulk scraping...\");\n  const scrapedData = await bulkScrapePages(linksToScrape, outFolderPath);\n  console.log(`Scraped ${scrapedData.length} pages.`);\n\n  return scrapedData;\n}\n\nmodule.exports = websiteScraper;\n"
  },
  {
    "path": "collector/utils/extensions/YoutubeTranscript/YoutubeLoader/index.js",
    "content": "const { validYoutubeVideoUrl } = require(\"../../../url\");\n\n/*\n * This is just a custom implementation of the Langchain JS YouTubeLoader class\n * as the dependency for YoutubeTranscript is quite fickle and its a rat race to keep it up\n * and instead of waiting for patches we can just bring this simple script in-house and at least\n * be able to patch it since its so flaky. When we have more connectors we can kill this because\n * it will be a pain to maintain over time.\n */\nclass YoutubeLoader {\n  #videoId;\n  #language;\n  #addVideoInfo;\n\n  constructor({ videoId = null, language = null, addVideoInfo = false } = {}) {\n    if (!videoId) throw new Error(\"Invalid video id!\");\n    this.#videoId = videoId;\n    this.#language = language;\n    this.#addVideoInfo = addVideoInfo;\n  }\n\n  /**\n   * Extracts the videoId from a YouTube video URL.\n   * @param url The URL of the YouTube video.\n   * @returns The videoId of the YouTube video.\n   */\n  static getVideoID(url) {\n    const videoId = validYoutubeVideoUrl(url, true);\n    if (videoId) return videoId;\n    throw new Error(\"Failed to get youtube video id from the url\");\n  }\n\n  /**\n   * Creates a new instance of the YoutubeLoader class from a YouTube video\n   * URL.\n   * @param url The URL of the YouTube video.\n   * @param config Optional configuration options for the YoutubeLoader instance, excluding the videoId.\n   * @returns A new instance of the YoutubeLoader class.\n   */\n  static createFromUrl(url, config = {}) {\n    const videoId = YoutubeLoader.getVideoID(url);\n    return new YoutubeLoader({ ...config, videoId });\n  }\n\n  /**\n   * Loads the transcript and video metadata from the specified YouTube\n   * video. It uses the youtube-transcript library to fetch the transcript\n   * and the youtubei.js library to fetch the video metadata.\n   * @returns Langchain like doc that is 1 element with PageContent and\n   */\n  async load() {\n    let transcript;\n    const metadata = {\n      source: this.#videoId,\n    };\n    try {\n      const fetchTranscript = await import(\"youtube-transcript-plus\").then(\n        (module) => module.fetchTranscript\n      );\n      const transcriptSegments = await fetchTranscript(this.#videoId, {\n        lang: this.#language,\n      });\n      if (!transcriptSegments || transcriptSegments.length === 0)\n        throw new Error(\"Transcription not found\");\n      transcript = this.#convertTranscriptSegmentsToText(transcriptSegments);\n      if (this.#addVideoInfo) {\n        const { Innertube } = require(\"youtubei.js\");\n        const youtube = await Innertube.create();\n        const info = (await youtube.getBasicInfo(this.#videoId)).basic_info;\n        metadata.description = info.short_description;\n        metadata.title = info.title;\n        metadata.view_count = info.view_count;\n        metadata.author = info.author;\n      }\n    } catch (e) {\n      throw new Error(\n        `Failed to get YouTube video transcription: ${e?.message}`\n      );\n    }\n    return [\n      {\n        pageContent: transcript,\n        metadata,\n      },\n    ];\n  }\n\n  #convertTranscriptSegmentsToText(transcriptSegments) {\n    return transcriptSegments\n      .map((segment) =>\n        typeof segment === \"string\" ? segment : segment.text || \"\"\n      )\n      .join(\" \")\n      .replace(/\\s+/g, \" \")\n      .trim();\n  }\n}\n\nmodule.exports.YoutubeLoader = YoutubeLoader;\n"
  },
  {
    "path": "collector/utils/extensions/YoutubeTranscript/YoutubeLoader/youtube-transcript.js",
    "content": "const { validYoutubeVideoUrl } = require(\"../../../url\");\n\nclass YoutubeTranscriptError extends Error {\n  constructor(message) {\n    super(`[YoutubeTranscript] ${message}`);\n  }\n}\n\n/**\n * Handles fetching and parsing YouTube video transcripts\n */\nclass YoutubeTranscript {\n  /**\n   * Encodes a string as a protobuf field\n   * @param {number} fieldNumber - The protobuf field number\n   * @param {string} str - The string to encode\n   * @returns {Buffer} Encoded protobuf field\n   */\n  static #encodeProtobufString(fieldNumber, str) {\n    const utf8Bytes = Buffer.from(str, \"utf8\");\n    const tag = (fieldNumber << 3) | 2; // wire type 2 for string\n    const lengthBytes = this.#encodeVarint(utf8Bytes.length);\n\n    return Buffer.concat([\n      Buffer.from([tag]),\n      Buffer.from(lengthBytes),\n      utf8Bytes,\n    ]);\n  }\n\n  /**\n   * Encodes a number as a protobuf varint\n   * @param {number} value - The number to encode\n   * @returns {number[]} Encoded varint bytes\n   */\n  static #encodeVarint(value) {\n    const bytes = [];\n    while (value >= 0x80) {\n      bytes.push((value & 0x7f) | 0x80);\n      value >>>= 7;\n    }\n    bytes.push(value);\n    return bytes;\n  }\n\n  /**\n   * Creates a base64 encoded protobuf message\n   * @param {Object} param - The parameters to encode\n   * @param {string} param.param1 - First parameter\n   * @param {string} param.param2 - Second parameter\n   * @returns {string} Base64 encoded protobuf\n   */\n  static #getBase64Protobuf({ param1, param2 }) {\n    const field1 = this.#encodeProtobufString(1, param1);\n    const field2 = this.#encodeProtobufString(2, param2);\n    return Buffer.concat([field1, field2]).toString(\"base64\");\n  }\n\n  /**\n   * Extracts transcript text from YouTube API response\n   * @param {Object} responseData - The YouTube API response\n   * @returns {string} Combined transcript text\n   */\n  static #extractTranscriptFromResponse(responseData) {\n    const transcriptRenderer =\n      responseData.actions?.[0]?.updateEngagementPanelAction?.content\n        ?.transcriptRenderer;\n    if (!transcriptRenderer) {\n      throw new Error(\"No transcript data found in response\");\n    }\n\n    const segments =\n      transcriptRenderer.content?.transcriptSearchPanelRenderer?.body\n        ?.transcriptSegmentListRenderer?.initialSegments;\n    if (!segments) {\n      throw new Error(\"Transcript segments not found in response\");\n    }\n\n    return segments\n      .map((segment) => {\n        const runs = segment.transcriptSegmentRenderer?.snippet?.runs;\n        return runs ? runs.map((run) => run.text).join(\"\") : \"\";\n      })\n      .filter((text) => text)\n      .join(\" \")\n      .trim()\n      .replace(/\\s+/g, \" \");\n  }\n\n  /**\n   * Calculates a preference score for a caption track to determine the best match\n   * @param {Object} track - The caption track object from YouTube\n   * @param {string} track.languageCode - ISO language code (e.g., 'zh-HK', 'en', 'es')\n   * @param {string} track.kind - Track type ('asr' for auto-generated, \"\" for human-transcribed)\n   * @param {string[]} preferredLanguages - Array of language codes in preference order (e.g., ['zh-HK', 'en'])\n   * @returns {number} Preference score (lower is better)\n   */\n  static #calculatePreferenceScore(track, preferredLanguages) {\n    // Language preference: index in preferredLanguages array (0 = most preferred)\n    const languagePreference = preferredLanguages.indexOf(track.languageCode);\n    const languageScore = languagePreference === -1 ? 9999 : languagePreference;\n\n    // Kind bonus: prefer human-transcribed (undefined) over auto-generated ('asr')\n    const kindBonus = track.kind === \"asr\" ? 0.5 : 0;\n\n    return languageScore + kindBonus;\n  }\n\n  /**\n   * Finds the most suitable caption track based on preferred languages\n   * @param {string} videoBody - The raw HTML response from YouTube\n   * @param {string[]} preferredLanguages - Array of language codes in preference order\n   * @returns {Object|null} The selected caption track or null if none found\n   */\n  static #findPreferredCaptionTrack(videoBody, preferredLanguages) {\n    const captionsConfigJson = videoBody.match(\n      /\"captions\":(.*?),\"videoDetails\":/s\n    );\n\n    const captionsConfig = captionsConfigJson?.[1]\n      ? JSON.parse(captionsConfigJson[1])\n      : null;\n\n    const captionTracks = captionsConfig\n      ? captionsConfig.playerCaptionsTracklistRenderer.captionTracks\n      : null;\n\n    if (!captionTracks || captionTracks.length === 0) {\n      return null;\n    }\n\n    const sortedTracks = [...captionTracks].sort((a, b) => {\n      const scoreA = this.#calculatePreferenceScore(a, preferredLanguages);\n      const scoreB = this.#calculatePreferenceScore(b, preferredLanguages);\n      return scoreA - scoreB;\n    });\n\n    return sortedTracks[0];\n  }\n\n  /**\n   * Fetches video page content and finds the preferred caption track\n   * @param {string} videoId - YouTube video ID\n   * @param {string[]} preferredLanguages - Array of preferred language codes\n   * @returns {Promise<Object>} The preferred caption track\n   * @throws {YoutubeTranscriptError} If no suitable caption track is found\n   */\n  static async #getPreferredCaptionTrack(videoId, preferredLanguages) {\n    const videoResponse = await fetch(\n      `https://www.youtube.com/watch?v=${videoId}`,\n      { credentials: \"omit\" }\n    );\n    const videoBody = await videoResponse.text();\n\n    const preferredCaptionTrack = this.#findPreferredCaptionTrack(\n      videoBody,\n      preferredLanguages\n    );\n\n    if (!preferredCaptionTrack) {\n      throw new YoutubeTranscriptError(\n        \"No suitable caption track found for the video\"\n      );\n    }\n\n    return preferredCaptionTrack;\n  }\n\n  /**\n   * Fetch transcript from YouTube video\n   * @param {string} videoId - Video URL or video identifier\n   * @param {Object} config - Configuration options\n   * @param {string} [config.lang='en'] - Language code (e.g., 'en', 'es', 'fr')\n   * @returns {Promise<string>} Video transcript text\n   */\n  static async fetchTranscript(videoId, config = {}) {\n    const preferredLanguages = config?.lang ? [config?.lang, \"en\"] : [\"en\"];\n    const identifier = this.retrieveVideoId(videoId);\n\n    try {\n      const preferredCaptionTrack = await this.#getPreferredCaptionTrack(\n        identifier,\n        preferredLanguages\n      );\n\n      const innerProto = this.#getBase64Protobuf({\n        param1: preferredCaptionTrack.kind || \"\",\n        param2: preferredCaptionTrack.languageCode,\n      });\n\n      const params = this.#getBase64Protobuf({\n        param1: identifier,\n        param2: innerProto,\n      });\n\n      const response = await fetch(\n        \"https://www.youtube.com/youtubei/v1/get_transcript\",\n        {\n          method: \"POST\",\n          headers: {\n            \"Content-Type\": \"application/json\",\n            \"User-Agent\":\n              \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36,gzip(gfe)\",\n          },\n          body: JSON.stringify({\n            context: {\n              client: {\n                clientName: \"WEB\",\n                clientVersion: \"2.20240826.01.00\",\n              },\n            },\n            params,\n          }),\n        }\n      );\n\n      if (!response.ok) {\n        throw new Error(`HTTP error! status: ${response.status}`);\n      }\n\n      const responseData = await response.json();\n      return this.#extractTranscriptFromResponse(responseData);\n    } catch (e) {\n      throw new YoutubeTranscriptError(e.message || e);\n    }\n  }\n\n  /**\n   * Extract video ID from a YouTube URL or verify an existing ID\n   * @param {string} videoId - Video URL or ID\n   * @returns {string} YouTube video ID\n   */\n  static retrieveVideoId(videoId) {\n    if (videoId.length === 11) return videoId; // already a valid ID most likely\n    const matchedId = validYoutubeVideoUrl(videoId, true);\n    if (matchedId) return matchedId;\n    throw new YoutubeTranscriptError(\n      \"Impossible to retrieve Youtube video ID.\"\n    );\n  }\n}\n\nmodule.exports = {\n  YoutubeTranscript,\n  YoutubeTranscriptError,\n};\n"
  },
  {
    "path": "collector/utils/extensions/YoutubeTranscript/index.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { default: slugify } = require(\"slugify\");\nconst { v4 } = require(\"uuid\");\nconst {\n  writeToServerDocuments,\n  sanitizeFileName,\n  documentsFolder,\n  isWithin,\n} = require(\"../../files\");\nconst { tokenizeString } = require(\"../../tokenizer\");\nconst { YoutubeLoader } = require(\"./YoutubeLoader\");\nconst { validYoutubeVideoUrl } = require(\"../../url\");\n\n/**\n * Fetch the transcript content for a YouTube video\n * @param {string} url - The URL of the YouTube video\n * @returns {Promise<{success: boolean, reason: string|null, content: string|null, metadata: TranscriptMetadata}>} - The transcript content for the YouTube video\n */\nasync function fetchVideoTranscriptContent({ url }) {\n  if (!validYoutubeVideoUrl(url)) {\n    return {\n      success: false,\n      reason: \"Invalid URL. Should be youtu.be or youtube.com/watch.\",\n      content: null,\n      metadata: {},\n    };\n  }\n\n  console.log(`-- Working YouTube ${url} --`);\n  const loader = YoutubeLoader.createFromUrl(url, { addVideoInfo: true });\n  const { docs, error } = await loader\n    .load()\n    .then((docs) => ({ docs, error: null }))\n    .catch((e) => ({\n      docs: [],\n      error: e.message?.split(\"Error:\")?.[1] || e.message,\n    }));\n\n  if (!docs.length || !!error) {\n    return {\n      success: false,\n      reason: error ?? \"No transcript found for that YouTube video.\",\n      content: null,\n      metadata: {},\n    };\n  }\n\n  const metadata = docs[0].metadata;\n  const content = docs[0].pageContent;\n  if (!content.length) {\n    return {\n      success: false,\n      reason: \"No transcript could be parsed for that YouTube video.\",\n      content: null,\n      metadata: {},\n    };\n  }\n\n  return {\n    success: true,\n    reason: null,\n    content,\n    metadata,\n  };\n}\n\n/**\n * @typedef {Object} TranscriptMetadata\n * @property {string} title - The title of the video\n * @property {string} author - The author of the video\n * @property {string} description - The description of the video\n * @property {string} view_count - The view count of the video\n * @property {string} source - The source of the video (videoId)\n */\n\n/**\n * @typedef {Object} TranscriptAsDocument\n * @property {boolean} success - Whether the transcript was successful\n * @property {string|null} reason - The reason for the transcript\n * @property {TranscriptMetadata} metadata - The metadata from the transcript\n */\n\n/**\n * @typedef {Object} TranscriptAsContent\n * @property {boolean} success - Whether the transcript was successful\n * @property {string|null} reason - The reason for the transcript\n * @property {string|null} content - The content of the transcript\n * @property {Object[]} documents - The documents from the transcript\n * @property {boolean} saveAsDocument - Whether to save the transcript as a document\n */\n\n/**\n * Load the transcript content for a YouTube video as well as save it to the server documents\n * @param {Object} params - The parameters for the YouTube transcript\n * @param {string} params.url - The URL of the YouTube video\n * @param {Object} options - The options for the YouTube transcript\n * @param {boolean} options.parseOnly - Whether to parse the transcript content only or save it to the server documents\n * @returns {Promise<TranscriptAsDocument | TranscriptAsContent>} - The transcript content for the YouTube video\n */\nasync function loadYouTubeTranscript({ url }, options = { parseOnly: false }) {\n  const transcriptResults = await fetchVideoTranscriptContent({ url });\n  if (!transcriptResults.success) {\n    return {\n      success: false,\n      reason:\n        transcriptResults.reason ||\n        \"An unknown error occurred during transcription retrieval\",\n      documents: [],\n      content: null,\n      saveAsDocument: options.parseOnly,\n      data: {},\n    };\n  }\n\n  const { content, metadata } = transcriptResults;\n\n  if (options.parseOnly) {\n    return {\n      success: true,\n      reason: null,\n      content: buildTranscriptContentWithMetadata(content, metadata),\n      documents: [],\n      saveAsDocument: options.parseOnly,\n      data: {},\n    };\n  }\n\n  const outFolder = sanitizeFileName(\n    slugify(`${metadata.author} YouTube transcripts`).toLowerCase()\n  );\n  const outFolderPath = path.resolve(documentsFolder, outFolder);\n  const uuid = v4();\n  const fileName = sanitizeFileName(`${slugify(metadata.title)}-${uuid}`);\n\n  if (!isWithin(documentsFolder, path.resolve(outFolderPath, fileName))) {\n    console.error(\n      `[YouTube Loader]: Invalid file path ${path.resolve(\n        outFolderPath,\n        fileName\n      )} is not within the documents folder ${documentsFolder}`\n    );\n    return {\n      success: false,\n      reason: `[YouTube Loader]: Invalid file path ${path.resolve(\n        outFolderPath,\n        fileName\n      )} is not within the documents folder ${documentsFolder}`,\n      documents: [],\n      data: {},\n    };\n  }\n\n  if (!fs.existsSync(outFolderPath))\n    fs.mkdirSync(outFolderPath, { recursive: true });\n  const data = {\n    id: uuid,\n    url: url + \".youtube\",\n    title: metadata.title || url,\n    docAuthor: metadata.author,\n    description: metadata.description,\n    docSource: url,\n    chunkSource: `youtube://${url}`,\n    published: new Date().toLocaleString(),\n    wordCount: content.split(\" \").length,\n    pageContent: content,\n    token_count_estimate: tokenizeString(content),\n  };\n\n  console.log(`[YouTube Loader]: Saving ${metadata.title} to ${outFolder}`);\n  const document = writeToServerDocuments({\n    data,\n    filename: fileName,\n    destinationOverride: outFolderPath,\n  });\n\n  return {\n    success: true,\n    reason: null,\n    documents: [document],\n    data: {\n      title: metadata.title,\n      author: metadata.author,\n      destination: outFolder,\n    },\n  };\n}\n\n/**\n * Generate the transcript content and metadata into a single string\n *\n * Why? For ephemeral documents where we just want the content, we want to include the metadata as keys in the content\n * so that the LLM has context about the video, this gives it a better understanding of the video\n * and allows it to use the metadata in the conversation if relevant.\n * Examples:\n * - How many views does <LINK> have?\n * - Checkout <LINK> and tell me the key points and if it is performing well\n * - Summarize this video <LINK>? -> description could have links and references\n * @param {string} content - The content of the transcript\n * @param {TranscriptMetadata} metadata - The metadata from the transcript\n * @returns {string} - The concatenated transcript content and metadata\n */\nfunction buildTranscriptContentWithMetadata(content = \"\", metadata = {}) {\n  const VALID_METADATA_KEYS = [\"title\", \"author\", \"description\", \"view_count\"];\n  if (!content || !metadata || Object.keys(metadata).length === 0)\n    return content;\n\n  let contentWithMetadata = \"\";\n  VALID_METADATA_KEYS.forEach((key) => {\n    if (!metadata[key]) return;\n    contentWithMetadata += `<${key}>${metadata[key]}</${key}>`;\n  });\n  return `${contentWithMetadata}\\nTranscript:\\n${content}`;\n}\n\nmodule.exports = {\n  loadYouTubeTranscript,\n  fetchVideoTranscriptContent,\n};\n"
  },
  {
    "path": "collector/utils/files/index.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { MimeDetector } = require(\"./mime\");\n\n/**\n * The folder where documents are stored to be stored when\n * processed by the collector.\n */\nconst documentsFolder =\n  process.env.NODE_ENV === \"development\"\n    ? path.resolve(__dirname, `../../../server/storage/documents`)\n    : path.resolve(process.env.STORAGE_DIR, `documents`);\n\n/**\n * The folder where direct uploads are stored to be stored when\n * processed by the collector. These are files that were DnD'd into UI\n * and are not to be embedded or selectable from the file picker.\n */\nconst directUploadsFolder =\n  process.env.NODE_ENV === \"development\"\n    ? path.resolve(__dirname, `../../../server/storage/direct-uploads`)\n    : path.resolve(process.env.STORAGE_DIR, `direct-uploads`);\n\n/**\n * Checks if a file is text by checking the mime type and then falling back to buffer inspection.\n * This way we can capture all the cases where the mime type is not known but still parseable as text\n * without having to constantly add new mime type overrides.\n * @param {string} filepath - The path to the file.\n * @returns {boolean} - Returns true if the file is text, false otherwise.\n */\nfunction isTextType(filepath) {\n  if (!fs.existsSync(filepath)) return false;\n  const result = isKnownTextMime(filepath);\n  if (result.valid) return true; // Known text type - return true.\n  if (result.reason !== \"generic\") return false; // If any other reason than generic - return false.\n  return parseableAsText(filepath); // Fallback to parsing as text via buffer inspection.\n}\n\n/**\n * Checks if a file is known to be text by checking the mime type.\n * @param {string} filepath - The path to the file.\n * @returns {boolean} - Returns true if the file is known to be text, false otherwise.\n */\nfunction isKnownTextMime(filepath) {\n  try {\n    const mimeLib = new MimeDetector();\n    const mime = mimeLib.getType(filepath);\n    if (mimeLib.badMimes.includes(mime))\n      return { valid: false, reason: \"bad_mime\" };\n\n    const type = mime.split(\"/\")[0];\n    if (mimeLib.nonTextTypes.includes(type))\n      return { valid: false, reason: \"non_text_mime\" };\n    return { valid: true, reason: \"valid_mime\" };\n  } catch {\n    return { valid: false, reason: \"generic\" };\n  }\n}\n\n/**\n * Checks if a file is parseable as text by forcing it to be read as text in utf8 encoding.\n * If the file looks too much like a binary file, it will return false.\n * @param {string} filepath - The path to the file.\n * @returns {boolean} - Returns true if the file is parseable as text, false otherwise.\n */\nfunction parseableAsText(filepath) {\n  try {\n    const fd = fs.openSync(filepath, \"r\");\n    const buffer = Buffer.alloc(1024); // Read first 1KB of the file synchronously\n    const bytesRead = fs.readSync(fd, buffer, 0, 1024, 0);\n    fs.closeSync(fd);\n\n    const content = buffer.subarray(0, bytesRead).toString(\"utf8\");\n    const nullCount = (content.match(/\\0/g) || []).length;\n    //eslint-disable-next-line\n    const controlCount = (content.match(/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/g) || [])\n      .length;\n\n    const threshold = bytesRead * 0.1;\n    return nullCount + controlCount < threshold;\n  } catch {\n    return false;\n  }\n}\n\nfunction trashFile(filepath) {\n  if (!fs.existsSync(filepath)) return;\n\n  try {\n    const isDir = fs.lstatSync(filepath).isDirectory();\n    if (isDir) return;\n  } catch {\n    return;\n  }\n\n  fs.rmSync(filepath);\n  return;\n}\n\nfunction createdDate(filepath) {\n  try {\n    const { birthtimeMs, birthtime } = fs.statSync(filepath);\n    if (birthtimeMs === 0) throw new Error(\"Invalid stat for file!\");\n    return birthtime.toLocaleString();\n  } catch {\n    return \"unknown\";\n  }\n}\n\n/**\n * Writes a document to the server documents folder.\n * @param {Object} params - The parameters for the function.\n * @param {Object} params.data - The data to write to the file. Must look like a document object.\n * @param {string} params.filename - The name of the file to write to.\n * @param {string|null} params.destinationOverride - A forced destination to write to - will be honored if provided.\n * @param {Object} params.options - The options for the function.\n * @param {boolean} params.options.parseOnly - If true, the file will be written to the direct uploads folder instead of the documents folder. Will be ignored if destinationOverride is provided.\n * @returns {Object} - The data with the location added.\n */\nfunction writeToServerDocuments({\n  data = {},\n  filename,\n  destinationOverride = null,\n  options = {},\n}) {\n  if (!filename) throw new Error(\"Filename is required!\");\n\n  let destination = null;\n  if (destinationOverride) destination = path.resolve(destinationOverride);\n  else if (options.parseOnly) destination = path.resolve(directUploadsFolder);\n  else destination = path.resolve(documentsFolder, \"custom-documents\");\n\n  if (!fs.existsSync(destination))\n    fs.mkdirSync(destination, { recursive: true });\n  const destinationFilePath = normalizePath(\n    path.resolve(destination, filename) + \".json\"\n  );\n\n  fs.writeFileSync(destinationFilePath, JSON.stringify(data, null, 4), {\n    encoding: \"utf-8\",\n  });\n\n  return {\n    ...data,\n    // relative location string that can be passed into the /update-embeddings api\n    // that will work since we know the location exists and since we only allow\n    // 1-level deep folders this will always work. This still works for integrations like GitHub and YouTube.\n    location: destinationFilePath.split(\"/\").slice(-2).join(\"/\"),\n    isDirectUpload: options.parseOnly || false,\n  };\n}\n\n// When required we can wipe the entire collector hotdir and tmp storage in case\n// there were some large file failures that we unable to be removed a reboot will\n// force remove them.\nasync function wipeCollectorStorage() {\n  const cleanHotDir = new Promise((resolve) => {\n    const directory = path.resolve(__dirname, \"../../hotdir\");\n    fs.readdir(directory, (err, files) => {\n      if (err) resolve();\n\n      for (const file of files) {\n        if (file === \"__HOTDIR__.md\") continue;\n        try {\n          fs.rmSync(path.join(directory, file));\n        } catch {}\n      }\n      resolve();\n    });\n  });\n\n  const cleanTmpDir = new Promise((resolve) => {\n    const directory = path.resolve(__dirname, \"../../storage/tmp\");\n    fs.readdir(directory, (err, files) => {\n      if (err) resolve();\n\n      for (const file of files) {\n        if (file === \".placeholder\") continue;\n        try {\n          fs.rmSync(path.join(directory, file));\n        } catch {}\n      }\n      resolve();\n    });\n  });\n\n  await Promise.all([cleanHotDir, cleanTmpDir]);\n  console.log(`Collector hot directory and tmp storage wiped!`);\n  return;\n}\n\n/**\n * Checks if a given path is within another path.\n * @param {string} outer - The outer path (should be resolved).\n * @param {string} inner - The inner path (should be resolved).\n * @returns {boolean} - Returns true if the inner path is within the outer path, false otherwise.\n */\nfunction isWithin(outer, inner) {\n  if (outer === inner) return false;\n  const rel = path.relative(outer, inner);\n  return !rel.startsWith(\"../\") && rel !== \"..\";\n}\n\nfunction normalizePath(filepath = \"\") {\n  const result = path\n    .normalize(filepath.trim())\n    .replace(/^(\\.\\.(\\/|\\\\|$))+/, \"\")\n    .trim();\n  if ([\"..\", \".\", \"/\"].includes(result)) throw new Error(\"Invalid path.\");\n  return result;\n}\n\nfunction sanitizeFileName(fileName) {\n  if (!fileName) return fileName;\n  //eslint-disable-next-line\n  return fileName.replace(/[<>:\"\\/\\\\|?*]/g, \"\");\n}\n\nmodule.exports = {\n  trashFile,\n  isTextType,\n  createdDate,\n  writeToServerDocuments,\n  wipeCollectorStorage,\n  normalizePath,\n  isWithin,\n  sanitizeFileName,\n  documentsFolder,\n  directUploadsFolder,\n};\n"
  },
  {
    "path": "collector/utils/files/mime.js",
    "content": "const MimeLib = require(\"mime\");\nclass MimeDetector {\n  nonTextTypes = [\"multipart\", \"model\", \"audio\", \"video\", \"font\"];\n  badMimes = [\n    \"application/octet-stream\",\n    \"application/zip\",\n    \"application/pkcs8\",\n    \"application/vnd.microsoft.portable-executable\",\n    \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\", // XLSX are binaries and need to be handled explicitly.\n    \"application/x-msdownload\",\n  ];\n\n  constructor() {\n    this.lib = MimeLib;\n    this.setOverrides();\n  }\n\n  setOverrides() {\n    // the .ts extension maps to video/mp2t because of https://en.wikipedia.org/wiki/MPEG_transport_stream\n    // which has had this extension far before TS was invented. So need to force re-map this MIME map.\n    this.lib.define(\n      {\n        \"text/plain\": [\n          \"ts\",\n          \"tsx\",\n          \"py\",\n          \"opts\",\n          \"lock\",\n          \"jsonl\",\n          \"qml\",\n          \"sh\",\n          \"c\",\n          \"cs\",\n          \"h\",\n          \"js\",\n          \"lua\",\n          \"pas\",\n          \"r\",\n          \"go\",\n          \"ino\",\n          \"hpp\",\n          \"linq\",\n          \"cs\",\n        ],\n      },\n      true\n    );\n  }\n\n  /**\n   * Returns the MIME type of the file. If the file has no extension found, it will be processed as a text file.\n   * @param {string} filepath\n   * @returns {string}\n   */\n  getType(filepath) {\n    const parsedMime = this.lib.getType(filepath);\n    if (!!parsedMime) return parsedMime;\n    return null;\n  }\n}\n\nmodule.exports = {\n  MimeDetector,\n};\n"
  },
  {
    "path": "collector/utils/http/index.js",
    "content": "process.env.NODE_ENV === \"development\"\n  ? require(\"dotenv\").config({ path: `.env.${process.env.NODE_ENV}` })\n  : require(\"dotenv\").config();\n\nfunction reqBody(request) {\n  return typeof request.body === \"string\"\n    ? JSON.parse(request.body)\n    : request.body;\n}\n\nfunction queryParams(request) {\n  return request.query;\n}\n\n/**\n * Validates if the provided baseUrl is a valid URL at all.\n * - Does not validate if the URL is reachable or accessible.\n * - Does not do any further validation of the URL like `validURL` in `utils/url/index.js`\n * @param {string} baseUrl\n * @returns {boolean}\n */\nfunction validBaseUrl(baseUrl) {\n  try {\n    new URL(baseUrl);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\nmodule.exports = {\n  reqBody,\n  queryParams,\n  validBaseUrl,\n};\n"
  },
  {
    "path": "collector/utils/logger/index.js",
    "content": "const winston = require(\"winston\");\n\nclass Logger {\n  logger = console;\n  static _instance;\n  constructor() {\n    if (Logger._instance) return Logger._instance;\n    this.logger =\n      process.env.NODE_ENV === \"production\" ? this.getWinstonLogger() : console;\n    Logger._instance = this;\n  }\n\n  getWinstonLogger() {\n    const logger = winston.createLogger({\n      level: \"info\",\n      defaultMeta: { service: \"collector\" },\n      transports: [\n        new winston.transports.Console({\n          format: winston.format.combine(\n            winston.format.colorize(),\n            winston.format.printf(\n              ({ level, message, service, origin = \"\" }) => {\n                return `\\x1b[36m[${service}]\\x1b[0m${\n                  origin ? `\\x1b[33m[${origin}]\\x1b[0m` : \"\"\n                } ${level}: ${message}`;\n              }\n            )\n          ),\n        }),\n      ],\n    });\n\n    function formatArgs(args) {\n      return args\n        .map((arg) => {\n          if (arg instanceof Error) {\n            return arg.stack; // If argument is an Error object, return its stack trace\n          } else if (typeof arg === \"object\") {\n            return JSON.stringify(arg); // Convert objects to JSON string\n          } else {\n            return arg; // Otherwise, return as-is\n          }\n        })\n        .join(\" \");\n    }\n\n    console.log = function (...args) {\n      logger.info(formatArgs(args));\n    };\n    console.error = function (...args) {\n      logger.error(formatArgs(args));\n    };\n    console.info = function (...args) {\n      logger.warn(formatArgs(args));\n    };\n    return logger;\n  }\n}\n\n/**\n * Sets and overrides Console methods for logging when called.\n * This is a singleton method and will not create multiple loggers.\n * @returns {winston.Logger | console} - instantiated logger interface.\n */\nfunction setLogger() {\n  return new Logger().logger;\n}\nmodule.exports = setLogger;\n"
  },
  {
    "path": "collector/utils/runtimeSettings/index.js",
    "content": "const { reqBody } = require(\"../http\");\n\n/**\n * Runtime settings are used to configure the collector per-request.\n * These settings are persisted across requests, but can be overridden per-request.\n *\n * The settings are passed in the request body via `options.runtimeSettings`\n * which is set in the backend #attachOptions function in CollectorApi.\n *\n * We do this so that the collector and backend can share the same ENV variables\n * but only pass the relevant settings to the collector per-request and be able to\n * access them across the collector via a single instance of RuntimeSettings.\n *\n * TODO: We may want to set all options passed from backend to collector here,\n * but for now - we are only setting the runtime settings specifically for backwards\n * compatibility with existing CollectorApi usage.\n */\nclass RuntimeSettings {\n  static _instance = null;\n  settings = {};\n\n  // Any settings here will be persisted across requests\n  // and must be explicitly defined here.\n  settingConfigs = {\n    seenAnyIpWarning: {\n      default: false,\n      validate: (value) => String(value) === \"true\",\n    },\n    allowAnyIp: {\n      default: false,\n      // Value must be explicitly \"true\" or \"false\" as a string\n      validate: (value) => String(value) === \"true\",\n    },\n    browserLaunchArgs: {\n      default: [],\n      validate: (value) => {\n        let args = [];\n        if (Array.isArray(value)) args = value.map((arg) => String(arg.trim()));\n        if (typeof value === \"string\")\n          args = value.split(\",\").map((arg) => arg.trim());\n        return args;\n      },\n    },\n  };\n\n  constructor() {\n    if (RuntimeSettings._instance) return RuntimeSettings._instance;\n    RuntimeSettings._instance = this;\n    return this;\n  }\n\n  /**\n   * Parse the runtime settings from the request body options body\n   * see #attachOptions https://github.com/Mintplex-Labs/anything-llm/blob/ebf112007e0d579af3d2b43569db95bdfc59074b/server/utils/collectorApi/index.js#L18\n   * @param {import('express').Request} request\n   * @returns {void}\n   */\n  parseOptionsFromRequest(request = {}) {\n    const options = reqBody(request)?.options?.runtimeSettings || {};\n    for (const [key, value] of Object.entries(options)) {\n      if (!this.settingConfigs.hasOwnProperty(key)) continue;\n      this.set(key, value);\n    }\n    return;\n  }\n\n  /**\n   * Get a runtime setting\n   * - Will throw an error if the setting requested is not a supported runtime setting key\n   * - Will return the default value if the setting requested is not set at all\n   * @param {string} key\n   * @returns {any}\n   */\n  get(key) {\n    if (!this.settingConfigs[key])\n      throw new Error(`Invalid runtime setting: ${key}`);\n    return this.settings.hasOwnProperty(key)\n      ? this.settings[key]\n      : this.settingConfigs[key].default;\n  }\n\n  /**\n   * Set a runtime setting\n   * - Will throw an error if the setting requested is not a supported runtime setting key\n   * - Will validate the value against the setting's validate function\n   * @param {string} key\n   * @param {any} value\n   * @returns {void}\n   */\n  set(key, value = null) {\n    if (!this.settingConfigs[key])\n      throw new Error(`Invalid runtime setting: ${key}`);\n    this.settings[key] = this.settingConfigs[key].validate(value);\n  }\n}\n\nmodule.exports = RuntimeSettings;\n"
  },
  {
    "path": "collector/utils/shell.js",
    "content": "/**\n * Patch the shell environment path to ensure the PATH is properly set for the current platform.\n * On Docker, we are on Node v18 and cannot support fix-path v5.\n * So we need to use the ESM-style import() to import the fix-path module + add the strip-ansi call to patch the PATH, which is the only change between v4 and v5.\n * https://github.com/sindresorhus/fix-path/issues/6\n * @returns {Promise<{[key: string]: string}>} - Environment variables from shell\n */\nasync function patchShellEnvironmentPath() {\n  try {\n    if (process.platform === \"win32\") return process.env;\n    const { default: fixPath } = await import(\"fix-path\");\n    const { default: stripAnsi } = await import(\"strip-ansi\");\n    fixPath();\n    if (process.env.PATH) process.env.PATH = stripAnsi(process.env.PATH);\n    console.log(\"Shell environment path patched successfully.\");\n    return process.env;\n  } catch (error) {\n    console.error(\"Failed to patch shell environment path:\", error);\n    return process.env;\n  }\n}\n\nmodule.exports = {\n  patchShellEnvironmentPath,\n};\n"
  },
  {
    "path": "collector/utils/tokenizer/index.js",
    "content": "const { getEncoding } = require(\"js-tiktoken\");\n\nclass TikTokenTokenizer {\n  static MAX_KB_ESTIMATE = 10;\n  static DIVISOR = 8;\n\n  constructor() {\n    if (TikTokenTokenizer.instance) {\n      this.log(\n        \"Singleton instance already exists. Returning existing instance.\"\n      );\n      return TikTokenTokenizer.instance;\n    }\n\n    this.encoder = getEncoding(\"cl100k_base\");\n    TikTokenTokenizer.instance = this;\n    this.log(\"Initialized new TikTokenTokenizer instance.\");\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[35m[TikTokenTokenizer]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Check if the input is too long to encode\n   * this is more of a rough estimate and a sanity check to prevent\n   * CPU issues from encoding too large of strings\n   * Assumes 1 character = 2 bytes in JS\n   * @param {string} input\n   * @returns {boolean}\n   */\n  #isTooLong(input) {\n    const bytesEstimate = input.length * 2;\n    const kbEstimate = Math.floor(bytesEstimate / 1024);\n    return kbEstimate >= TikTokenTokenizer.MAX_KB_ESTIMATE;\n  }\n\n  /**\n   * Encode a string into tokens for rough token count estimation.\n   * @param {string} input\n   * @returns {number}\n   */\n  tokenizeString(input = \"\") {\n    try {\n      if (this.#isTooLong(input)) {\n        this.log(\"Input will take too long to encode - estimating\");\n        return Math.ceil(input.length / TikTokenTokenizer.DIVISOR);\n      }\n\n      return this.encoder.encode(input).length;\n    } catch (e) {\n      this.log(\"Could not tokenize string! Estimating...\", e.message, e.stack);\n      return Math.ceil(input?.length / TikTokenTokenizer.DIVISOR) || 0;\n    }\n  }\n}\n\nconst tokenizer = new TikTokenTokenizer();\nmodule.exports = {\n  /**\n   * Encode a string into tokens for rough token count estimation.\n   * @param {string} input\n   * @returns {number}\n   */\n  tokenizeString: (input) => tokenizer.tokenizeString(input),\n};\n"
  },
  {
    "path": "collector/utils/url/index.js",
    "content": "const RuntimeSettings = require(\"../runtimeSettings\");\n/**  ATTN: SECURITY RESEARCHERS\n * To Security researchers about to submit an SSRF report CVE - please don't.\n * We are aware that the code below is does not defend against any of the thousands of ways\n * you can map a hostname to another IP via tunneling, hosts editing, etc. The code below does not have intention of blocking this\n * and is simply to prevent the user from accidentally putting in non-valid websites, which is all this protects\n * since _all urls must be submitted by the user anyway_ and cannot be done with authentication and manager or admin roles.\n * If an attacker has those roles then the system is already vulnerable and this is not a primary concern.\n *\n * We have gotten this report may times, marked them as duplicate or information and continue to get them. We communicate\n * already that deployment (and security) of an instance is on the deployer and system admin deploying it. This would include\n * isolation, firewalls, and the general security of the instance.\n */\n\nconst VALID_PROTOCOLS = [\"https:\", \"http:\"];\nconst INVALID_OCTETS = [192, 172, 10, 127];\nconst runtimeSettings = new RuntimeSettings();\n\n/**\n * If an ip address is passed in the user is attempting to collector some internal service running on internal/private IP.\n * This is not a security feature and simply just prevents the user from accidentally entering invalid IP addresses.\n * Can be bypassed via COLLECTOR_ALLOW_ANY_IP environment variable.\n * @param {URL} param0\n * @param {URL['hostname']} param0.hostname\n * @returns {boolean}\n */\nfunction isInvalidIp({ hostname }) {\n  if (runtimeSettings.get(\"allowAnyIp\")) {\n    if (!runtimeSettings.get(\"seenAnyIpWarning\")) {\n      console.log(\n        \"\\x1b[33mURL IP local address restrictions have been disabled by administrator!\\x1b[0m\"\n      );\n      runtimeSettings.set(\"seenAnyIpWarning\", true);\n    }\n    return false;\n  }\n\n  const IPRegex = new RegExp(\n    /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/gi\n  );\n\n  // Not an IP address at all - passthrough\n  if (!IPRegex.test(hostname)) return false;\n  const [octetOne, ..._rest] = hostname.split(\".\");\n\n  // If fails to validate to number - abort and return as invalid.\n  if (isNaN(Number(octetOne))) return true;\n\n  // Allow localhost loopback and 0.0.0.0 for scraping convenience\n  // for locally hosted services or websites\n  if ([\"127.0.0.1\", \"0.0.0.0\"].includes(hostname)) return false;\n\n  return INVALID_OCTETS.includes(Number(octetOne));\n}\n\n/**\n * Validates a URL strictly\n * - Checks the URL forms a valid URL\n * - Checks the URL is at least HTTP(S)\n * - Checks the URL is not an internal IP - can be bypassed via COLLECTOR_ALLOW_ANY_IP\n * @param {string} url\n * @returns {boolean}\n */\nfunction validURL(url) {\n  try {\n    const destination = new URL(url);\n    if (!VALID_PROTOCOLS.includes(destination.protocol)) return false;\n    if (isInvalidIp(destination)) return false;\n    return true;\n  } catch {}\n  return false;\n}\n\n/**\n * Modifies a URL to be valid:\n * - Checks the URL is at least HTTP(S) so that protocol exists\n * - Checks the URL forms a valid URL\n * @param {string} url\n * @returns {string}\n */\nfunction validateURL(url) {\n  try {\n    let destination = url.trim();\n    // If the URL has a protocol, just pass through\n    // If the URL doesn't have a protocol, assume https://\n    if (destination.includes(\"://\"))\n      destination = new URL(destination).toString();\n    else destination = new URL(`https://${destination}`).toString();\n\n    // If the URL ends with a slash, remove it\n    return destination.endsWith(\"/\") ? destination.slice(0, -1) : destination;\n  } catch {\n    if (typeof url !== \"string\") return \"\";\n    return url.trim();\n  }\n}\n\n/**\n * Validate if a link is a valid YouTube video URL\n * - Checks youtu.be, youtube.com, m.youtube.com, music.youtube.com\n * - Embed video URLs\n * - Short URLs\n * - Live URLs\n * - Regular watch URLs\n * - Optional query parameters (including ?v parameter)\n *\n * Can be used to extract the video ID from a YouTube video URL via the returnVideoId parameter.\n * @param {string} link - The link to validate\n * @param {boolean} returnVideoId - Whether to return the video ID if the link is a valid YouTube video URL\n * @returns {boolean|string} - Whether the link is a valid YouTube video URL or the video ID if returnVideoId is true\n */\nfunction validYoutubeVideoUrl(link, returnVideoId = false) {\n  try {\n    if (!link || typeof link !== \"string\") return false;\n    let urlToValidate = link;\n\n    if (!link.startsWith(\"http://\") && !link.startsWith(\"https://\")) {\n      urlToValidate = \"https://\" + link;\n      urlToValidate = new URL(urlToValidate).toString();\n    }\n\n    const regex =\n      /^(?:https?:\\/\\/)?(?:www\\.|m\\.|music\\.)?(?:youtu\\.be\\/|youtube\\.com\\/(?:embed\\/|v\\/|watch\\?(?:.*&)?v=|(?:live\\/)?|shorts\\/))([\\w-]{11})(?:\\S+)?$/;\n    const match = urlToValidate.match(regex);\n    if (returnVideoId) return match?.[1] ?? null;\n    return !!match?.[1];\n  } catch (error) {\n    console.error(\"Error validating YouTube video URL\", error);\n    return returnVideoId ? null : false;\n  }\n}\n\nmodule.exports = {\n  validURL,\n  validateURL,\n  validYoutubeVideoUrl,\n};\n"
  },
  {
    "path": "collector/yarn.lock",
    "content": "# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n# yarn lockfile v1\n\n\n\"@anthropic-ai/sdk@^0.9.1\":\n  version \"0.9.1\"\n  resolved \"https://registry.yarnpkg.com/@anthropic-ai/sdk/-/sdk-0.9.1.tgz#b2d2b7bf05c90dce502c9a2e869066870f69ba88\"\n  integrity sha512-wa1meQ2WSfoY8Uor3EdrJq0jTiZJoKoSii2ZVWRY1oN4Tlr5s59pADg9T79FTbPe1/se5c3pBeZgJL63wmuoBA==\n  dependencies:\n    \"@types/node\" \"^18.11.18\"\n    \"@types/node-fetch\" \"^2.6.4\"\n    abort-controller \"^3.0.0\"\n    agentkeepalive \"^4.2.1\"\n    digest-fetch \"^1.3.0\"\n    form-data-encoder \"1.7.2\"\n    formdata-node \"^4.3.2\"\n    node-fetch \"^2.6.7\"\n    web-streams-polyfill \"^3.2.1\"\n\n\"@babel/code-frame@^7.0.0\":\n  version \"7.27.1\"\n  resolved \"https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be\"\n  integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==\n  dependencies:\n    \"@babel/helper-validator-identifier\" \"^7.27.1\"\n    js-tokens \"^4.0.0\"\n    picocolors \"^1.1.1\"\n\n\"@babel/helper-validator-identifier@^7.27.1\":\n  version \"7.28.5\"\n  resolved \"https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4\"\n  integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==\n\n\"@colors/colors@1.6.0\", \"@colors/colors@^1.6.0\":\n  version \"1.6.0\"\n  resolved \"https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0\"\n  integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==\n\n\"@dabh/diagnostics@^2.0.8\":\n  version \"2.0.8\"\n  resolved \"https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.8.tgz#ead97e72ca312cf0e6dd7af0d300b58993a31a5e\"\n  integrity sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==\n  dependencies:\n    \"@so-ric/colorspace\" \"^1.1.6\"\n    enabled \"2.0.x\"\n    kuler \"^2.0.0\"\n\n\"@emnapi/runtime@^1.2.0\":\n  version \"1.7.1\"\n  resolved \"https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.1.tgz#a73784e23f5d57287369c808197288b52276b791\"\n  integrity sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==\n  dependencies:\n    tslib \"^2.4.0\"\n\n\"@eslint-community/eslint-utils@^4.8.0\":\n  version \"4.9.1\"\n  resolved \"https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595\"\n  integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==\n  dependencies:\n    eslint-visitor-keys \"^3.4.3\"\n\n\"@eslint-community/regexpp@^4.12.1\":\n  version \"4.12.2\"\n  resolved \"https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b\"\n  integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==\n\n\"@eslint/config-array@^0.21.1\":\n  version \"0.21.1\"\n  resolved \"https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.1.tgz#7d1b0060fea407f8301e932492ba8c18aff29713\"\n  integrity sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==\n  dependencies:\n    \"@eslint/object-schema\" \"^2.1.7\"\n    debug \"^4.3.1\"\n    minimatch \"^3.1.2\"\n\n\"@eslint/config-helpers@^0.4.2\":\n  version \"0.4.2\"\n  resolved \"https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz#1bd006ceeb7e2e55b2b773ab318d300e1a66aeda\"\n  integrity sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==\n  dependencies:\n    \"@eslint/core\" \"^0.17.0\"\n\n\"@eslint/core@^0.17.0\":\n  version \"0.17.0\"\n  resolved \"https://registry.yarnpkg.com/@eslint/core/-/core-0.17.0.tgz#77225820413d9617509da9342190a2019e78761c\"\n  integrity sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==\n  dependencies:\n    \"@types/json-schema\" \"^7.0.15\"\n\n\"@eslint/eslintrc@^3.3.1\":\n  version \"3.3.4\"\n  resolved \"https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.4.tgz#e402b1920f7c1f5a15342caa432b1348cacbb641\"\n  integrity sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==\n  dependencies:\n    ajv \"^6.14.0\"\n    debug \"^4.3.2\"\n    espree \"^10.0.1\"\n    globals \"^14.0.0\"\n    ignore \"^5.2.0\"\n    import-fresh \"^3.2.1\"\n    js-yaml \"^4.1.1\"\n    minimatch \"^3.1.3\"\n    strip-json-comments \"^3.1.1\"\n\n\"@eslint/js@9.39.3\", \"@eslint/js@^9.0.0\":\n  version \"9.39.3\"\n  resolved \"https://registry.yarnpkg.com/@eslint/js/-/js-9.39.3.tgz#c6168736c7e0c43ead49654ed06a4bcb3833363d\"\n  integrity sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==\n\n\"@eslint/object-schema@^2.1.7\":\n  version \"2.1.7\"\n  resolved \"https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad\"\n  integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==\n\n\"@eslint/plugin-kit@^0.4.1\":\n  version \"0.4.1\"\n  resolved \"https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz#9779e3fd9b7ee33571a57435cf4335a1794a6cb2\"\n  integrity sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==\n  dependencies:\n    \"@eslint/core\" \"^0.17.0\"\n    levn \"^0.4.1\"\n\n\"@fastify/busboy@^2.0.0\":\n  version \"2.1.1\"\n  resolved \"https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d\"\n  integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==\n\n\"@huggingface/jinja@^0.2.2\":\n  version \"0.2.2\"\n  resolved \"https://registry.yarnpkg.com/@huggingface/jinja/-/jinja-0.2.2.tgz#faeb205a9d6995089bef52655ddd8245d3190627\"\n  integrity sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==\n\n\"@humanfs/core@^0.19.1\":\n  version \"0.19.1\"\n  resolved \"https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77\"\n  integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==\n\n\"@humanfs/node@^0.16.6\":\n  version \"0.16.7\"\n  resolved \"https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.7.tgz#822cb7b3a12c5a240a24f621b5a2413e27a45f26\"\n  integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==\n  dependencies:\n    \"@humanfs/core\" \"^0.19.1\"\n    \"@humanwhocodes/retry\" \"^0.4.0\"\n\n\"@humanwhocodes/module-importer@^1.0.1\":\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c\"\n  integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==\n\n\"@humanwhocodes/retry@^0.4.0\", \"@humanwhocodes/retry@^0.4.2\":\n  version \"0.4.3\"\n  resolved \"https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba\"\n  integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==\n\n\"@img/sharp-darwin-arm64@0.33.5\":\n  version \"0.33.5\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08\"\n  integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==\n  optionalDependencies:\n    \"@img/sharp-libvips-darwin-arm64\" \"1.0.4\"\n\n\"@img/sharp-darwin-x64@0.33.5\":\n  version \"0.33.5\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61\"\n  integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==\n  optionalDependencies:\n    \"@img/sharp-libvips-darwin-x64\" \"1.0.4\"\n\n\"@img/sharp-libvips-darwin-arm64@1.0.4\":\n  version \"1.0.4\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f\"\n  integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==\n\n\"@img/sharp-libvips-darwin-x64@1.0.4\":\n  version \"1.0.4\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062\"\n  integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==\n\n\"@img/sharp-libvips-linux-arm64@1.0.4\":\n  version \"1.0.4\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704\"\n  integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==\n\n\"@img/sharp-libvips-linux-arm@1.0.5\":\n  version \"1.0.5\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197\"\n  integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==\n\n\"@img/sharp-libvips-linux-s390x@1.0.4\":\n  version \"1.0.4\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce\"\n  integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==\n\n\"@img/sharp-libvips-linux-x64@1.0.4\":\n  version \"1.0.4\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0\"\n  integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==\n\n\"@img/sharp-libvips-linuxmusl-arm64@1.0.4\":\n  version \"1.0.4\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5\"\n  integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==\n\n\"@img/sharp-libvips-linuxmusl-x64@1.0.4\":\n  version \"1.0.4\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff\"\n  integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==\n\n\"@img/sharp-linux-arm64@0.33.5\":\n  version \"0.33.5\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22\"\n  integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==\n  optionalDependencies:\n    \"@img/sharp-libvips-linux-arm64\" \"1.0.4\"\n\n\"@img/sharp-linux-arm@0.33.5\":\n  version \"0.33.5\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff\"\n  integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==\n  optionalDependencies:\n    \"@img/sharp-libvips-linux-arm\" \"1.0.5\"\n\n\"@img/sharp-linux-s390x@0.33.5\":\n  version \"0.33.5\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667\"\n  integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==\n  optionalDependencies:\n    \"@img/sharp-libvips-linux-s390x\" \"1.0.4\"\n\n\"@img/sharp-linux-x64@0.33.5\":\n  version \"0.33.5\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb\"\n  integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==\n  optionalDependencies:\n    \"@img/sharp-libvips-linux-x64\" \"1.0.4\"\n\n\"@img/sharp-linuxmusl-arm64@0.33.5\":\n  version \"0.33.5\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b\"\n  integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==\n  optionalDependencies:\n    \"@img/sharp-libvips-linuxmusl-arm64\" \"1.0.4\"\n\n\"@img/sharp-linuxmusl-x64@0.33.5\":\n  version \"0.33.5\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48\"\n  integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==\n  optionalDependencies:\n    \"@img/sharp-libvips-linuxmusl-x64\" \"1.0.4\"\n\n\"@img/sharp-wasm32@0.33.5\":\n  version \"0.33.5\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1\"\n  integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==\n  dependencies:\n    \"@emnapi/runtime\" \"^1.2.0\"\n\n\"@img/sharp-win32-ia32@0.33.5\":\n  version \"0.33.5\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9\"\n  integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==\n\n\"@img/sharp-win32-x64@0.33.5\":\n  version \"0.33.5\"\n  resolved \"https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342\"\n  integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==\n\n\"@isaacs/cliui@^8.0.2\":\n  version \"8.0.2\"\n  resolved \"https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550\"\n  integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==\n  dependencies:\n    string-width \"^5.1.2\"\n    string-width-cjs \"npm:string-width@^4.2.0\"\n    strip-ansi \"^7.0.1\"\n    strip-ansi-cjs \"npm:strip-ansi@^6.0.1\"\n    wrap-ansi \"^8.1.0\"\n    wrap-ansi-cjs \"npm:wrap-ansi@^7.0.0\"\n\n\"@langchain/community@^0.2.23\":\n  version \"0.2.33\"\n  resolved \"https://registry.yarnpkg.com/@langchain/community/-/community-0.2.33.tgz#c3c920492470196d0f560552cc95954fccdde0e8\"\n  integrity sha512-YsytROnBYoPqtUcV2In+afyLACzxwFrYRn7EBKYL7XWl3XNwrT85U1+nLM5b+MOjXvg9YfJSjrs1Tlbfy4st8g==\n  dependencies:\n    \"@langchain/core\" \">=0.2.21 <0.3.0\"\n    \"@langchain/openai\" \">=0.2.0 <0.3.0\"\n    binary-extensions \"^2.2.0\"\n    expr-eval \"^2.0.2\"\n    flat \"^5.0.2\"\n    js-yaml \"^4.1.0\"\n    langchain \"~0.2.3\"\n    langsmith \"~0.1.30\"\n    uuid \"^10.0.0\"\n    zod \"^3.22.3\"\n    zod-to-json-schema \"^3.22.5\"\n\n\"@langchain/community@~0.0.47\":\n  version \"0.0.57\"\n  resolved \"https://registry.yarnpkg.com/@langchain/community/-/community-0.0.57.tgz#9d77c5acb74a4a8ec01d2cefb71dcd4088701c44\"\n  integrity sha512-tib4UJNkyA4TPNsTNChiBtZmThVJBr7X/iooSmKeCr+yUEha2Yxly3A4OAO95Vlpj4Q+od8HAfCbZih/1XqAMw==\n  dependencies:\n    \"@langchain/core\" \"~0.1.60\"\n    \"@langchain/openai\" \"~0.0.28\"\n    expr-eval \"^2.0.2\"\n    flat \"^5.0.2\"\n    langsmith \"~0.1.1\"\n    uuid \"^9.0.0\"\n    zod \"^3.22.3\"\n    zod-to-json-schema \"^3.22.5\"\n\n\"@langchain/core@>0.1.56 <0.3.0\", \"@langchain/core@>0.2.0 <0.3.0\", \"@langchain/core@>=0.2.21 <0.3.0\", \"@langchain/core@>=0.2.26 <0.3.0\":\n  version \"0.2.36\"\n  resolved \"https://registry.yarnpkg.com/@langchain/core/-/core-0.2.36.tgz#75754c33aa5b9310dcf117047374a1ae011005a4\"\n  integrity sha512-qHLvScqERDeH7y2cLuJaSAlMwg3f/3Oc9nayRSXRU2UuaK/SOhI42cxiPLj1FnuHJSmN0rBQFkrLx02gI4mcVg==\n  dependencies:\n    ansi-styles \"^5.0.0\"\n    camelcase \"6\"\n    decamelize \"1.2.0\"\n    js-tiktoken \"^1.0.12\"\n    langsmith \"^0.1.56-rc.1\"\n    mustache \"^4.2.0\"\n    p-queue \"^6.6.2\"\n    p-retry \"4\"\n    uuid \"^10.0.0\"\n    zod \"^3.22.4\"\n    zod-to-json-schema \"^3.22.3\"\n\n\"@langchain/core@~0.1.60\":\n  version \"0.1.63\"\n  resolved \"https://registry.yarnpkg.com/@langchain/core/-/core-0.1.63.tgz#33cc48877739e9fdb5885fbd4b16fd08d1597050\"\n  integrity sha512-+fjyYi8wy6x1P+Ee1RWfIIEyxd9Ee9jksEwvrggPwwI/p45kIDTdYTblXsM13y4mNWTiACyLSdbwnPaxxdoz+w==\n  dependencies:\n    ansi-styles \"^5.0.0\"\n    camelcase \"6\"\n    decamelize \"1.2.0\"\n    js-tiktoken \"^1.0.12\"\n    langsmith \"~0.1.7\"\n    ml-distance \"^4.0.0\"\n    mustache \"^4.2.0\"\n    p-queue \"^6.6.2\"\n    p-retry \"4\"\n    uuid \"^9.0.0\"\n    zod \"^3.22.4\"\n    zod-to-json-schema \"^3.22.3\"\n\n\"@langchain/openai@>=0.1.0 <0.3.0\", \"@langchain/openai@>=0.2.0 <0.3.0\":\n  version \"0.2.11\"\n  resolved \"https://registry.yarnpkg.com/@langchain/openai/-/openai-0.2.11.tgz#b1a0403eb5db8133bb4ff41fe0680e727b78ddfc\"\n  integrity sha512-Pu8+WfJojCgSf0bAsXb4AjqvcDyAWyoEB1AoCRNACgEnBWZuitz3hLwCo9I+6hAbeg3QJ37g82yKcmvKAg1feg==\n  dependencies:\n    \"@langchain/core\" \">=0.2.26 <0.3.0\"\n    js-tiktoken \"^1.0.12\"\n    openai \"^4.57.3\"\n    zod \"^3.22.4\"\n    zod-to-json-schema \"^3.22.3\"\n\n\"@langchain/openai@~0.0.28\":\n  version \"0.0.34\"\n  resolved \"https://registry.yarnpkg.com/@langchain/openai/-/openai-0.0.34.tgz#36c9bca0721ab9f7e5d40927e7c0429cacbd5b56\"\n  integrity sha512-M+CW4oXle5fdoz2T2SwdOef8pl3/1XmUx1vjn2mXUVM/128aO0l23FMF0SNBsAbRV6P+p/TuzjodchJbi0Ht/A==\n  dependencies:\n    \"@langchain/core\" \">0.1.56 <0.3.0\"\n    js-tiktoken \"^1.0.12\"\n    openai \"^4.41.1\"\n    zod \"^3.22.4\"\n    zod-to-json-schema \"^3.22.3\"\n\n\"@langchain/textsplitters@~0.0.0\":\n  version \"0.0.3\"\n  resolved \"https://registry.yarnpkg.com/@langchain/textsplitters/-/textsplitters-0.0.3.tgz#1a3cc93dd2ab330edb225400ded190a22fea14e3\"\n  integrity sha512-cXWgKE3sdWLSqAa8ykbCcUsUF1Kyr5J3HOWYGuobhPEycXW4WI++d5DhzdpL238mzoEXTi90VqfSCra37l5YqA==\n  dependencies:\n    \"@langchain/core\" \">0.2.0 <0.3.0\"\n    js-tiktoken \"^1.0.12\"\n\n\"@pkgjs/parseargs@^0.11.0\":\n  version \"0.11.0\"\n  resolved \"https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33\"\n  integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==\n\n\"@pkgr/core@^0.2.9\":\n  version \"0.2.9\"\n  resolved \"https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b\"\n  integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==\n\n\"@protobufjs/aspromise@^1.1.1\", \"@protobufjs/aspromise@^1.1.2\":\n  version \"1.1.2\"\n  resolved \"https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf\"\n  integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==\n\n\"@protobufjs/base64@^1.1.2\":\n  version \"1.1.2\"\n  resolved \"https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735\"\n  integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==\n\n\"@protobufjs/codegen@^2.0.4\":\n  version \"2.0.4\"\n  resolved \"https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb\"\n  integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==\n\n\"@protobufjs/eventemitter@^1.1.0\":\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70\"\n  integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==\n\n\"@protobufjs/fetch@^1.1.0\":\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45\"\n  integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==\n  dependencies:\n    \"@protobufjs/aspromise\" \"^1.1.1\"\n    \"@protobufjs/inquire\" \"^1.1.0\"\n\n\"@protobufjs/float@^1.0.2\":\n  version \"1.0.2\"\n  resolved \"https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1\"\n  integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==\n\n\"@protobufjs/inquire@^1.1.0\":\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089\"\n  integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==\n\n\"@protobufjs/path@^1.1.2\":\n  version \"1.1.2\"\n  resolved \"https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d\"\n  integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==\n\n\"@protobufjs/pool@^1.1.0\":\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54\"\n  integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==\n\n\"@protobufjs/utf8@^1.1.0\":\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570\"\n  integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==\n\n\"@puppeteer/browsers@1.8.0\":\n  version \"1.8.0\"\n  resolved \"https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-1.8.0.tgz#fb6ee61de15e7f0e67737aea9f9bab1512dbd7d8\"\n  integrity sha512-TkRHIV6k2D8OlUe8RtG+5jgOF/H98Myx0M6AOafC8DdNVOFiBSFa5cpRDtpm8LXOa9sVwe0+e6Q3FC56X/DZfg==\n  dependencies:\n    debug \"4.3.4\"\n    extract-zip \"2.0.1\"\n    progress \"2.0.3\"\n    proxy-agent \"6.3.1\"\n    tar-fs \"3.0.4\"\n    unbzip2-stream \"1.4.3\"\n    yargs \"17.7.2\"\n\n\"@selderee/plugin-htmlparser2@^0.11.0\":\n  version \"0.11.0\"\n  resolved \"https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz#d5b5e29a7ba6d3958a1972c7be16f4b2c188c517\"\n  integrity sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==\n  dependencies:\n    domhandler \"^5.0.3\"\n    selderee \"^0.11.0\"\n\n\"@so-ric/colorspace@^1.1.6\":\n  version \"1.1.6\"\n  resolved \"https://registry.yarnpkg.com/@so-ric/colorspace/-/colorspace-1.1.6.tgz#62515d8b9f27746b76950a83bde1af812d91923b\"\n  integrity sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==\n  dependencies:\n    color \"^5.0.2\"\n    text-hex \"1.0.x\"\n\n\"@tokenizer/token@^0.3.0\":\n  version \"0.3.0\"\n  resolved \"https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276\"\n  integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==\n\n\"@tootallnate/quickjs-emscripten@^0.23.0\":\n  version \"0.23.0\"\n  resolved \"https://registry.yarnpkg.com/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz#db4ecfd499a9765ab24002c3b696d02e6d32a12c\"\n  integrity sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==\n\n\"@types/estree@^1.0.6\":\n  version \"1.0.8\"\n  resolved \"https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e\"\n  integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==\n\n\"@types/json-schema@^7.0.15\":\n  version \"7.0.15\"\n  resolved \"https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841\"\n  integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==\n\n\"@types/long@^4.0.1\":\n  version \"4.0.2\"\n  resolved \"https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a\"\n  integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==\n\n\"@types/mailparser@^3.0.0\":\n  version \"3.4.6\"\n  resolved \"https://registry.yarnpkg.com/@types/mailparser/-/mailparser-3.4.6.tgz#fcca99fe9f919f3da691a0bf5e3022c6283c3068\"\n  integrity sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q==\n  dependencies:\n    \"@types/node\" \"*\"\n    iconv-lite \"^0.6.3\"\n\n\"@types/node-fetch@^2.6.4\":\n  version \"2.6.13\"\n  resolved \"https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.13.tgz#e0c9b7b5edbdb1b50ce32c127e85e880872d56ee\"\n  integrity sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==\n  dependencies:\n    \"@types/node\" \"*\"\n    form-data \"^4.0.4\"\n\n\"@types/node@*\", \"@types/node@>=13.7.0\":\n  version \"24.10.1\"\n  resolved \"https://registry.yarnpkg.com/@types/node/-/node-24.10.1.tgz#91e92182c93db8bd6224fca031e2370cef9a8f01\"\n  integrity sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==\n  dependencies:\n    undici-types \"~7.16.0\"\n\n\"@types/node@^18.11.18\":\n  version \"18.19.130\"\n  resolved \"https://registry.yarnpkg.com/@types/node/-/node-18.19.130.tgz#da4c6324793a79defb7a62cba3947ec5add00d59\"\n  integrity sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==\n  dependencies:\n    undici-types \"~5.26.4\"\n\n\"@types/retry@0.12.0\":\n  version \"0.12.0\"\n  resolved \"https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d\"\n  integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==\n\n\"@types/triple-beam@^1.3.2\":\n  version \"1.3.5\"\n  resolved \"https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c\"\n  integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==\n\n\"@types/uuid@^10.0.0\":\n  version \"10.0.0\"\n  resolved \"https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d\"\n  integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==\n\n\"@types/yauzl@^2.9.1\":\n  version \"2.10.3\"\n  resolved \"https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999\"\n  integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==\n  dependencies:\n    \"@types/node\" \"*\"\n\n\"@xenova/transformers@^2.14.0\":\n  version \"2.17.2\"\n  resolved \"https://registry.yarnpkg.com/@xenova/transformers/-/transformers-2.17.2.tgz#7448d73b90f67bced66f39fe2dd656adc891fde5\"\n  integrity sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==\n  dependencies:\n    \"@huggingface/jinja\" \"^0.2.2\"\n    onnxruntime-web \"1.14.0\"\n    sharp \"^0.32.0\"\n  optionalDependencies:\n    onnxruntime-node \"1.14.0\"\n\n\"@xmldom/xmldom@^0.8.10\", \"@xmldom/xmldom@^0.8.6\":\n  version \"0.8.11\"\n  resolved \"https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.11.tgz#b79de2d67389734c57c52595f7a7305e30c2d608\"\n  integrity sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==\n\n\"@zone-eu/mailsplit@5.4.7\":\n  version \"5.4.7\"\n  resolved \"https://registry.yarnpkg.com/@zone-eu/mailsplit/-/mailsplit-5.4.7.tgz#ad86fe08222883418f33cf02d57025de02d2eb38\"\n  integrity sha512-jApX86aDgolMz08pP20/J2zcns02NSK3zSiYouf01QQg4250L+GUAWSWicmS7eRvs+Z7wP7QfXrnkaTBGrIpwQ==\n  dependencies:\n    libbase64 \"1.3.0\"\n    libmime \"5.3.7\"\n    libqp \"2.1.1\"\n\nabort-controller@^3.0.0:\n  version \"3.0.0\"\n  resolved \"https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392\"\n  integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==\n  dependencies:\n    event-target-shim \"^5.0.0\"\n\naccepts@~1.3.8:\n  version \"1.3.8\"\n  resolved \"https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e\"\n  integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==\n  dependencies:\n    mime-types \"~2.1.34\"\n    negotiator \"0.6.3\"\n\nacorn-jsx@^5.3.2:\n  version \"5.3.2\"\n  resolved \"https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937\"\n  integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==\n\nacorn@^8.15.0:\n  version \"8.16.0\"\n  resolved \"https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a\"\n  integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==\n\nacorn@^8.8.0:\n  version \"8.15.0\"\n  resolved \"https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816\"\n  integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==\n\nadm-zip@^0.5.10:\n  version \"0.5.16\"\n  resolved \"https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909\"\n  integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==\n\nagent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.2:\n  version \"7.1.4\"\n  resolved \"https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8\"\n  integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==\n\nagentkeepalive@^4.2.1:\n  version \"4.6.0\"\n  resolved \"https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a\"\n  integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==\n  dependencies:\n    humanize-ms \"^1.2.1\"\n\najv@^6.12.4, ajv@^6.14.0:\n  version \"6.14.0\"\n  resolved \"https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a\"\n  integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==\n  dependencies:\n    fast-deep-equal \"^3.1.1\"\n    fast-json-stable-stringify \"^2.0.0\"\n    json-schema-traverse \"^0.4.1\"\n    uri-js \"^4.2.2\"\n\nansi-regex@^5.0.1:\n  version \"5.0.1\"\n  resolved \"https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304\"\n  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==\n\nansi-regex@^6.0.1:\n  version \"6.2.2\"\n  resolved \"https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1\"\n  integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==\n\nansi-styles@^4.0.0, ansi-styles@^4.1.0:\n  version \"4.3.0\"\n  resolved \"https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937\"\n  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==\n  dependencies:\n    color-convert \"^2.0.1\"\n\nansi-styles@^5.0.0:\n  version \"5.2.0\"\n  resolved \"https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b\"\n  integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==\n\nanymatch@~3.1.2:\n  version \"3.1.3\"\n  resolved \"https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e\"\n  integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==\n  dependencies:\n    normalize-path \"^3.0.0\"\n    picomatch \"^2.0.4\"\n\nargparse@^2.0.1:\n  version \"2.0.1\"\n  resolved \"https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38\"\n  integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==\n\nargparse@~1.0.3:\n  version \"1.0.10\"\n  resolved \"https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911\"\n  integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==\n  dependencies:\n    sprintf-js \"~1.0.2\"\n\narray-flatten@1.1.1:\n  version \"1.1.1\"\n  resolved \"https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2\"\n  integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==\n\narray-hyper-unique@^2.1.4:\n  version \"2.1.6\"\n  resolved \"https://registry.yarnpkg.com/array-hyper-unique/-/array-hyper-unique-2.1.6.tgz#429412fd63b7bd7c920f6cdbf60d1dd292855b2e\"\n  integrity sha512-BdlHRqjKSYs88WFaVNVEc6Kv8ln/FdzCKPbcDPuWs4/EXkQFhnjc8TyR7hnPxRjcjo5LKOhUMGUWpAqRgeJvpA==\n  dependencies:\n    deep-eql \"= 4.0.0\"\n    lodash \"^4.17.21\"\n\nast-types@^0.13.4:\n  version \"0.13.4\"\n  resolved \"https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782\"\n  integrity sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==\n  dependencies:\n    tslib \"^2.0.1\"\n\nasync@^3.2.3:\n  version \"3.2.6\"\n  resolved \"https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce\"\n  integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==\n\nasynckit@^0.4.0:\n  version \"0.4.0\"\n  resolved \"https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79\"\n  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==\n\navailable-typed-arrays@^1.0.7:\n  version \"1.0.7\"\n  resolved \"https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846\"\n  integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==\n  dependencies:\n    possible-typed-array-names \"^1.0.0\"\n\nb4a@^1.6.4:\n  version \"1.7.3\"\n  resolved \"https://registry.yarnpkg.com/b4a/-/b4a-1.7.3.tgz#24cf7ccda28f5465b66aec2bac69e32809bf112f\"\n  integrity sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==\n\nbalanced-match@^1.0.0:\n  version \"1.0.2\"\n  resolved \"https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee\"\n  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==\n\nbare-events@^2.5.4, bare-events@^2.7.0:\n  version \"2.8.2\"\n  resolved \"https://registry.yarnpkg.com/bare-events/-/bare-events-2.8.2.tgz#7b3e10bd8e1fc80daf38bb516921678f566ab89f\"\n  integrity sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==\n\nbare-fs@^4.0.1:\n  version \"4.5.1\"\n  resolved \"https://registry.yarnpkg.com/bare-fs/-/bare-fs-4.5.1.tgz#498a20a332d4a7f0b310eb89b8d2319041aa1eef\"\n  integrity sha512-zGUCsm3yv/ePt2PHNbVxjjn0nNB1MkIaR4wOCxJ2ig5pCf5cCVAYJXVhQg/3OhhJV6DB1ts7Hv0oUaElc2TPQg==\n  dependencies:\n    bare-events \"^2.5.4\"\n    bare-path \"^3.0.0\"\n    bare-stream \"^2.6.4\"\n    bare-url \"^2.2.2\"\n    fast-fifo \"^1.3.2\"\n\nbare-os@^3.0.1:\n  version \"3.6.2\"\n  resolved \"https://registry.yarnpkg.com/bare-os/-/bare-os-3.6.2.tgz#b3c4f5ad5e322c0fd0f3c29fc97d19009e2796e5\"\n  integrity sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==\n\nbare-path@^3.0.0:\n  version \"3.0.0\"\n  resolved \"https://registry.yarnpkg.com/bare-path/-/bare-path-3.0.0.tgz#b59d18130ba52a6af9276db3e96a2e3d3ea52178\"\n  integrity sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==\n  dependencies:\n    bare-os \"^3.0.1\"\n\nbare-stream@^2.6.4:\n  version \"2.7.0\"\n  resolved \"https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.7.0.tgz#5b9e7dd0a354d06e82d6460c426728536c35d789\"\n  integrity sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==\n  dependencies:\n    streamx \"^2.21.0\"\n\nbare-url@^2.2.2:\n  version \"2.3.2\"\n  resolved \"https://registry.yarnpkg.com/bare-url/-/bare-url-2.3.2.tgz#4aef382efa662b2180a6fe4ca07a71b39bdf7ca3\"\n  integrity sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==\n  dependencies:\n    bare-path \"^3.0.0\"\n\nbase-64@^0.1.0:\n  version \"0.1.0\"\n  resolved \"https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb\"\n  integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==\n\nbase64-js@^1.3.1, base64-js@^1.5.1:\n  version \"1.5.1\"\n  resolved \"https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a\"\n  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==\n\nbasic-ftp@^5.0.2:\n  version \"5.0.5\"\n  resolved \"https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.5.tgz#14a474f5fffecca1f4f406f1c26b18f800225ac0\"\n  integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==\n\nbinary-extensions@^2.0.0, binary-extensions@^2.2.0:\n  version \"2.3.0\"\n  resolved \"https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522\"\n  integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==\n\nbinary-search@^1.3.5:\n  version \"1.3.6\"\n  resolved \"https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c\"\n  integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==\n\nbl@^1.0.0:\n  version \"1.2.3\"\n  resolved \"https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7\"\n  integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==\n  dependencies:\n    readable-stream \"^2.3.5\"\n    safe-buffer \"^5.1.1\"\n\nbl@^4.0.3:\n  version \"4.1.0\"\n  resolved \"https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a\"\n  integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==\n  dependencies:\n    buffer \"^5.5.0\"\n    inherits \"^2.0.4\"\n    readable-stream \"^3.4.0\"\n\nbluebird@^3.7.2:\n  version \"3.7.2\"\n  resolved \"https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f\"\n  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==\n\nbluebird@~3.4.0:\n  version \"3.4.7\"\n  resolved \"https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3\"\n  integrity sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==\n\nbmp-js@^0.1.0:\n  version \"0.1.0\"\n  resolved \"https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233\"\n  integrity sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==\n\nbody-parser@^1.20.3, body-parser@~1.20.3:\n  version \"1.20.4\"\n  resolved \"https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.4.tgz#f8e20f4d06ca8a50a71ed329c15dccad1cdc547f\"\n  integrity sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==\n  dependencies:\n    bytes \"~3.1.2\"\n    content-type \"~1.0.5\"\n    debug \"2.6.9\"\n    depd \"2.0.0\"\n    destroy \"~1.2.0\"\n    http-errors \"~2.0.1\"\n    iconv-lite \"~0.4.24\"\n    on-finished \"~2.4.1\"\n    qs \"~6.14.0\"\n    raw-body \"~2.5.3\"\n    type-is \"~1.6.18\"\n    unpipe \"~1.0.0\"\n\nboolbase@^1.0.0:\n  version \"1.0.0\"\n  resolved \"https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e\"\n  integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==\n\nbrace-expansion@^1.1.7:\n  version \"1.1.12\"\n  resolved \"https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843\"\n  integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==\n  dependencies:\n    balanced-match \"^1.0.0\"\n    concat-map \"0.0.1\"\n\nbrace-expansion@^2.0.1:\n  version \"2.0.2\"\n  resolved \"https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7\"\n  integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==\n  dependencies:\n    balanced-match \"^1.0.0\"\n\nbraces@~3.0.2:\n  version \"3.0.3\"\n  resolved \"https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789\"\n  integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==\n  dependencies:\n    fill-range \"^7.1.1\"\n\nbuffer-alloc-unsafe@^1.1.0:\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0\"\n  integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==\n\nbuffer-alloc@^1.2.0:\n  version \"1.2.0\"\n  resolved \"https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec\"\n  integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==\n  dependencies:\n    buffer-alloc-unsafe \"^1.1.0\"\n    buffer-fill \"^1.0.0\"\n\nbuffer-crc32@~0.2.3:\n  version \"0.2.13\"\n  resolved \"https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242\"\n  integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==\n\nbuffer-fill@^1.0.0:\n  version \"1.0.0\"\n  resolved \"https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c\"\n  integrity sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==\n\nbuffer@^5.2.1, buffer@^5.5.0:\n  version \"5.7.1\"\n  resolved \"https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0\"\n  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==\n  dependencies:\n    base64-js \"^1.3.1\"\n    ieee754 \"^1.1.13\"\n\nbuffer@^6.0.3:\n  version \"6.0.3\"\n  resolved \"https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6\"\n  integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==\n  dependencies:\n    base64-js \"^1.3.1\"\n    ieee754 \"^1.2.1\"\n\nbytes@~3.1.2:\n  version \"3.1.2\"\n  resolved \"https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5\"\n  integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==\n\ncall-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:\n  version \"1.0.2\"\n  resolved \"https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6\"\n  integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==\n  dependencies:\n    es-errors \"^1.3.0\"\n    function-bind \"^1.1.2\"\n\ncall-bind@^1.0.8:\n  version \"1.0.8\"\n  resolved \"https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c\"\n  integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==\n  dependencies:\n    call-bind-apply-helpers \"^1.0.0\"\n    es-define-property \"^1.0.0\"\n    get-intrinsic \"^1.2.4\"\n    set-function-length \"^1.2.2\"\n\ncall-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4:\n  version \"1.0.4\"\n  resolved \"https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a\"\n  integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==\n  dependencies:\n    call-bind-apply-helpers \"^1.0.2\"\n    get-intrinsic \"^1.3.0\"\n\ncallsites@^3.0.0:\n  version \"3.1.0\"\n  resolved \"https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73\"\n  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==\n\ncamelcase@6:\n  version \"6.3.0\"\n  resolved \"https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a\"\n  integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==\n\nchalk@^4.0.0:\n  version \"4.1.2\"\n  resolved \"https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01\"\n  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==\n  dependencies:\n    ansi-styles \"^4.1.0\"\n    supports-color \"^7.1.0\"\n\ncharenc@0.0.2:\n  version \"0.0.2\"\n  resolved \"https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667\"\n  integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==\n\nchokidar@^3.5.2:\n  version \"3.6.0\"\n  resolved \"https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b\"\n  integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==\n  dependencies:\n    anymatch \"~3.1.2\"\n    braces \"~3.0.2\"\n    glob-parent \"~5.1.2\"\n    is-binary-path \"~2.1.0\"\n    is-glob \"~4.0.1\"\n    normalize-path \"~3.0.0\"\n    readdirp \"~3.6.0\"\n  optionalDependencies:\n    fsevents \"~2.3.2\"\n\nchownr@^1.1.1:\n  version \"1.1.4\"\n  resolved \"https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b\"\n  integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==\n\nchromium-bidi@0.4.33:\n  version \"0.4.33\"\n  resolved \"https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.4.33.tgz#9a9aba5a5b07118c8e7d6405f8ee79f47418dd1d\"\n  integrity sha512-IxoFM5WGQOIAd95qrSXzJUv4eXIrh+RvU3rwwqIiwYuvfE7U/Llj4fejbsJnjJMUYCuGtVQsY2gv7oGl4aTNSQ==\n  dependencies:\n    mitt \"3.0.1\"\n    urlpattern-polyfill \"9.0.0\"\n\ncliui@^8.0.1:\n  version \"8.0.1\"\n  resolved \"https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa\"\n  integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==\n  dependencies:\n    string-width \"^4.2.0\"\n    strip-ansi \"^6.0.1\"\n    wrap-ansi \"^7.0.0\"\n\ncolor-convert@^2.0.1:\n  version \"2.0.1\"\n  resolved \"https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3\"\n  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==\n  dependencies:\n    color-name \"~1.1.4\"\n\ncolor-convert@^3.1.3:\n  version \"3.1.3\"\n  resolved \"https://registry.yarnpkg.com/color-convert/-/color-convert-3.1.3.tgz#db6627b97181cb8facdfce755ae26f97ab0711f1\"\n  integrity sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==\n  dependencies:\n    color-name \"^2.0.0\"\n\ncolor-name@^1.0.0, color-name@~1.1.4:\n  version \"1.1.4\"\n  resolved \"https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2\"\n  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==\n\ncolor-name@^2.0.0:\n  version \"2.1.0\"\n  resolved \"https://registry.yarnpkg.com/color-name/-/color-name-2.1.0.tgz#0b677385c1c4b4edfdeaf77e38fa338e3a40b693\"\n  integrity sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==\n\ncolor-string@^1.9.0:\n  version \"1.9.1\"\n  resolved \"https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4\"\n  integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==\n  dependencies:\n    color-name \"^1.0.0\"\n    simple-swizzle \"^0.2.2\"\n\ncolor-string@^2.1.3:\n  version \"2.1.4\"\n  resolved \"https://registry.yarnpkg.com/color-string/-/color-string-2.1.4.tgz#9dcf566ff976e23368c8bd673f5c35103ab41058\"\n  integrity sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==\n  dependencies:\n    color-name \"^2.0.0\"\n\ncolor@^4.2.3:\n  version \"4.2.3\"\n  resolved \"https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a\"\n  integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==\n  dependencies:\n    color-convert \"^2.0.1\"\n    color-string \"^1.9.0\"\n\ncolor@^5.0.2:\n  version \"5.0.3\"\n  resolved \"https://registry.yarnpkg.com/color/-/color-5.0.3.tgz#f79390b1b778e222ffbb54304d3dbeaef633f97f\"\n  integrity sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==\n  dependencies:\n    color-convert \"^3.1.3\"\n    color-string \"^2.1.3\"\n\ncombined-stream@^1.0.8:\n  version \"1.0.8\"\n  resolved \"https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f\"\n  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==\n  dependencies:\n    delayed-stream \"~1.0.0\"\n\ncommander@^10.0.1:\n  version \"10.0.1\"\n  resolved \"https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06\"\n  integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==\n\ncommander@^2.8.1:\n  version \"2.20.3\"\n  resolved \"https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33\"\n  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==\n\nconcat-map@0.0.1:\n  version \"0.0.1\"\n  resolved \"https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b\"\n  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==\n\ncontent-disposition@~0.5.4:\n  version \"0.5.4\"\n  resolved \"https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe\"\n  integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==\n  dependencies:\n    safe-buffer \"5.2.1\"\n\ncontent-type@~1.0.4, content-type@~1.0.5:\n  version \"1.0.5\"\n  resolved \"https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918\"\n  integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==\n\ncookie-signature@~1.0.6:\n  version \"1.0.7\"\n  resolved \"https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454\"\n  integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==\n\ncookie@~0.7.1:\n  version \"0.7.2\"\n  resolved \"https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7\"\n  integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==\n\ncore-util-is@~1.0.0:\n  version \"1.0.3\"\n  resolved \"https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85\"\n  integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==\n\ncors@^2.8.5:\n  version \"2.8.5\"\n  resolved \"https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29\"\n  integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==\n  dependencies:\n    object-assign \"^4\"\n    vary \"^1\"\n\ncosmiconfig@8.3.6:\n  version \"8.3.6\"\n  resolved \"https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3\"\n  integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==\n  dependencies:\n    import-fresh \"^3.3.0\"\n    js-yaml \"^4.1.0\"\n    parse-json \"^5.2.0\"\n    path-type \"^4.0.0\"\n\ncrlf-normalize@^1.0.19:\n  version \"1.0.20\"\n  resolved \"https://registry.yarnpkg.com/crlf-normalize/-/crlf-normalize-1.0.20.tgz#0b3105d3de807bce8a7599113235d725fe9361a8\"\n  integrity sha512-h/rBerTd3YHQGfv7tNT25mfhWvRq2BBLCZZ80GFarFxf6HQGbpW6iqDL3N+HBLpjLfAdcBXfWAzVlLfHkRUQBQ==\n  dependencies:\n    ts-type \">=2\"\n\ncross-env@^7.0.3:\n  version \"7.0.3\"\n  resolved \"https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf\"\n  integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==\n  dependencies:\n    cross-spawn \"^7.0.1\"\n\ncross-fetch@4.0.0:\n  version \"4.0.0\"\n  resolved \"https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983\"\n  integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==\n  dependencies:\n    node-fetch \"^2.6.12\"\n\ncross-spawn@^7.0.1, cross-spawn@^7.0.3, cross-spawn@^7.0.6:\n  version \"7.0.6\"\n  resolved \"https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f\"\n  integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==\n  dependencies:\n    path-key \"^3.1.0\"\n    shebang-command \"^2.0.0\"\n    which \"^2.0.1\"\n\ncrypt@0.0.2:\n  version \"0.0.2\"\n  resolved \"https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b\"\n  integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==\n\ncss-select@^5.1.0:\n  version \"5.2.2\"\n  resolved \"https://registry.yarnpkg.com/css-select/-/css-select-5.2.2.tgz#01b6e8d163637bb2dd6c982ca4ed65863682786e\"\n  integrity sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==\n  dependencies:\n    boolbase \"^1.0.0\"\n    css-what \"^6.1.0\"\n    domhandler \"^5.0.2\"\n    domutils \"^3.0.1\"\n    nth-check \"^2.0.1\"\n\ncss-what@^6.1.0:\n  version \"6.2.2\"\n  resolved \"https://registry.yarnpkg.com/css-what/-/css-what-6.2.2.tgz#cdcc8f9b6977719fdfbd1de7aec24abf756b9dea\"\n  integrity sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==\n\ndata-uri-to-buffer@^6.0.2:\n  version \"6.0.2\"\n  resolved \"https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b\"\n  integrity sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==\n\ndebug@2.6.9:\n  version \"2.6.9\"\n  resolved \"https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f\"\n  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==\n  dependencies:\n    ms \"2.0.0\"\n\ndebug@4, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:\n  version \"4.4.3\"\n  resolved \"https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a\"\n  integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==\n  dependencies:\n    ms \"^2.1.3\"\n\ndebug@4.3.4:\n  version \"4.3.4\"\n  resolved \"https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865\"\n  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==\n  dependencies:\n    ms \"2.1.2\"\n\ndebug@^3.2.7:\n  version \"3.2.7\"\n  resolved \"https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a\"\n  integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==\n  dependencies:\n    ms \"^2.1.1\"\n\ndecamelize@1.2.0:\n  version \"1.2.0\"\n  resolved \"https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290\"\n  integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==\n\ndecompress-response@^6.0.0:\n  version \"6.0.0\"\n  resolved \"https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc\"\n  integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==\n  dependencies:\n    mimic-response \"^3.1.0\"\n\ndecompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1:\n  version \"4.1.1\"\n  resolved \"https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1\"\n  integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==\n  dependencies:\n    file-type \"^5.2.0\"\n    is-stream \"^1.1.0\"\n    tar-stream \"^1.5.2\"\n\ndecompress-tarbz2@^4.0.0:\n  version \"4.1.1\"\n  resolved \"https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b\"\n  integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==\n  dependencies:\n    decompress-tar \"^4.1.0\"\n    file-type \"^6.1.0\"\n    is-stream \"^1.1.0\"\n    seek-bzip \"^1.0.5\"\n    unbzip2-stream \"^1.0.9\"\n\ndecompress-targz@^4.0.0:\n  version \"4.1.1\"\n  resolved \"https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee\"\n  integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==\n  dependencies:\n    decompress-tar \"^4.1.1\"\n    file-type \"^5.2.0\"\n    is-stream \"^1.1.0\"\n\ndecompress-unzip@^4.0.1:\n  version \"4.0.1\"\n  resolved \"https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69\"\n  integrity sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==\n  dependencies:\n    file-type \"^3.8.0\"\n    get-stream \"^2.2.0\"\n    pify \"^2.3.0\"\n    yauzl \"^2.4.2\"\n\ndecompress@^4.2.1:\n  version \"4.2.1\"\n  resolved \"https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118\"\n  integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==\n  dependencies:\n    decompress-tar \"^4.0.0\"\n    decompress-tarbz2 \"^4.0.0\"\n    decompress-targz \"^4.0.0\"\n    decompress-unzip \"^4.0.1\"\n    graceful-fs \"^4.1.10\"\n    make-dir \"^1.0.0\"\n    pify \"^2.3.0\"\n    strip-dirs \"^2.0.0\"\n\n\"deep-eql@= 4.0.0\":\n  version \"4.0.0\"\n  resolved \"https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.0.0.tgz#c70af2713a4e18d9c2c1203ff9d11abbd51c8fbd\"\n  integrity sha512-GxJC5MOg2KyQlv6WiUF/VAnMj4MWnYiXo4oLgeptOELVoknyErb4Z8+5F/IM/K4g9/80YzzatxmWcyRwUseH0A==\n  dependencies:\n    type-detect \"^4.0.0\"\n\ndeep-extend@^0.6.0:\n  version \"0.6.0\"\n  resolved \"https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac\"\n  integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==\n\ndeep-is@^0.1.3:\n  version \"0.1.4\"\n  resolved \"https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831\"\n  integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==\n\ndeepmerge@^4.3.1:\n  version \"4.3.1\"\n  resolved \"https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a\"\n  integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==\n\ndefault-shell@^2.0.0:\n  version \"2.2.0\"\n  resolved \"https://registry.yarnpkg.com/default-shell/-/default-shell-2.2.0.tgz#31481c19747bfe59319b486591643eaf115a1864\"\n  integrity sha512-sPpMZcVhRQ0nEMDtuMJ+RtCxt7iHPAMBU+I4tAlo5dU1sjRpNax0crj6nR3qKpvVnckaQ9U38enXcwW9nZJeCw==\n\ndefine-data-property@^1.1.4:\n  version \"1.1.4\"\n  resolved \"https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e\"\n  integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==\n  dependencies:\n    es-define-property \"^1.0.0\"\n    es-errors \"^1.3.0\"\n    gopd \"^1.0.1\"\n\ndegenerator@^5.0.0:\n  version \"5.0.1\"\n  resolved \"https://registry.yarnpkg.com/degenerator/-/degenerator-5.0.1.tgz#9403bf297c6dad9a1ece409b37db27954f91f2f5\"\n  integrity sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==\n  dependencies:\n    ast-types \"^0.13.4\"\n    escodegen \"^2.1.0\"\n    esprima \"^4.0.1\"\n\ndelayed-stream@~1.0.0:\n  version \"1.0.0\"\n  resolved \"https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619\"\n  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==\n\ndepd@2.0.0, depd@~2.0.0:\n  version \"2.0.0\"\n  resolved \"https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df\"\n  integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==\n\ndestroy@1.2.0, destroy@~1.2.0:\n  version \"1.2.0\"\n  resolved \"https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015\"\n  integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==\n\ndetect-libc@^2.0.0, detect-libc@^2.0.2, detect-libc@^2.0.3:\n  version \"2.1.2\"\n  resolved \"https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad\"\n  integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==\n\ndevtools-protocol@0.0.1203626:\n  version \"0.0.1203626\"\n  resolved \"https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1203626.tgz#4366a4c81a7e0d4fd6924e9182c67f1e5941e820\"\n  integrity sha512-nEzHZteIUZfGCZtTiS1fRpC8UZmsfD1SiyPvaUNvS13dvKf666OAm8YTi0+Ca3n1nLEyu49Cy4+dPWpaHFJk9g==\n\ndigest-fetch@^1.3.0:\n  version \"1.3.0\"\n  resolved \"https://registry.yarnpkg.com/digest-fetch/-/digest-fetch-1.3.0.tgz#898e69264d00012a23cf26e8a3e40320143fc661\"\n  integrity sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==\n  dependencies:\n    base-64 \"^0.1.0\"\n    md5 \"^2.3.0\"\n\ndingbat-to-unicode@^1.0.1:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz#5091dd673241453e6b5865e26e5a4452cdef5c83\"\n  integrity sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==\n\ndom-serializer@^2.0.0:\n  version \"2.0.0\"\n  resolved \"https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53\"\n  integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==\n  dependencies:\n    domelementtype \"^2.3.0\"\n    domhandler \"^5.0.2\"\n    entities \"^4.2.0\"\n\ndomelementtype@^2.3.0:\n  version \"2.3.0\"\n  resolved \"https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d\"\n  integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==\n\ndomhandler@^5.0.2, domhandler@^5.0.3:\n  version \"5.0.3\"\n  resolved \"https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31\"\n  integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==\n  dependencies:\n    domelementtype \"^2.3.0\"\n\ndomutils@^3.0.1:\n  version \"3.2.2\"\n  resolved \"https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78\"\n  integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==\n  dependencies:\n    dom-serializer \"^2.0.0\"\n    domelementtype \"^2.3.0\"\n    domhandler \"^5.0.3\"\n\ndotenv@^16.0.3:\n  version \"16.6.1\"\n  resolved \"https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020\"\n  integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==\n\nduck@^0.1.12:\n  version \"0.1.12\"\n  resolved \"https://registry.yarnpkg.com/duck/-/duck-0.1.12.tgz#de7adf758421230b6d7aee799ce42670586b9efa\"\n  integrity sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==\n  dependencies:\n    underscore \"^1.13.1\"\n\ndunder-proto@^1.0.1:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a\"\n  integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==\n  dependencies:\n    call-bind-apply-helpers \"^1.0.1\"\n    es-errors \"^1.3.0\"\n    gopd \"^1.2.0\"\n\nee-first@1.1.1:\n  version \"1.1.1\"\n  resolved \"https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d\"\n  integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==\n\nemoji-regex@^8.0.0:\n  version \"8.0.0\"\n  resolved \"https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37\"\n  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==\n\nenabled@2.0.x:\n  version \"2.0.0\"\n  resolved \"https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2\"\n  integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==\n\nencodeurl@~1.0.2:\n  version \"1.0.2\"\n  resolved \"https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59\"\n  integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==\n\nencodeurl@~2.0.0:\n  version \"2.0.0\"\n  resolved \"https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58\"\n  integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==\n\nencoding-japanese@2.2.0:\n  version \"2.2.0\"\n  resolved \"https://registry.yarnpkg.com/encoding-japanese/-/encoding-japanese-2.2.0.tgz#0ef2d2351250547f432a2dd155453555c16deb59\"\n  integrity sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==\n\nend-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:\n  version \"1.4.5\"\n  resolved \"https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c\"\n  integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==\n  dependencies:\n    once \"^1.4.0\"\n\nentities@^4.2.0, entities@^4.4.0:\n  version \"4.5.0\"\n  resolved \"https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48\"\n  integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==\n\n\"epub2@git+https://github.com/Mintplex-Labs/epub2-static.git#main\":\n  version \"3.0.2\"\n  resolved \"git+https://github.com/Mintplex-Labs/epub2-static.git#eb0a45cd41ac1a5b4c97766e9935d97583104d35\"\n  dependencies:\n    adm-zip \"^0.5.10\"\n    array-hyper-unique \"^2.1.4\"\n    bluebird \"^3.7.2\"\n    crlf-normalize \"^1.0.19\"\n    tslib \"^2.6.2\"\n    xml2js \"^0.6.2\"\n\nerror-ex@^1.3.1:\n  version \"1.3.4\"\n  resolved \"https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414\"\n  integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==\n  dependencies:\n    is-arrayish \"^0.2.1\"\n\nes-define-property@^1.0.0, es-define-property@^1.0.1:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa\"\n  integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==\n\nes-errors@^1.3.0:\n  version \"1.3.0\"\n  resolved \"https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f\"\n  integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==\n\nes-object-atoms@^1.0.0, es-object-atoms@^1.1.1:\n  version \"1.1.1\"\n  resolved \"https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1\"\n  integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==\n  dependencies:\n    es-errors \"^1.3.0\"\n\nes-set-tostringtag@^2.1.0:\n  version \"2.1.0\"\n  resolved \"https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d\"\n  integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==\n  dependencies:\n    es-errors \"^1.3.0\"\n    get-intrinsic \"^1.2.6\"\n    has-tostringtag \"^1.0.2\"\n    hasown \"^2.0.2\"\n\nescalade@^3.1.1:\n  version \"3.2.0\"\n  resolved \"https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5\"\n  integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==\n\nescape-html@~1.0.3:\n  version \"1.0.3\"\n  resolved \"https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988\"\n  integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==\n\nescape-string-regexp@^4.0.0:\n  version \"4.0.0\"\n  resolved \"https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34\"\n  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==\n\nescodegen@^2.1.0:\n  version \"2.1.0\"\n  resolved \"https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17\"\n  integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==\n  dependencies:\n    esprima \"^4.0.1\"\n    estraverse \"^5.2.0\"\n    esutils \"^2.0.2\"\n  optionalDependencies:\n    source-map \"~0.6.1\"\n\neslint-config-prettier@^9.0.0:\n  version \"9.1.2\"\n  resolved \"https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz#90deb4fa0259592df774b600dbd1d2249a78ce91\"\n  integrity sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==\n\neslint-plugin-prettier@^5.0.0:\n  version \"5.5.5\"\n  resolved \"https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz#9eae11593faa108859c26f9a9c367d619a0769c0\"\n  integrity sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==\n  dependencies:\n    prettier-linter-helpers \"^1.0.1\"\n    synckit \"^0.11.12\"\n\neslint-plugin-unused-imports@^4.0.0:\n  version \"4.4.1\"\n  resolved \"https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz#a831f0a2937d7631eba30cb87091ab7d3a5da0e1\"\n  integrity sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==\n\neslint-scope@^8.4.0:\n  version \"8.4.0\"\n  resolved \"https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82\"\n  integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==\n  dependencies:\n    esrecurse \"^4.3.0\"\n    estraverse \"^5.2.0\"\n\neslint-visitor-keys@^3.4.3:\n  version \"3.4.3\"\n  resolved \"https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800\"\n  integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==\n\neslint-visitor-keys@^4.2.1:\n  version \"4.2.1\"\n  resolved \"https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1\"\n  integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==\n\neslint@^9.0.0:\n  version \"9.39.3\"\n  resolved \"https://registry.yarnpkg.com/eslint/-/eslint-9.39.3.tgz#08d63df1533d7743c0907b32a79a7e134e63ee2f\"\n  integrity sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==\n  dependencies:\n    \"@eslint-community/eslint-utils\" \"^4.8.0\"\n    \"@eslint-community/regexpp\" \"^4.12.1\"\n    \"@eslint/config-array\" \"^0.21.1\"\n    \"@eslint/config-helpers\" \"^0.4.2\"\n    \"@eslint/core\" \"^0.17.0\"\n    \"@eslint/eslintrc\" \"^3.3.1\"\n    \"@eslint/js\" \"9.39.3\"\n    \"@eslint/plugin-kit\" \"^0.4.1\"\n    \"@humanfs/node\" \"^0.16.6\"\n    \"@humanwhocodes/module-importer\" \"^1.0.1\"\n    \"@humanwhocodes/retry\" \"^0.4.2\"\n    \"@types/estree\" \"^1.0.6\"\n    ajv \"^6.12.4\"\n    chalk \"^4.0.0\"\n    cross-spawn \"^7.0.6\"\n    debug \"^4.3.2\"\n    escape-string-regexp \"^4.0.0\"\n    eslint-scope \"^8.4.0\"\n    eslint-visitor-keys \"^4.2.1\"\n    espree \"^10.4.0\"\n    esquery \"^1.5.0\"\n    esutils \"^2.0.2\"\n    fast-deep-equal \"^3.1.3\"\n    file-entry-cache \"^8.0.0\"\n    find-up \"^5.0.0\"\n    glob-parent \"^6.0.2\"\n    ignore \"^5.2.0\"\n    imurmurhash \"^0.1.4\"\n    is-glob \"^4.0.0\"\n    json-stable-stringify-without-jsonify \"^1.0.1\"\n    lodash.merge \"^4.6.2\"\n    minimatch \"^3.1.2\"\n    natural-compare \"^1.4.0\"\n    optionator \"^0.9.3\"\n\nespree@^10.0.1, espree@^10.4.0:\n  version \"10.4.0\"\n  resolved \"https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837\"\n  integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==\n  dependencies:\n    acorn \"^8.15.0\"\n    acorn-jsx \"^5.3.2\"\n    eslint-visitor-keys \"^4.2.1\"\n\nesprima@^4.0.1:\n  version \"4.0.1\"\n  resolved \"https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71\"\n  integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==\n\nesquery@^1.5.0:\n  version \"1.7.0\"\n  resolved \"https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d\"\n  integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==\n  dependencies:\n    estraverse \"^5.1.0\"\n\nesrecurse@^4.3.0:\n  version \"4.3.0\"\n  resolved \"https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921\"\n  integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==\n  dependencies:\n    estraverse \"^5.2.0\"\n\nestraverse@^5.1.0, estraverse@^5.2.0:\n  version \"5.3.0\"\n  resolved \"https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123\"\n  integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==\n\nesutils@^2.0.2:\n  version \"2.0.3\"\n  resolved \"https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64\"\n  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==\n\netag@~1.8.1:\n  version \"1.8.1\"\n  resolved \"https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887\"\n  integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==\n\nevent-target-shim@^5.0.0:\n  version \"5.0.1\"\n  resolved \"https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789\"\n  integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==\n\neventemitter3@^4.0.4:\n  version \"4.0.7\"\n  resolved \"https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f\"\n  integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==\n\nevents-universal@^1.0.0:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/events-universal/-/events-universal-1.0.1.tgz#b56a84fd611b6610e0a2d0f09f80fdf931e2dfe6\"\n  integrity sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==\n  dependencies:\n    bare-events \"^2.7.0\"\n\nevents@^3.3.0:\n  version \"3.3.0\"\n  resolved \"https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400\"\n  integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==\n\nexeca@^5.1.1:\n  version \"5.1.1\"\n  resolved \"https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd\"\n  integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==\n  dependencies:\n    cross-spawn \"^7.0.3\"\n    get-stream \"^6.0.0\"\n    human-signals \"^2.1.0\"\n    is-stream \"^2.0.0\"\n    merge-stream \"^2.0.0\"\n    npm-run-path \"^4.0.1\"\n    onetime \"^5.1.2\"\n    signal-exit \"^3.0.3\"\n    strip-final-newline \"^2.0.0\"\n\nexpand-template@^2.0.3:\n  version \"2.0.3\"\n  resolved \"https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c\"\n  integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==\n\nexpr-eval@^2.0.2:\n  version \"2.0.2\"\n  resolved \"https://registry.yarnpkg.com/expr-eval/-/expr-eval-2.0.2.tgz#fa6f044a7b0c93fde830954eb9c5b0f7fbc7e201\"\n  integrity sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==\n\nexpress@^4.21.2:\n  version \"4.22.1\"\n  resolved \"https://registry.yarnpkg.com/express/-/express-4.22.1.tgz#1de23a09745a4fffdb39247b344bb5eaff382069\"\n  integrity sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==\n  dependencies:\n    accepts \"~1.3.8\"\n    array-flatten \"1.1.1\"\n    body-parser \"~1.20.3\"\n    content-disposition \"~0.5.4\"\n    content-type \"~1.0.4\"\n    cookie \"~0.7.1\"\n    cookie-signature \"~1.0.6\"\n    debug \"2.6.9\"\n    depd \"2.0.0\"\n    encodeurl \"~2.0.0\"\n    escape-html \"~1.0.3\"\n    etag \"~1.8.1\"\n    finalhandler \"~1.3.1\"\n    fresh \"~0.5.2\"\n    http-errors \"~2.0.0\"\n    merge-descriptors \"1.0.3\"\n    methods \"~1.1.2\"\n    on-finished \"~2.4.1\"\n    parseurl \"~1.3.3\"\n    path-to-regexp \"~0.1.12\"\n    proxy-addr \"~2.0.7\"\n    qs \"~6.14.0\"\n    range-parser \"~1.2.1\"\n    safe-buffer \"5.2.1\"\n    send \"~0.19.0\"\n    serve-static \"~1.16.2\"\n    setprototypeof \"1.2.0\"\n    statuses \"~2.0.1\"\n    type-is \"~1.6.18\"\n    utils-merge \"1.0.1\"\n    vary \"~1.1.2\"\n\nextract-zip@2.0.1:\n  version \"2.0.1\"\n  resolved \"https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a\"\n  integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==\n  dependencies:\n    debug \"^4.1.1\"\n    get-stream \"^5.1.0\"\n    yauzl \"^2.10.0\"\n  optionalDependencies:\n    \"@types/yauzl\" \"^2.9.1\"\n\nfast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:\n  version \"3.1.3\"\n  resolved \"https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525\"\n  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==\n\nfast-diff@^1.1.2:\n  version \"1.3.0\"\n  resolved \"https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0\"\n  integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==\n\nfast-fifo@^1.2.0, fast-fifo@^1.3.2:\n  version \"1.3.2\"\n  resolved \"https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c\"\n  integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==\n\nfast-json-stable-stringify@^2.0.0:\n  version \"2.1.0\"\n  resolved \"https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633\"\n  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==\n\nfast-levenshtein@^2.0.6:\n  version \"2.0.6\"\n  resolved \"https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917\"\n  integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==\n\nfd-slicer@~1.1.0:\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e\"\n  integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==\n  dependencies:\n    pend \"~1.2.0\"\n\nfecha@^4.2.0:\n  version \"4.2.3\"\n  resolved \"https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd\"\n  integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==\n\nfile-entry-cache@^8.0.0:\n  version \"8.0.0\"\n  resolved \"https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f\"\n  integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==\n  dependencies:\n    flat-cache \"^4.0.0\"\n\nfile-type@^16.5.4:\n  version \"16.5.4\"\n  resolved \"https://registry.yarnpkg.com/file-type/-/file-type-16.5.4.tgz#474fb4f704bee427681f98dd390058a172a6c2fd\"\n  integrity sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==\n  dependencies:\n    readable-web-to-node-stream \"^3.0.0\"\n    strtok3 \"^6.2.4\"\n    token-types \"^4.1.1\"\n\nfile-type@^3.8.0:\n  version \"3.9.0\"\n  resolved \"https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9\"\n  integrity sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==\n\nfile-type@^5.2.0:\n  version \"5.2.0\"\n  resolved \"https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6\"\n  integrity sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==\n\nfile-type@^6.1.0:\n  version \"6.2.0\"\n  resolved \"https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919\"\n  integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==\n\nfill-range@^7.1.1:\n  version \"7.1.1\"\n  resolved \"https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292\"\n  integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==\n  dependencies:\n    to-regex-range \"^5.0.1\"\n\nfinalhandler@~1.3.1:\n  version \"1.3.2\"\n  resolved \"https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.2.tgz#1ebc2228fc7673aac4a472c310cc05b77d852b88\"\n  integrity sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==\n  dependencies:\n    debug \"2.6.9\"\n    encodeurl \"~2.0.0\"\n    escape-html \"~1.0.3\"\n    on-finished \"~2.4.1\"\n    parseurl \"~1.3.3\"\n    statuses \"~2.0.2\"\n    unpipe \"~1.0.0\"\n\nfind-up@^5.0.0:\n  version \"5.0.0\"\n  resolved \"https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc\"\n  integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==\n  dependencies:\n    locate-path \"^6.0.0\"\n    path-exists \"^4.0.0\"\n\nfix-path@^4.0.0:\n  version \"4.0.0\"\n  resolved \"https://registry.yarnpkg.com/fix-path/-/fix-path-4.0.0.tgz#bc1d14f038edb734ac46944a45454106952ca429\"\n  integrity sha512-g31GX207Tt+psI53ZSaB1egprYbEN0ZYl90aKcO22A2LmCNnFsSq3b5YpoKp3E/QEiWByTXGJOkFQG4S07Bc1A==\n  dependencies:\n    shell-path \"^3.0.0\"\n\nflat-cache@^4.0.0:\n  version \"4.0.1\"\n  resolved \"https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c\"\n  integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==\n  dependencies:\n    flatted \"^3.2.9\"\n    keyv \"^4.5.4\"\n\nflat@^5.0.2:\n  version \"5.0.2\"\n  resolved \"https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241\"\n  integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==\n\nflatbuffers@^1.12.0:\n  version \"1.12.0\"\n  resolved \"https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-1.12.0.tgz#72e87d1726cb1b216e839ef02658aa87dcef68aa\"\n  integrity sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==\n\nflatted@^3.2.9:\n  version \"3.3.4\"\n  resolved \"https://registry.yarnpkg.com/flatted/-/flatted-3.3.4.tgz#0986e681008f0f13f58e18656c47967682db5ff6\"\n  integrity sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==\n\nfn.name@1.x.x:\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc\"\n  integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==\n\nfor-each@^0.3.5:\n  version \"0.3.5\"\n  resolved \"https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47\"\n  integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==\n  dependencies:\n    is-callable \"^1.2.7\"\n\nforeground-child@^3.1.0:\n  version \"3.3.1\"\n  resolved \"https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f\"\n  integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==\n  dependencies:\n    cross-spawn \"^7.0.6\"\n    signal-exit \"^4.0.1\"\n\nform-data-encoder@1.7.2:\n  version \"1.7.2\"\n  resolved \"https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040\"\n  integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==\n\nform-data@^4.0.4:\n  version \"4.0.5\"\n  resolved \"https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053\"\n  integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==\n  dependencies:\n    asynckit \"^0.4.0\"\n    combined-stream \"^1.0.8\"\n    es-set-tostringtag \"^2.1.0\"\n    hasown \"^2.0.2\"\n    mime-types \"^2.1.12\"\n\nformdata-node@^4.3.2:\n  version \"4.4.1\"\n  resolved \"https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2\"\n  integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==\n  dependencies:\n    node-domexception \"1.0.0\"\n    web-streams-polyfill \"4.0.0-beta.3\"\n\nforwarded@0.2.0:\n  version \"0.2.0\"\n  resolved \"https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811\"\n  integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==\n\nfresh@0.5.2, fresh@~0.5.2:\n  version \"0.5.2\"\n  resolved \"https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7\"\n  integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==\n\nfs-constants@^1.0.0:\n  version \"1.0.0\"\n  resolved \"https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad\"\n  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==\n\nfsevents@~2.3.2:\n  version \"2.3.3\"\n  resolved \"https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6\"\n  integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==\n\nfunction-bind@^1.1.2:\n  version \"1.1.2\"\n  resolved \"https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c\"\n  integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==\n\nget-caller-file@^2.0.5:\n  version \"2.0.5\"\n  resolved \"https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e\"\n  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==\n\nget-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0:\n  version \"1.3.0\"\n  resolved \"https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01\"\n  integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==\n  dependencies:\n    call-bind-apply-helpers \"^1.0.2\"\n    es-define-property \"^1.0.1\"\n    es-errors \"^1.3.0\"\n    es-object-atoms \"^1.1.1\"\n    function-bind \"^1.1.2\"\n    get-proto \"^1.0.1\"\n    gopd \"^1.2.0\"\n    has-symbols \"^1.1.0\"\n    hasown \"^2.0.2\"\n    math-intrinsics \"^1.1.0\"\n\nget-proto@^1.0.1:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1\"\n  integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==\n  dependencies:\n    dunder-proto \"^1.0.1\"\n    es-object-atoms \"^1.0.0\"\n\nget-stream@^2.2.0:\n  version \"2.3.1\"\n  resolved \"https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de\"\n  integrity sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==\n  dependencies:\n    object-assign \"^4.0.1\"\n    pinkie-promise \"^2.0.0\"\n\nget-stream@^5.1.0:\n  version \"5.2.0\"\n  resolved \"https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3\"\n  integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==\n  dependencies:\n    pump \"^3.0.0\"\n\nget-stream@^6.0.0:\n  version \"6.0.1\"\n  resolved \"https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7\"\n  integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==\n\nget-uri@^6.0.1:\n  version \"6.0.5\"\n  resolved \"https://registry.yarnpkg.com/get-uri/-/get-uri-6.0.5.tgz#714892aa4a871db671abc5395e5e9447bc306a16\"\n  integrity sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==\n  dependencies:\n    basic-ftp \"^5.0.2\"\n    data-uri-to-buffer \"^6.0.2\"\n    debug \"^4.3.4\"\n\ngithub-from-package@0.0.0:\n  version \"0.0.0\"\n  resolved \"https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce\"\n  integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==\n\nglob-parent@^6.0.2:\n  version \"6.0.2\"\n  resolved \"https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3\"\n  integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==\n  dependencies:\n    is-glob \"^4.0.3\"\n\nglob-parent@~5.1.2:\n  version \"5.1.2\"\n  resolved \"https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4\"\n  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==\n  dependencies:\n    is-glob \"^4.0.1\"\n\nglob@^10.3.7:\n  version \"10.5.0\"\n  resolved \"https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c\"\n  integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==\n  dependencies:\n    foreground-child \"^3.1.0\"\n    jackspeak \"^3.1.2\"\n    minimatch \"^9.0.4\"\n    minipass \"^7.1.2\"\n    package-json-from-dist \"^1.0.0\"\n    path-scurry \"^1.11.1\"\n\nglobals@^14.0.0:\n  version \"14.0.0\"\n  resolved \"https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e\"\n  integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==\n\nglobals@^17.4.0:\n  version \"17.4.0\"\n  resolved \"https://registry.yarnpkg.com/globals/-/globals-17.4.0.tgz#33d7d297ed1536b388a0e2f4bcd0ff19c8ff91b5\"\n  integrity sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==\n\ngopd@^1.0.1, gopd@^1.2.0:\n  version \"1.2.0\"\n  resolved \"https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1\"\n  integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==\n\ngraceful-fs@^4.1.10:\n  version \"4.2.11\"\n  resolved \"https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3\"\n  integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==\n\nguid-typescript@^1.0.9:\n  version \"1.0.9\"\n  resolved \"https://registry.yarnpkg.com/guid-typescript/-/guid-typescript-1.0.9.tgz#e35f77003535b0297ea08548f5ace6adb1480ddc\"\n  integrity sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==\n\nhas-flag@^3.0.0:\n  version \"3.0.0\"\n  resolved \"https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd\"\n  integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==\n\nhas-flag@^4.0.0:\n  version \"4.0.0\"\n  resolved \"https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b\"\n  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==\n\nhas-property-descriptors@^1.0.2:\n  version \"1.0.2\"\n  resolved \"https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854\"\n  integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==\n  dependencies:\n    es-define-property \"^1.0.0\"\n\nhas-symbols@^1.0.3, has-symbols@^1.1.0:\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338\"\n  integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==\n\nhas-tostringtag@^1.0.2:\n  version \"1.0.2\"\n  resolved \"https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc\"\n  integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==\n  dependencies:\n    has-symbols \"^1.0.3\"\n\nhasown@^2.0.2:\n  version \"2.0.2\"\n  resolved \"https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003\"\n  integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==\n  dependencies:\n    function-bind \"^1.1.2\"\n\nhe@1.2.0:\n  version \"1.2.0\"\n  resolved \"https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f\"\n  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==\n\nhtml-to-text@9.0.5, html-to-text@^9.0.5:\n  version \"9.0.5\"\n  resolved \"https://registry.yarnpkg.com/html-to-text/-/html-to-text-9.0.5.tgz#6149a0f618ae7a0db8085dca9bbf96d32bb8368d\"\n  integrity sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==\n  dependencies:\n    \"@selderee/plugin-htmlparser2\" \"^0.11.0\"\n    deepmerge \"^4.3.1\"\n    dom-serializer \"^2.0.0\"\n    htmlparser2 \"^8.0.2\"\n    selderee \"^0.11.0\"\n\nhtmlparser2@^8.0.2:\n  version \"8.0.2\"\n  resolved \"https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21\"\n  integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==\n  dependencies:\n    domelementtype \"^2.3.0\"\n    domhandler \"^5.0.3\"\n    domutils \"^3.0.1\"\n    entities \"^4.4.0\"\n\nhttp-errors@2.0.0:\n  version \"2.0.0\"\n  resolved \"https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3\"\n  integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==\n  dependencies:\n    depd \"2.0.0\"\n    inherits \"2.0.4\"\n    setprototypeof \"1.2.0\"\n    statuses \"2.0.1\"\n    toidentifier \"1.0.1\"\n\nhttp-errors@~2.0.0, http-errors@~2.0.1:\n  version \"2.0.1\"\n  resolved \"https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b\"\n  integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==\n  dependencies:\n    depd \"~2.0.0\"\n    inherits \"~2.0.4\"\n    setprototypeof \"~1.2.0\"\n    statuses \"~2.0.2\"\n    toidentifier \"~1.0.1\"\n\nhttp-proxy-agent@^7.0.0:\n  version \"7.0.2\"\n  resolved \"https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e\"\n  integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==\n  dependencies:\n    agent-base \"^7.1.0\"\n    debug \"^4.3.4\"\n\nhttps-proxy-agent@^7.0.2, https-proxy-agent@^7.0.6:\n  version \"7.0.6\"\n  resolved \"https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9\"\n  integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==\n  dependencies:\n    agent-base \"^7.1.2\"\n    debug \"4\"\n\nhuman-signals@^2.1.0:\n  version \"2.1.0\"\n  resolved \"https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0\"\n  integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==\n\nhumanize-duration@^3.25.1:\n  version \"3.33.1\"\n  resolved \"https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.33.1.tgz#e4df2ce6660f24a6a3bf4a7b3bc63edb5be7826f\"\n  integrity sha512-hwzSCymnRdFx9YdRkQQ0OYequXiVAV6ZGQA2uzocwB0F4309Ke6pO8dg0P8LHhRQJyVjGteRTAA/zNfEcpXn8A==\n\nhumanize-ms@^1.2.1:\n  version \"1.2.1\"\n  resolved \"https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed\"\n  integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==\n  dependencies:\n    ms \"^2.0.0\"\n\niconv-lite@0.6.3, iconv-lite@^0.6.3:\n  version \"0.6.3\"\n  resolved \"https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501\"\n  integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==\n  dependencies:\n    safer-buffer \">= 2.1.2 < 3.0.0\"\n\niconv-lite@0.7.0:\n  version \"0.7.0\"\n  resolved \"https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.0.tgz#c50cd80e6746ca8115eb98743afa81aa0e147a3e\"\n  integrity sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==\n  dependencies:\n    safer-buffer \">= 2.1.2 < 3.0.0\"\n\niconv-lite@~0.4.24:\n  version \"0.4.24\"\n  resolved \"https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b\"\n  integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==\n  dependencies:\n    safer-buffer \">= 2.1.2 < 3\"\n\nidb-keyval@^6.2.0:\n  version \"6.2.2\"\n  resolved \"https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.2.tgz#b0171b5f73944854a3291a5cdba8e12768c4854a\"\n  integrity sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==\n\nieee754@^1.1.13, ieee754@^1.2.1:\n  version \"1.2.1\"\n  resolved \"https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352\"\n  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==\n\nignore-by-default@^1.0.1:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09\"\n  integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==\n\nignore@^5.2.0, ignore@^5.3.0:\n  version \"5.3.2\"\n  resolved \"https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5\"\n  integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==\n\nimmediate@~3.0.5:\n  version \"3.0.6\"\n  resolved \"https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b\"\n  integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==\n\nimport-fresh@^3.2.1, import-fresh@^3.3.0:\n  version \"3.3.1\"\n  resolved \"https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf\"\n  integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==\n  dependencies:\n    parent-module \"^1.0.0\"\n    resolve-from \"^4.0.0\"\n\nimurmurhash@^0.1.4:\n  version \"0.1.4\"\n  resolved \"https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea\"\n  integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==\n\ninherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4:\n  version \"2.0.4\"\n  resolved \"https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c\"\n  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==\n\nini@~1.3.0:\n  version \"1.3.8\"\n  resolved \"https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c\"\n  integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==\n\nip-address@^10.0.1:\n  version \"10.1.0\"\n  resolved \"https://registry.yarnpkg.com/ip-address/-/ip-address-10.1.0.tgz#d8dcffb34d0e02eb241427444a6e23f5b0595aa4\"\n  integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==\n\nipaddr.js@1.9.1:\n  version \"1.9.1\"\n  resolved \"https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3\"\n  integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==\n\nis-any-array@^2.0.0:\n  version \"2.0.1\"\n  resolved \"https://registry.yarnpkg.com/is-any-array/-/is-any-array-2.0.1.tgz#9233242a9c098220290aa2ec28f82ca7fa79899e\"\n  integrity sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==\n\nis-arrayish@^0.2.1:\n  version \"0.2.1\"\n  resolved \"https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d\"\n  integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==\n\nis-arrayish@^0.3.1:\n  version \"0.3.4\"\n  resolved \"https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.4.tgz#1ee5553818511915685d33bb13d31bf854e5059d\"\n  integrity sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==\n\nis-binary-path@~2.1.0:\n  version \"2.1.0\"\n  resolved \"https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09\"\n  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==\n  dependencies:\n    binary-extensions \"^2.0.0\"\n\nis-buffer@~1.1.6:\n  version \"1.1.6\"\n  resolved \"https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be\"\n  integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==\n\nis-callable@^1.2.7:\n  version \"1.2.7\"\n  resolved \"https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055\"\n  integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==\n\nis-extglob@^2.1.1:\n  version \"2.1.1\"\n  resolved \"https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2\"\n  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==\n\nis-fullwidth-code-point@^3.0.0:\n  version \"3.0.0\"\n  resolved \"https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d\"\n  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==\n\nis-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:\n  version \"4.0.3\"\n  resolved \"https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084\"\n  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==\n  dependencies:\n    is-extglob \"^2.1.1\"\n\nis-natural-number@^4.0.1:\n  version \"4.0.1\"\n  resolved \"https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8\"\n  integrity sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==\n\nis-number@^7.0.0:\n  version \"7.0.0\"\n  resolved \"https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b\"\n  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==\n\nis-stream@^1.1.0:\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44\"\n  integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==\n\nis-stream@^2.0.0:\n  version \"2.0.1\"\n  resolved \"https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077\"\n  integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==\n\nis-typed-array@^1.1.14:\n  version \"1.1.15\"\n  resolved \"https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b\"\n  integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==\n  dependencies:\n    which-typed-array \"^1.1.16\"\n\nis-url@^1.2.4:\n  version \"1.2.4\"\n  resolved \"https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52\"\n  integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==\n\nisarray@^2.0.5:\n  version \"2.0.5\"\n  resolved \"https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723\"\n  integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==\n\nisarray@~1.0.0:\n  version \"1.0.0\"\n  resolved \"https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11\"\n  integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==\n\nisexe@^2.0.0:\n  version \"2.0.0\"\n  resolved \"https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10\"\n  integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==\n\njackspeak@^3.1.2:\n  version \"3.4.3\"\n  resolved \"https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a\"\n  integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==\n  dependencies:\n    \"@isaacs/cliui\" \"^8.0.2\"\n  optionalDependencies:\n    \"@pkgjs/parseargs\" \"^0.11.0\"\n\njintr@^1.1.0:\n  version \"1.2.0\"\n  resolved \"https://registry.yarnpkg.com/jintr/-/jintr-1.2.0.tgz#f000275ee86f32a7d3d9eb9b71ffdb3c93600f50\"\n  integrity sha512-OYPVJPlDih+G7LXjnLR5Brsz8ClxkXYn0ZmWyJVIUmFI6lySQu/dzpGRK/ujBLAYCvuJu/VqXzf84C1edowbLg==\n  dependencies:\n    acorn \"^8.8.0\"\n\njs-tiktoken@^1.0.12, js-tiktoken@^1.0.7, js-tiktoken@^1.0.8:\n  version \"1.0.21\"\n  resolved \"https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.21.tgz#368a9957591a30a62997dd0c4cf30866f00f8221\"\n  integrity sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==\n  dependencies:\n    base64-js \"^1.5.1\"\n\njs-tokens@^4.0.0:\n  version \"4.0.0\"\n  resolved \"https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499\"\n  integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==\n\njs-yaml@^4.1.0, js-yaml@^4.1.1:\n  version \"4.1.1\"\n  resolved \"https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b\"\n  integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==\n  dependencies:\n    argparse \"^2.0.1\"\n\njson-buffer@3.0.1:\n  version \"3.0.1\"\n  resolved \"https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13\"\n  integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==\n\njson-parse-even-better-errors@^2.3.0:\n  version \"2.3.1\"\n  resolved \"https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d\"\n  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==\n\njson-schema-traverse@^0.4.1:\n  version \"0.4.1\"\n  resolved \"https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660\"\n  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==\n\njson-stable-stringify-without-jsonify@^1.0.1:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651\"\n  integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==\n\njsonpointer@^5.0.1:\n  version \"5.0.1\"\n  resolved \"https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559\"\n  integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==\n\njszip@^3.7.1:\n  version \"3.10.1\"\n  resolved \"https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2\"\n  integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==\n  dependencies:\n    lie \"~3.3.0\"\n    pako \"~1.0.2\"\n    readable-stream \"~2.3.6\"\n    setimmediate \"^1.0.5\"\n\nkeyv@^4.5.4:\n  version \"4.5.4\"\n  resolved \"https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93\"\n  integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==\n  dependencies:\n    json-buffer \"3.0.1\"\n\nkuler@^2.0.0:\n  version \"2.0.0\"\n  resolved \"https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3\"\n  integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==\n\nlangchain@0.1.36:\n  version \"0.1.36\"\n  resolved \"https://registry.yarnpkg.com/langchain/-/langchain-0.1.36.tgz#2f201f681d83fb265716e28e7dfcfe61fbeef2c2\"\n  integrity sha512-NTbnCL/jKWIeEI//Nm1oG8nhW3vkYWvEMr1MPotmTThTfeKfO87eV/OAzAyh6Ruy6GFs/qofRgQZGIe6XvXTNQ==\n  dependencies:\n    \"@anthropic-ai/sdk\" \"^0.9.1\"\n    \"@langchain/community\" \"~0.0.47\"\n    \"@langchain/core\" \"~0.1.60\"\n    \"@langchain/openai\" \"~0.0.28\"\n    \"@langchain/textsplitters\" \"~0.0.0\"\n    binary-extensions \"^2.2.0\"\n    js-tiktoken \"^1.0.7\"\n    js-yaml \"^4.1.0\"\n    jsonpointer \"^5.0.1\"\n    langchainhub \"~0.0.8\"\n    langsmith \"~0.1.7\"\n    ml-distance \"^4.0.0\"\n    openapi-types \"^12.1.3\"\n    p-retry \"4\"\n    uuid \"^9.0.0\"\n    yaml \"^2.2.1\"\n    zod \"^3.22.4\"\n    zod-to-json-schema \"^3.22.3\"\n\nlangchain@~0.2.3:\n  version \"0.2.20\"\n  resolved \"https://registry.yarnpkg.com/langchain/-/langchain-0.2.20.tgz#05bf348d684ece96cba73e0b8afe6b9778164857\"\n  integrity sha512-tbels6Rr524iMM3VOQ4aTGnEOOjAA1BQuBR8u/8gJ2yT48lMtIQRAN32Y4KVjKK+hEWxHHlmLBrtgLpTphFjNA==\n  dependencies:\n    \"@langchain/core\" \">=0.2.21 <0.3.0\"\n    \"@langchain/openai\" \">=0.1.0 <0.3.0\"\n    \"@langchain/textsplitters\" \"~0.0.0\"\n    binary-extensions \"^2.2.0\"\n    js-tiktoken \"^1.0.12\"\n    js-yaml \"^4.1.0\"\n    jsonpointer \"^5.0.1\"\n    langsmith \"^0.1.56-rc.1\"\n    openapi-types \"^12.1.3\"\n    p-retry \"4\"\n    uuid \"^10.0.0\"\n    yaml \"^2.2.1\"\n    zod \"^3.22.4\"\n    zod-to-json-schema \"^3.22.3\"\n\nlangchainhub@~0.0.8:\n  version \"0.0.11\"\n  resolved \"https://registry.yarnpkg.com/langchainhub/-/langchainhub-0.0.11.tgz#2ce22def9c84699dcbd4fd4b78270d34bd2a9ae9\"\n  integrity sha512-WnKI4g9kU2bHQP136orXr2bcRdgz9iiTBpTN0jWt9IlScUKnJBoD0aa2HOzHURQKeQDnt2JwqVmQ6Depf5uDLQ==\n\nlangsmith@^0.1.56-rc.1, langsmith@~0.1.1, langsmith@~0.1.30, langsmith@~0.1.7:\n  version \"0.1.68\"\n  resolved \"https://registry.yarnpkg.com/langsmith/-/langsmith-0.1.68.tgz#848332e822fe5e6734a07f1c36b6530cc1798afb\"\n  integrity sha512-otmiysWtVAqzMx3CJ4PrtUBhWRG5Co8Z4o7hSZENPjlit9/j3/vm3TSvbaxpDYakZxtMjhkcJTqrdYFipISEiQ==\n  dependencies:\n    \"@types/uuid\" \"^10.0.0\"\n    commander \"^10.0.1\"\n    p-queue \"^6.6.2\"\n    p-retry \"4\"\n    semver \"^7.6.3\"\n    uuid \"^10.0.0\"\n\nleac@^0.6.0:\n  version \"0.6.0\"\n  resolved \"https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912\"\n  integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==\n\nlevn@^0.4.1:\n  version \"0.4.1\"\n  resolved \"https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade\"\n  integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==\n  dependencies:\n    prelude-ls \"^1.2.1\"\n    type-check \"~0.4.0\"\n\nlibbase64@1.3.0:\n  version \"1.3.0\"\n  resolved \"https://registry.yarnpkg.com/libbase64/-/libbase64-1.3.0.tgz#053314755a05d2e5f08bbfc48d0290e9322f4406\"\n  integrity sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==\n\nlibmime@5.3.7:\n  version \"5.3.7\"\n  resolved \"https://registry.yarnpkg.com/libmime/-/libmime-5.3.7.tgz#3835b6443d982d5cd1ac32ee241adbbc11b34406\"\n  integrity sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==\n  dependencies:\n    encoding-japanese \"2.2.0\"\n    iconv-lite \"0.6.3\"\n    libbase64 \"1.3.0\"\n    libqp \"2.1.1\"\n\nlibqp@2.1.1:\n  version \"2.1.1\"\n  resolved \"https://registry.yarnpkg.com/libqp/-/libqp-2.1.1.tgz#f1be767a58f966f500597997cab72cfc1e17abfa\"\n  integrity sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==\n\nlie@~3.3.0:\n  version \"3.3.0\"\n  resolved \"https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a\"\n  integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==\n  dependencies:\n    immediate \"~3.0.5\"\n\nlines-and-columns@^1.1.6:\n  version \"1.2.4\"\n  resolved \"https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632\"\n  integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==\n\nlinkify-it@5.0.0:\n  version \"5.0.0\"\n  resolved \"https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421\"\n  integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==\n  dependencies:\n    uc.micro \"^2.0.0\"\n\nlocate-path@^6.0.0:\n  version \"6.0.0\"\n  resolved \"https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286\"\n  integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==\n  dependencies:\n    p-locate \"^5.0.0\"\n\nlodash.merge@^4.6.2:\n  version \"4.6.2\"\n  resolved \"https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a\"\n  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==\n\nlodash@^4.17.21:\n  version \"4.17.21\"\n  resolved \"https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c\"\n  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==\n\nlogform@^2.7.0:\n  version \"2.7.0\"\n  resolved \"https://registry.yarnpkg.com/logform/-/logform-2.7.0.tgz#cfca97528ef290f2e125a08396805002b2d060d1\"\n  integrity sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==\n  dependencies:\n    \"@colors/colors\" \"1.6.0\"\n    \"@types/triple-beam\" \"^1.3.2\"\n    fecha \"^4.2.0\"\n    ms \"^2.1.1\"\n    safe-stable-stringify \"^2.3.1\"\n    triple-beam \"^1.3.0\"\n\nlong@^4.0.0:\n  version \"4.0.0\"\n  resolved \"https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28\"\n  integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==\n\nlop@^0.4.2:\n  version \"0.4.2\"\n  resolved \"https://registry.yarnpkg.com/lop/-/lop-0.4.2.tgz#c9c2f958a39b9da1c2f36ca9ad66891a9fe84640\"\n  integrity sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==\n  dependencies:\n    duck \"^0.1.12\"\n    option \"~0.2.1\"\n    underscore \"^1.13.1\"\n\nlru-cache@^10.2.0:\n  version \"10.4.3\"\n  resolved \"https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119\"\n  integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==\n\nlru-cache@^7.14.1:\n  version \"7.18.3\"\n  resolved \"https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89\"\n  integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==\n\nmailparser@^3.0.1:\n  version \"3.9.0\"\n  resolved \"https://registry.yarnpkg.com/mailparser/-/mailparser-3.9.0.tgz#3b71732a26aa7c3390cd39540713eed7daf0aa2c\"\n  integrity sha512-jpaNLhDjwy0w2f8sySOSRiWREjPqssSc0C2czV98btCXCRX3EyNloQ2IWirmMDj1Ies8Fkm0l96bZBZpDG7qkg==\n  dependencies:\n    \"@zone-eu/mailsplit\" \"5.4.7\"\n    encoding-japanese \"2.2.0\"\n    he \"1.2.0\"\n    html-to-text \"9.0.5\"\n    iconv-lite \"0.7.0\"\n    libmime \"5.3.7\"\n    linkify-it \"5.0.0\"\n    nodemailer \"7.0.10\"\n    punycode.js \"2.3.1\"\n    tlds \"1.261.0\"\n\nmake-dir@^1.0.0:\n  version \"1.3.0\"\n  resolved \"https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c\"\n  integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==\n  dependencies:\n    pify \"^3.0.0\"\n\nmammoth@^1.6.0:\n  version \"1.11.0\"\n  resolved \"https://registry.yarnpkg.com/mammoth/-/mammoth-1.11.0.tgz#f6c68624eaffcf56728a792fcccd3495d688bac5\"\n  integrity sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==\n  dependencies:\n    \"@xmldom/xmldom\" \"^0.8.6\"\n    argparse \"~1.0.3\"\n    base64-js \"^1.5.1\"\n    bluebird \"~3.4.0\"\n    dingbat-to-unicode \"^1.0.1\"\n    jszip \"^3.7.1\"\n    lop \"^0.4.2\"\n    path-is-absolute \"^1.0.0\"\n    underscore \"^1.13.1\"\n    xmlbuilder \"^10.0.0\"\n\nmath-intrinsics@^1.1.0:\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9\"\n  integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==\n\nmbox-parser@^1.0.1:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/mbox-parser/-/mbox-parser-1.0.1.tgz#cba6bb372a1d3c27aff9da356b4519aeee458e1f\"\n  integrity sha512-9PNE026G/SodrYYxZiDZDJ5YQ0piN8QCqO662MupIGwgsvBtJHFPxYc/uUsQyhUysl4kMG0NLYvfELNkM5+HzQ==\n  dependencies:\n    \"@types/mailparser\" \"^3.0.0\"\n    humanize-duration \"^3.25.1\"\n    mailparser \"^3.0.1\"\n\nmd5@^2.3.0:\n  version \"2.3.0\"\n  resolved \"https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f\"\n  integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==\n  dependencies:\n    charenc \"0.0.2\"\n    crypt \"0.0.2\"\n    is-buffer \"~1.1.6\"\n\nmedia-typer@0.3.0:\n  version \"0.3.0\"\n  resolved \"https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748\"\n  integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==\n\nmerge-descriptors@1.0.3:\n  version \"1.0.3\"\n  resolved \"https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5\"\n  integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==\n\nmerge-stream@^2.0.0:\n  version \"2.0.0\"\n  resolved \"https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60\"\n  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==\n\nmethods@~1.1.2:\n  version \"1.1.2\"\n  resolved \"https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee\"\n  integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==\n\nmime-db@1.52.0:\n  version \"1.52.0\"\n  resolved \"https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70\"\n  integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==\n\nmime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34:\n  version \"2.1.35\"\n  resolved \"https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a\"\n  integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==\n  dependencies:\n    mime-db \"1.52.0\"\n\nmime@1.6.0:\n  version \"1.6.0\"\n  resolved \"https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1\"\n  integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==\n\nmime@^3.0.0:\n  version \"3.0.0\"\n  resolved \"https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7\"\n  integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==\n\nmimic-fn@^2.1.0:\n  version \"2.1.0\"\n  resolved \"https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b\"\n  integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==\n\nmimic-response@^3.1.0:\n  version \"3.1.0\"\n  resolved \"https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9\"\n  integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==\n\nminimatch@^3.1.2:\n  version \"3.1.2\"\n  resolved \"https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b\"\n  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==\n  dependencies:\n    brace-expansion \"^1.1.7\"\n\nminimatch@^3.1.3:\n  version \"3.1.5\"\n  resolved \"https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e\"\n  integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==\n  dependencies:\n    brace-expansion \"^1.1.7\"\n\nminimatch@^9.0.4:\n  version \"9.0.5\"\n  resolved \"https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5\"\n  integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==\n  dependencies:\n    brace-expansion \"^2.0.1\"\n\nminimist@^1.2.0, minimist@^1.2.3:\n  version \"1.2.8\"\n  resolved \"https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c\"\n  integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==\n\n\"minipass@^5.0.0 || ^6.0.2 || ^7.0.0\", minipass@^7.1.2:\n  version \"7.1.2\"\n  resolved \"https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707\"\n  integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==\n\nmitt@3.0.1:\n  version \"3.0.1\"\n  resolved \"https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1\"\n  integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==\n\nmkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:\n  version \"0.5.3\"\n  resolved \"https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113\"\n  integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==\n\nml-array-mean@^1.1.6:\n  version \"1.1.6\"\n  resolved \"https://registry.yarnpkg.com/ml-array-mean/-/ml-array-mean-1.1.6.tgz#d951a700dc8e3a17b3e0a583c2c64abd0c619c56\"\n  integrity sha512-MIdf7Zc8HznwIisyiJGRH9tRigg3Yf4FldW8DxKxpCCv/g5CafTw0RRu51nojVEOXuCQC7DRVVu5c7XXO/5joQ==\n  dependencies:\n    ml-array-sum \"^1.1.6\"\n\nml-array-sum@^1.1.6:\n  version \"1.1.6\"\n  resolved \"https://registry.yarnpkg.com/ml-array-sum/-/ml-array-sum-1.1.6.tgz#d1d89c20793cd29c37b09d40e85681aa4515a955\"\n  integrity sha512-29mAh2GwH7ZmiRnup4UyibQZB9+ZLyMShvt4cH4eTK+cL2oEMIZFnSyB3SS8MlsTh6q/w/yh48KmqLxmovN4Dw==\n  dependencies:\n    is-any-array \"^2.0.0\"\n\nml-distance-euclidean@^2.0.0:\n  version \"2.0.0\"\n  resolved \"https://registry.yarnpkg.com/ml-distance-euclidean/-/ml-distance-euclidean-2.0.0.tgz#3a668d236649d1b8fec96380b9435c6f42c9a817\"\n  integrity sha512-yC9/2o8QF0A3m/0IXqCTXCzz2pNEzvmcE/9HFKOZGnTjatvBbsn4lWYJkxENkA4Ug2fnYl7PXQxnPi21sgMy/Q==\n\nml-distance@^4.0.0:\n  version \"4.0.1\"\n  resolved \"https://registry.yarnpkg.com/ml-distance/-/ml-distance-4.0.1.tgz#4741d17a1735888c5388823762271dfe604bd019\"\n  integrity sha512-feZ5ziXs01zhyFUUUeZV5hwc0f5JW0Sh0ckU1koZe/wdVkJdGxcP06KNQuF0WBTj8FttQUzcvQcpcrOp/XrlEw==\n  dependencies:\n    ml-array-mean \"^1.1.6\"\n    ml-distance-euclidean \"^2.0.0\"\n    ml-tree-similarity \"^1.0.0\"\n\nml-tree-similarity@^1.0.0:\n  version \"1.0.0\"\n  resolved \"https://registry.yarnpkg.com/ml-tree-similarity/-/ml-tree-similarity-1.0.0.tgz#24705a107e32829e24d945e87219e892159c53f0\"\n  integrity sha512-XJUyYqjSuUQkNQHMscr6tcjldsOoAekxADTplt40QKfwW6nd++1wHWV9AArl0Zvw/TIHgNaZZNvr8QGvE8wLRg==\n  dependencies:\n    binary-search \"^1.3.5\"\n    num-sort \"^2.0.0\"\n\nmoment@^2.29.4:\n  version \"2.30.1\"\n  resolved \"https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae\"\n  integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==\n\nms@2.0.0:\n  version \"2.0.0\"\n  resolved \"https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8\"\n  integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==\n\nms@2.1.2:\n  version \"2.1.2\"\n  resolved \"https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009\"\n  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==\n\nms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3:\n  version \"2.1.3\"\n  resolved \"https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2\"\n  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==\n\nmustache@^4.2.0:\n  version \"4.2.0\"\n  resolved \"https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64\"\n  integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==\n\nnapi-build-utils@^2.0.0:\n  version \"2.0.0\"\n  resolved \"https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e\"\n  integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==\n\nnatural-compare@^1.4.0:\n  version \"1.4.0\"\n  resolved \"https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7\"\n  integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==\n\nnegotiator@0.6.3:\n  version \"0.6.3\"\n  resolved \"https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd\"\n  integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==\n\nnetmask@^2.0.2:\n  version \"2.0.2\"\n  resolved \"https://registry.yarnpkg.com/netmask/-/netmask-2.0.2.tgz#8b01a07644065d536383835823bc52004ebac5e7\"\n  integrity sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==\n\nnode-abi@^3.3.0:\n  version \"3.85.0\"\n  resolved \"https://registry.yarnpkg.com/node-abi/-/node-abi-3.85.0.tgz#b115d575e52b2495ef08372b058e13d202875a7d\"\n  integrity sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==\n  dependencies:\n    semver \"^7.3.5\"\n\nnode-addon-api@^6.1.0:\n  version \"6.1.0\"\n  resolved \"https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76\"\n  integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==\n\nnode-domexception@1.0.0:\n  version \"1.0.0\"\n  resolved \"https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5\"\n  integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==\n\nnode-ensure@^0.0.0:\n  version \"0.0.0\"\n  resolved \"https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7\"\n  integrity sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==\n\nnode-fetch@^2.6.12, node-fetch@^2.6.7, node-fetch@^2.6.9:\n  version \"2.7.0\"\n  resolved \"https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d\"\n  integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==\n  dependencies:\n    whatwg-url \"^5.0.0\"\n\nnode-html-parser@^6.1.13:\n  version \"6.1.13\"\n  resolved \"https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.13.tgz#a1df799b83df5c6743fcd92740ba14682083b7e4\"\n  integrity sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==\n  dependencies:\n    css-select \"^5.1.0\"\n    he \"1.2.0\"\n\nnode-xlsx@^0.24.0:\n  version \"0.24.0\"\n  resolved \"https://registry.yarnpkg.com/node-xlsx/-/node-xlsx-0.24.0.tgz#a6a365acb18ad37c66c2b254b6ebe0c22dc9dc6f\"\n  integrity sha512-1olwK48XK9nXZsyH/FCltvGrQYvXXZuxVitxXXv2GIuRm51aBi1+5KwR4rWM4KeO61sFU+00913WLZTD+AcXEg==\n  dependencies:\n    xlsx \"https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz\"\n\nnodemailer@7.0.10:\n  version \"7.0.10\"\n  resolved \"https://registry.yarnpkg.com/nodemailer/-/nodemailer-7.0.10.tgz#540062dbbe574220b42e79d2d949956d3eac5a46\"\n  integrity sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==\n\nnodemon@^2.0.22:\n  version \"2.0.22\"\n  resolved \"https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.22.tgz#182c45c3a78da486f673d6c1702e00728daf5258\"\n  integrity sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==\n  dependencies:\n    chokidar \"^3.5.2\"\n    debug \"^3.2.7\"\n    ignore-by-default \"^1.0.1\"\n    minimatch \"^3.1.2\"\n    pstree.remy \"^1.1.8\"\n    semver \"^5.7.1\"\n    simple-update-notifier \"^1.0.7\"\n    supports-color \"^5.5.0\"\n    touch \"^3.1.0\"\n    undefsafe \"^2.0.5\"\n\nnormalize-path@^3.0.0, normalize-path@~3.0.0:\n  version \"3.0.0\"\n  resolved \"https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65\"\n  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==\n\nnpm-run-path@^4.0.1:\n  version \"4.0.1\"\n  resolved \"https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea\"\n  integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==\n  dependencies:\n    path-key \"^3.0.0\"\n\nnth-check@^2.0.1:\n  version \"2.1.1\"\n  resolved \"https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d\"\n  integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==\n  dependencies:\n    boolbase \"^1.0.0\"\n\nnum-sort@^2.0.0:\n  version \"2.1.0\"\n  resolved \"https://registry.yarnpkg.com/num-sort/-/num-sort-2.1.0.tgz#1cbb37aed071329fdf41151258bc011898577a9b\"\n  integrity sha512-1MQz1Ed8z2yckoBeSfkQHHO9K1yDRxxtotKSJ9yvcTUUxSvfvzEq5GwBrjjHEpMlq/k5gvXdmJ1SbYxWtpNoVg==\n\nobject-assign@^4, object-assign@^4.0.1:\n  version \"4.1.1\"\n  resolved \"https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863\"\n  integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==\n\nobject-inspect@^1.13.3:\n  version \"1.13.4\"\n  resolved \"https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213\"\n  integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==\n\nofficeparser@^4.0.5:\n  version \"4.2.0\"\n  resolved \"https://registry.yarnpkg.com/officeparser/-/officeparser-4.2.0.tgz#6d0baf411bd21e7e31a4b1f206af7034a4506ca2\"\n  integrity sha512-LXSfaET8ZOBNjmSev4K1N6AiKTaY7m9NkddeCaMUdEe5D/HUuv2byB8VoPIaiLldtKun0I92tbhO+VGDUr/aXQ==\n  dependencies:\n    \"@xmldom/xmldom\" \"^0.8.10\"\n    decompress \"^4.2.1\"\n    file-type \"^16.5.4\"\n    node-ensure \"^0.0.0\"\n    rimraf \"^5.0.10\"\n\non-finished@2.4.1, on-finished@~2.4.1:\n  version \"2.4.1\"\n  resolved \"https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f\"\n  integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==\n  dependencies:\n    ee-first \"1.1.1\"\n\nonce@^1.3.1, once@^1.4.0:\n  version \"1.4.0\"\n  resolved \"https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1\"\n  integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==\n  dependencies:\n    wrappy \"1\"\n\none-time@^1.0.0:\n  version \"1.0.0\"\n  resolved \"https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45\"\n  integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==\n  dependencies:\n    fn.name \"1.x.x\"\n\nonetime@^5.1.2:\n  version \"5.1.2\"\n  resolved \"https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e\"\n  integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==\n  dependencies:\n    mimic-fn \"^2.1.0\"\n\nonnx-proto@^4.0.4:\n  version \"4.0.4\"\n  resolved \"https://registry.yarnpkg.com/onnx-proto/-/onnx-proto-4.0.4.tgz#2431a25bee25148e915906dda0687aafe3b9e044\"\n  integrity sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==\n  dependencies:\n    protobufjs \"^6.8.8\"\n\nonnxruntime-common@~1.14.0:\n  version \"1.14.0\"\n  resolved \"https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.14.0.tgz#2bb5dac5261269779aa5fb6536ca379657de8bf6\"\n  integrity sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==\n\nonnxruntime-node@1.14.0:\n  version \"1.14.0\"\n  resolved \"https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.14.0.tgz#c4ae6c355cfae7d83abaf36dd39a905c4a010217\"\n  integrity sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==\n  dependencies:\n    onnxruntime-common \"~1.14.0\"\n\nonnxruntime-web@1.14.0:\n  version \"1.14.0\"\n  resolved \"https://registry.yarnpkg.com/onnxruntime-web/-/onnxruntime-web-1.14.0.tgz#c8cee538781b1d4c1c6b043934f4a3e6ddf1466e\"\n  integrity sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==\n  dependencies:\n    flatbuffers \"^1.12.0\"\n    guid-typescript \"^1.0.9\"\n    long \"^4.0.0\"\n    onnx-proto \"^4.0.4\"\n    onnxruntime-common \"~1.14.0\"\n    platform \"^1.3.6\"\n\nopenai@4.95.1:\n  version \"4.95.1\"\n  resolved \"https://registry.yarnpkg.com/openai/-/openai-4.95.1.tgz#7157697c2b150a546b13eb860180c4a6058051da\"\n  integrity sha512-IqJy+ymeW+k/Wq+2YVN3693OQMMcODRtHEYOlz263MdUwnN/Dwdl9c2EXSxLLtGEHkSHAfvzpDMHI5MaWJKXjQ==\n  dependencies:\n    \"@types/node\" \"^18.11.18\"\n    \"@types/node-fetch\" \"^2.6.4\"\n    abort-controller \"^3.0.0\"\n    agentkeepalive \"^4.2.1\"\n    form-data-encoder \"1.7.2\"\n    formdata-node \"^4.3.2\"\n    node-fetch \"^2.6.7\"\n\nopenai@^4.41.1, openai@^4.57.3:\n  version \"4.104.0\"\n  resolved \"https://registry.yarnpkg.com/openai/-/openai-4.104.0.tgz#c489765dc051b95019845dab64b0e5207cae4d30\"\n  integrity sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==\n  dependencies:\n    \"@types/node\" \"^18.11.18\"\n    \"@types/node-fetch\" \"^2.6.4\"\n    abort-controller \"^3.0.0\"\n    agentkeepalive \"^4.2.1\"\n    form-data-encoder \"1.7.2\"\n    formdata-node \"^4.3.2\"\n    node-fetch \"^2.6.7\"\n\nopenapi-types@^12.1.3:\n  version \"12.1.3\"\n  resolved \"https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3\"\n  integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==\n\nopencollective-postinstall@^2.0.3:\n  version \"2.0.3\"\n  resolved \"https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259\"\n  integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==\n\noption@~0.2.1:\n  version \"0.2.4\"\n  resolved \"https://registry.yarnpkg.com/option/-/option-0.2.4.tgz#fd475cdf98dcabb3cb397a3ba5284feb45edbfe4\"\n  integrity sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==\n\noptionator@^0.9.3:\n  version \"0.9.4\"\n  resolved \"https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734\"\n  integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==\n  dependencies:\n    deep-is \"^0.1.3\"\n    fast-levenshtein \"^2.0.6\"\n    levn \"^0.4.1\"\n    prelude-ls \"^1.2.1\"\n    type-check \"^0.4.0\"\n    word-wrap \"^1.2.5\"\n\np-finally@^1.0.0:\n  version \"1.0.0\"\n  resolved \"https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae\"\n  integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==\n\np-limit@^3.0.2:\n  version \"3.1.0\"\n  resolved \"https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b\"\n  integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==\n  dependencies:\n    yocto-queue \"^0.1.0\"\n\np-locate@^5.0.0:\n  version \"5.0.0\"\n  resolved \"https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834\"\n  integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==\n  dependencies:\n    p-limit \"^3.0.2\"\n\np-queue@^6.6.2:\n  version \"6.6.2\"\n  resolved \"https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426\"\n  integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==\n  dependencies:\n    eventemitter3 \"^4.0.4\"\n    p-timeout \"^3.2.0\"\n\np-retry@4:\n  version \"4.6.2\"\n  resolved \"https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16\"\n  integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==\n  dependencies:\n    \"@types/retry\" \"0.12.0\"\n    retry \"^0.13.1\"\n\np-timeout@^3.2.0:\n  version \"3.2.0\"\n  resolved \"https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe\"\n  integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==\n  dependencies:\n    p-finally \"^1.0.0\"\n\npac-proxy-agent@^7.0.1:\n  version \"7.2.0\"\n  resolved \"https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz#9cfaf33ff25da36f6147a20844230ec92c06e5df\"\n  integrity sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==\n  dependencies:\n    \"@tootallnate/quickjs-emscripten\" \"^0.23.0\"\n    agent-base \"^7.1.2\"\n    debug \"^4.3.4\"\n    get-uri \"^6.0.1\"\n    http-proxy-agent \"^7.0.0\"\n    https-proxy-agent \"^7.0.6\"\n    pac-resolver \"^7.0.1\"\n    socks-proxy-agent \"^8.0.5\"\n\npac-resolver@^7.0.1:\n  version \"7.0.1\"\n  resolved \"https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-7.0.1.tgz#54675558ea368b64d210fd9c92a640b5f3b8abb6\"\n  integrity sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==\n  dependencies:\n    degenerator \"^5.0.0\"\n    netmask \"^2.0.2\"\n\npackage-json-from-dist@^1.0.0:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505\"\n  integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==\n\npako@~1.0.2:\n  version \"1.0.11\"\n  resolved \"https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf\"\n  integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==\n\nparent-module@^1.0.0:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2\"\n  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==\n  dependencies:\n    callsites \"^3.0.0\"\n\nparse-json@^5.2.0:\n  version \"5.2.0\"\n  resolved \"https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd\"\n  integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==\n  dependencies:\n    \"@babel/code-frame\" \"^7.0.0\"\n    error-ex \"^1.3.1\"\n    json-parse-even-better-errors \"^2.3.0\"\n    lines-and-columns \"^1.1.6\"\n\nparseley@^0.12.0:\n  version \"0.12.1\"\n  resolved \"https://registry.yarnpkg.com/parseley/-/parseley-0.12.1.tgz#4afd561d50215ebe259e3e7a853e62f600683aef\"\n  integrity sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==\n  dependencies:\n    leac \"^0.6.0\"\n    peberminta \"^0.9.0\"\n\nparseurl@~1.3.3:\n  version \"1.3.3\"\n  resolved \"https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4\"\n  integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==\n\npath-exists@^4.0.0:\n  version \"4.0.0\"\n  resolved \"https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3\"\n  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==\n\npath-is-absolute@^1.0.0:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f\"\n  integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==\n\npath-key@^3.0.0, path-key@^3.1.0:\n  version \"3.1.1\"\n  resolved \"https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375\"\n  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==\n\npath-scurry@^1.11.1:\n  version \"1.11.1\"\n  resolved \"https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2\"\n  integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==\n  dependencies:\n    lru-cache \"^10.2.0\"\n    minipass \"^5.0.0 || ^6.0.2 || ^7.0.0\"\n\npath-to-regexp@~0.1.12:\n  version \"0.1.12\"\n  resolved \"https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7\"\n  integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==\n\npath-type@^4.0.0:\n  version \"4.0.0\"\n  resolved \"https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b\"\n  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==\n\npdf-parse@^1.1.1:\n  version \"1.1.4\"\n  resolved \"https://registry.yarnpkg.com/pdf-parse/-/pdf-parse-1.1.4.tgz#97bca6f46758130dafb1fdd9df905efd07581f4a\"\n  integrity sha512-XRIRcLgk6ZnUbsHsYXExMw+krrPE81hJ6FQPLdBNhhBefqIQKXu/WeTgNBGSwPrfU0v+UCEwn7AoAUOsVKHFvQ==\n  dependencies:\n    node-ensure \"^0.0.0\"\n\npeberminta@^0.9.0:\n  version \"0.9.0\"\n  resolved \"https://registry.yarnpkg.com/peberminta/-/peberminta-0.9.0.tgz#8ec9bc0eb84b7d368126e71ce9033501dca2a352\"\n  integrity sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==\n\npeek-readable@^4.1.0:\n  version \"4.1.0\"\n  resolved \"https://registry.yarnpkg.com/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72\"\n  integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==\n\npend@~1.2.0:\n  version \"1.2.0\"\n  resolved \"https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50\"\n  integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==\n\npicocolors@^1.1.1:\n  version \"1.1.1\"\n  resolved \"https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b\"\n  integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==\n\npicomatch@^2.0.4, picomatch@^2.2.1:\n  version \"2.3.1\"\n  resolved \"https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42\"\n  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==\n\npify@^2.3.0:\n  version \"2.3.0\"\n  resolved \"https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c\"\n  integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==\n\npify@^3.0.0:\n  version \"3.0.0\"\n  resolved \"https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176\"\n  integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==\n\npinkie-promise@^2.0.0:\n  version \"2.0.1\"\n  resolved \"https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa\"\n  integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==\n  dependencies:\n    pinkie \"^2.0.0\"\n\npinkie@^2.0.0:\n  version \"2.0.4\"\n  resolved \"https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870\"\n  integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==\n\nplatform@^1.3.6:\n  version \"1.3.6\"\n  resolved \"https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7\"\n  integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==\n\npossible-typed-array-names@^1.0.0:\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae\"\n  integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==\n\nprebuild-install@^7.1.1:\n  version \"7.1.3\"\n  resolved \"https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec\"\n  integrity sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==\n  dependencies:\n    detect-libc \"^2.0.0\"\n    expand-template \"^2.0.3\"\n    github-from-package \"0.0.0\"\n    minimist \"^1.2.3\"\n    mkdirp-classic \"^0.5.3\"\n    napi-build-utils \"^2.0.0\"\n    node-abi \"^3.3.0\"\n    pump \"^3.0.0\"\n    rc \"^1.2.7\"\n    simple-get \"^4.0.0\"\n    tar-fs \"^2.0.0\"\n    tunnel-agent \"^0.6.0\"\n\nprelude-ls@^1.2.1:\n  version \"1.2.1\"\n  resolved \"https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396\"\n  integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==\n\nprettier-linter-helpers@^1.0.1:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz#6a31f88a4bad6c7adda253de12ba4edaea80ebcd\"\n  integrity sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==\n  dependencies:\n    fast-diff \"^1.1.2\"\n\nprettier@^2.4.1:\n  version \"2.8.8\"\n  resolved \"https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da\"\n  integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==\n\nprocess-nextick-args@~2.0.0:\n  version \"2.0.1\"\n  resolved \"https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2\"\n  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==\n\nprocess@^0.11.10:\n  version \"0.11.10\"\n  resolved \"https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182\"\n  integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==\n\nprogress@2.0.3:\n  version \"2.0.3\"\n  resolved \"https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8\"\n  integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==\n\nprotobufjs@^6.8.8:\n  version \"6.11.4\"\n  resolved \"https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.4.tgz#29a412c38bf70d89e537b6d02d904a6f448173aa\"\n  integrity sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==\n  dependencies:\n    \"@protobufjs/aspromise\" \"^1.1.2\"\n    \"@protobufjs/base64\" \"^1.1.2\"\n    \"@protobufjs/codegen\" \"^2.0.4\"\n    \"@protobufjs/eventemitter\" \"^1.1.0\"\n    \"@protobufjs/fetch\" \"^1.1.0\"\n    \"@protobufjs/float\" \"^1.0.2\"\n    \"@protobufjs/inquire\" \"^1.1.0\"\n    \"@protobufjs/path\" \"^1.1.2\"\n    \"@protobufjs/pool\" \"^1.1.0\"\n    \"@protobufjs/utf8\" \"^1.1.0\"\n    \"@types/long\" \"^4.0.1\"\n    \"@types/node\" \">=13.7.0\"\n    long \"^4.0.0\"\n\nproxy-addr@~2.0.7:\n  version \"2.0.7\"\n  resolved \"https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025\"\n  integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==\n  dependencies:\n    forwarded \"0.2.0\"\n    ipaddr.js \"1.9.1\"\n\nproxy-agent@6.3.1:\n  version \"6.3.1\"\n  resolved \"https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-6.3.1.tgz#40e7b230552cf44fd23ffaf7c59024b692612687\"\n  integrity sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==\n  dependencies:\n    agent-base \"^7.0.2\"\n    debug \"^4.3.4\"\n    http-proxy-agent \"^7.0.0\"\n    https-proxy-agent \"^7.0.2\"\n    lru-cache \"^7.14.1\"\n    pac-proxy-agent \"^7.0.1\"\n    proxy-from-env \"^1.1.0\"\n    socks-proxy-agent \"^8.0.2\"\n\nproxy-from-env@^1.1.0:\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2\"\n  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==\n\npstree.remy@^1.1.8:\n  version \"1.1.8\"\n  resolved \"https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a\"\n  integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==\n\npump@^3.0.0:\n  version \"3.0.3\"\n  resolved \"https://registry.yarnpkg.com/pump/-/pump-3.0.3.tgz#151d979f1a29668dc0025ec589a455b53282268d\"\n  integrity sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==\n  dependencies:\n    end-of-stream \"^1.1.0\"\n    once \"^1.3.1\"\n\npunycode.js@2.3.1:\n  version \"2.3.1\"\n  resolved \"https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7\"\n  integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==\n\npunycode@^2.1.0:\n  version \"2.3.1\"\n  resolved \"https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5\"\n  integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==\n\npuppeteer-core@21.5.2:\n  version \"21.5.2\"\n  resolved \"https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-21.5.2.tgz#6d3de4efb2ae65f1ee072043787b75594e88035f\"\n  integrity sha512-v4T0cWnujSKs+iEfmb8ccd7u4/x8oblEyKqplqKnJ582Kw8PewYAWvkH4qUWhitN3O2q9RF7dzkvjyK5HbzjLA==\n  dependencies:\n    \"@puppeteer/browsers\" \"1.8.0\"\n    chromium-bidi \"0.4.33\"\n    cross-fetch \"4.0.0\"\n    debug \"4.3.4\"\n    devtools-protocol \"0.0.1203626\"\n    ws \"8.14.2\"\n\npuppeteer@~21.5.2:\n  version \"21.5.2\"\n  resolved \"https://registry.yarnpkg.com/puppeteer/-/puppeteer-21.5.2.tgz#0a4a72175c0fd0944d6486f4734807e1671d527b\"\n  integrity sha512-BaAGJOq8Fl6/cck6obmwaNLksuY0Bg/lIahCLhJPGXBFUD2mCffypa4A592MaWnDcye7eaHmSK9yot0pxctY8A==\n  dependencies:\n    \"@puppeteer/browsers\" \"1.8.0\"\n    cosmiconfig \"8.3.6\"\n    puppeteer-core \"21.5.2\"\n\nqs@~6.14.0:\n  version \"6.14.0\"\n  resolved \"https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930\"\n  integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==\n  dependencies:\n    side-channel \"^1.1.0\"\n\nrange-parser@~1.2.1:\n  version \"1.2.1\"\n  resolved \"https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031\"\n  integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==\n\nraw-body@~2.5.3:\n  version \"2.5.3\"\n  resolved \"https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2\"\n  integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==\n  dependencies:\n    bytes \"~3.1.2\"\n    http-errors \"~2.0.1\"\n    iconv-lite \"~0.4.24\"\n    unpipe \"~1.0.0\"\n\nrc@^1.2.7:\n  version \"1.2.8\"\n  resolved \"https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed\"\n  integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==\n  dependencies:\n    deep-extend \"^0.6.0\"\n    ini \"~1.3.0\"\n    minimist \"^1.2.0\"\n    strip-json-comments \"~2.0.1\"\n\nreadable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6:\n  version \"2.3.8\"\n  resolved \"https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b\"\n  integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==\n  dependencies:\n    core-util-is \"~1.0.0\"\n    inherits \"~2.0.3\"\n    isarray \"~1.0.0\"\n    process-nextick-args \"~2.0.0\"\n    safe-buffer \"~5.1.1\"\n    string_decoder \"~1.1.1\"\n    util-deprecate \"~1.0.1\"\n\nreadable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.2:\n  version \"3.6.2\"\n  resolved \"https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967\"\n  integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==\n  dependencies:\n    inherits \"^2.0.3\"\n    string_decoder \"^1.1.1\"\n    util-deprecate \"^1.0.1\"\n\nreadable-stream@^4.7.0:\n  version \"4.7.0\"\n  resolved \"https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91\"\n  integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==\n  dependencies:\n    abort-controller \"^3.0.0\"\n    buffer \"^6.0.3\"\n    events \"^3.3.0\"\n    process \"^0.11.10\"\n    string_decoder \"^1.3.0\"\n\nreadable-web-to-node-stream@^3.0.0:\n  version \"3.0.4\"\n  resolved \"https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz#392ba37707af5bf62d725c36c1b5d6ef4119eefc\"\n  integrity sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==\n  dependencies:\n    readable-stream \"^4.7.0\"\n\nreaddirp@~3.6.0:\n  version \"3.6.0\"\n  resolved \"https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7\"\n  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==\n  dependencies:\n    picomatch \"^2.2.1\"\n\nregenerator-runtime@^0.13.3:\n  version \"0.13.11\"\n  resolved \"https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9\"\n  integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==\n\nrequire-directory@^2.1.1:\n  version \"2.1.1\"\n  resolved \"https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42\"\n  integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==\n\nresolve-from@^4.0.0:\n  version \"4.0.0\"\n  resolved \"https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6\"\n  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==\n\nretry@^0.13.1:\n  version \"0.13.1\"\n  resolved \"https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658\"\n  integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==\n\nrimraf@^5.0.10:\n  version \"5.0.10\"\n  resolved \"https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.10.tgz#23b9843d3dc92db71f96e1a2ce92e39fd2a8221c\"\n  integrity sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==\n  dependencies:\n    glob \"^10.3.7\"\n\nsafe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.2.1, safe-buffer@~5.2.0:\n  version \"5.2.1\"\n  resolved \"https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6\"\n  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==\n\nsafe-buffer@~5.1.0, safe-buffer@~5.1.1:\n  version \"5.1.2\"\n  resolved \"https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d\"\n  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==\n\nsafe-stable-stringify@^2.3.1:\n  version \"2.5.0\"\n  resolved \"https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd\"\n  integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==\n\n\"safer-buffer@>= 2.1.2 < 3\", \"safer-buffer@>= 2.1.2 < 3.0.0\":\n  version \"2.1.2\"\n  resolved \"https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a\"\n  integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==\n\nsax@>=0.6.0:\n  version \"1.4.3\"\n  resolved \"https://registry.yarnpkg.com/sax/-/sax-1.4.3.tgz#fcebae3b756cdc8428321805f4b70f16ec0ab5db\"\n  integrity sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==\n\nseek-bzip@^1.0.5:\n  version \"1.0.6\"\n  resolved \"https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.6.tgz#35c4171f55a680916b52a07859ecf3b5857f21c4\"\n  integrity sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==\n  dependencies:\n    commander \"^2.8.1\"\n\nselderee@^0.11.0:\n  version \"0.11.0\"\n  resolved \"https://registry.yarnpkg.com/selderee/-/selderee-0.11.0.tgz#6af0c7983e073ad3e35787ffe20cefd9daf0ec8a\"\n  integrity sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==\n  dependencies:\n    parseley \"^0.12.0\"\n\nsemver@^5.7.1:\n  version \"5.7.2\"\n  resolved \"https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8\"\n  integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==\n\nsemver@^7.3.5, semver@^7.5.4, semver@^7.6.3:\n  version \"7.7.3\"\n  resolved \"https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946\"\n  integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==\n\nsemver@~7.0.0:\n  version \"7.0.0\"\n  resolved \"https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e\"\n  integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==\n\nsend@0.19.0:\n  version \"0.19.0\"\n  resolved \"https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8\"\n  integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==\n  dependencies:\n    debug \"2.6.9\"\n    depd \"2.0.0\"\n    destroy \"1.2.0\"\n    encodeurl \"~1.0.2\"\n    escape-html \"~1.0.3\"\n    etag \"~1.8.1\"\n    fresh \"0.5.2\"\n    http-errors \"2.0.0\"\n    mime \"1.6.0\"\n    ms \"2.1.3\"\n    on-finished \"2.4.1\"\n    range-parser \"~1.2.1\"\n    statuses \"2.0.1\"\n\nsend@~0.19.0:\n  version \"0.19.1\"\n  resolved \"https://registry.yarnpkg.com/send/-/send-0.19.1.tgz#1c2563b2ee4fe510b806b21ec46f355005a369f9\"\n  integrity sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==\n  dependencies:\n    debug \"2.6.9\"\n    depd \"2.0.0\"\n    destroy \"1.2.0\"\n    encodeurl \"~2.0.0\"\n    escape-html \"~1.0.3\"\n    etag \"~1.8.1\"\n    fresh \"0.5.2\"\n    http-errors \"2.0.0\"\n    mime \"1.6.0\"\n    ms \"2.1.3\"\n    on-finished \"2.4.1\"\n    range-parser \"~1.2.1\"\n    statuses \"2.0.1\"\n\nserve-static@~1.16.2:\n  version \"1.16.2\"\n  resolved \"https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296\"\n  integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==\n  dependencies:\n    encodeurl \"~2.0.0\"\n    escape-html \"~1.0.3\"\n    parseurl \"~1.3.3\"\n    send \"0.19.0\"\n\nset-function-length@^1.2.2:\n  version \"1.2.2\"\n  resolved \"https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449\"\n  integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==\n  dependencies:\n    define-data-property \"^1.1.4\"\n    es-errors \"^1.3.0\"\n    function-bind \"^1.1.2\"\n    get-intrinsic \"^1.2.4\"\n    gopd \"^1.0.1\"\n    has-property-descriptors \"^1.0.2\"\n\nsetimmediate@^1.0.5:\n  version \"1.0.5\"\n  resolved \"https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285\"\n  integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==\n\nsetprototypeof@1.2.0, setprototypeof@~1.2.0:\n  version \"1.2.0\"\n  resolved \"https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424\"\n  integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==\n\nsharp@^0.32.0:\n  version \"0.32.6\"\n  resolved \"https://registry.yarnpkg.com/sharp/-/sharp-0.32.6.tgz#6ad30c0b7cd910df65d5f355f774aa4fce45732a\"\n  integrity sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==\n  dependencies:\n    color \"^4.2.3\"\n    detect-libc \"^2.0.2\"\n    node-addon-api \"^6.1.0\"\n    prebuild-install \"^7.1.1\"\n    semver \"^7.5.4\"\n    simple-get \"^4.0.1\"\n    tar-fs \"^3.0.4\"\n    tunnel-agent \"^0.6.0\"\n\nsharp@^0.33.5:\n  version \"0.33.5\"\n  resolved \"https://registry.yarnpkg.com/sharp/-/sharp-0.33.5.tgz#13e0e4130cc309d6a9497596715240b2ec0c594e\"\n  integrity sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==\n  dependencies:\n    color \"^4.2.3\"\n    detect-libc \"^2.0.3\"\n    semver \"^7.6.3\"\n  optionalDependencies:\n    \"@img/sharp-darwin-arm64\" \"0.33.5\"\n    \"@img/sharp-darwin-x64\" \"0.33.5\"\n    \"@img/sharp-libvips-darwin-arm64\" \"1.0.4\"\n    \"@img/sharp-libvips-darwin-x64\" \"1.0.4\"\n    \"@img/sharp-libvips-linux-arm\" \"1.0.5\"\n    \"@img/sharp-libvips-linux-arm64\" \"1.0.4\"\n    \"@img/sharp-libvips-linux-s390x\" \"1.0.4\"\n    \"@img/sharp-libvips-linux-x64\" \"1.0.4\"\n    \"@img/sharp-libvips-linuxmusl-arm64\" \"1.0.4\"\n    \"@img/sharp-libvips-linuxmusl-x64\" \"1.0.4\"\n    \"@img/sharp-linux-arm\" \"0.33.5\"\n    \"@img/sharp-linux-arm64\" \"0.33.5\"\n    \"@img/sharp-linux-s390x\" \"0.33.5\"\n    \"@img/sharp-linux-x64\" \"0.33.5\"\n    \"@img/sharp-linuxmusl-arm64\" \"0.33.5\"\n    \"@img/sharp-linuxmusl-x64\" \"0.33.5\"\n    \"@img/sharp-wasm32\" \"0.33.5\"\n    \"@img/sharp-win32-ia32\" \"0.33.5\"\n    \"@img/sharp-win32-x64\" \"0.33.5\"\n\nshebang-command@^2.0.0:\n  version \"2.0.0\"\n  resolved \"https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea\"\n  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==\n  dependencies:\n    shebang-regex \"^3.0.0\"\n\nshebang-regex@^3.0.0:\n  version \"3.0.0\"\n  resolved \"https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172\"\n  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==\n\nshell-env@^4.0.1:\n  version \"4.0.1\"\n  resolved \"https://registry.yarnpkg.com/shell-env/-/shell-env-4.0.1.tgz#883302d9426095d398a39b102a851adb306b8cb8\"\n  integrity sha512-w3oeZ9qg/P6Lu6qqwavvMnB/bwfsz67gPB3WXmLd/n6zuh7TWQZtGa3iMEdmua0kj8rivkwl+vUjgLWlqZOMPw==\n  dependencies:\n    default-shell \"^2.0.0\"\n    execa \"^5.1.1\"\n    strip-ansi \"^7.0.1\"\n\nshell-path@^3.0.0:\n  version \"3.1.0\"\n  resolved \"https://registry.yarnpkg.com/shell-path/-/shell-path-3.1.0.tgz#950671fe15de70fb4d984b886d55e8a2f10bfe33\"\n  integrity sha512-s/9q9PEtcRmDTz69+cJ3yYBAe9yGrL7e46gm2bU4pQ9N48ecPK9QrGFnLwYgb4smOHskx4PL7wCNMktW2AoD+g==\n  dependencies:\n    shell-env \"^4.0.1\"\n\nside-channel-list@^1.0.0:\n  version \"1.0.0\"\n  resolved \"https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad\"\n  integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==\n  dependencies:\n    es-errors \"^1.3.0\"\n    object-inspect \"^1.13.3\"\n\nside-channel-map@^1.0.1:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42\"\n  integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==\n  dependencies:\n    call-bound \"^1.0.2\"\n    es-errors \"^1.3.0\"\n    get-intrinsic \"^1.2.5\"\n    object-inspect \"^1.13.3\"\n\nside-channel-weakmap@^1.0.2:\n  version \"1.0.2\"\n  resolved \"https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea\"\n  integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==\n  dependencies:\n    call-bound \"^1.0.2\"\n    es-errors \"^1.3.0\"\n    get-intrinsic \"^1.2.5\"\n    object-inspect \"^1.13.3\"\n    side-channel-map \"^1.0.1\"\n\nside-channel@^1.1.0:\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9\"\n  integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==\n  dependencies:\n    es-errors \"^1.3.0\"\n    object-inspect \"^1.13.3\"\n    side-channel-list \"^1.0.0\"\n    side-channel-map \"^1.0.1\"\n    side-channel-weakmap \"^1.0.2\"\n\nsignal-exit@^3.0.3:\n  version \"3.0.7\"\n  resolved \"https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9\"\n  integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==\n\nsignal-exit@^4.0.1:\n  version \"4.1.0\"\n  resolved \"https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04\"\n  integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==\n\nsimple-concat@^1.0.0:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f\"\n  integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==\n\nsimple-get@^4.0.0, simple-get@^4.0.1:\n  version \"4.0.1\"\n  resolved \"https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543\"\n  integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==\n  dependencies:\n    decompress-response \"^6.0.0\"\n    once \"^1.3.1\"\n    simple-concat \"^1.0.0\"\n\nsimple-swizzle@^0.2.2:\n  version \"0.2.4\"\n  resolved \"https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz#a8d11a45a11600d6a1ecdff6363329e3648c3667\"\n  integrity sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==\n  dependencies:\n    is-arrayish \"^0.3.1\"\n\nsimple-update-notifier@^1.0.7:\n  version \"1.1.0\"\n  resolved \"https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz#67694c121de354af592b347cdba798463ed49c82\"\n  integrity sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==\n  dependencies:\n    semver \"~7.0.0\"\n\nslugify@^1.6.6:\n  version \"1.6.6\"\n  resolved \"https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b\"\n  integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==\n\nsmart-buffer@^4.2.0:\n  version \"4.2.0\"\n  resolved \"https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae\"\n  integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==\n\nsocks-proxy-agent@^8.0.2, socks-proxy-agent@^8.0.5:\n  version \"8.0.5\"\n  resolved \"https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz#b9cdb4e7e998509d7659d689ce7697ac21645bee\"\n  integrity sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==\n  dependencies:\n    agent-base \"^7.1.2\"\n    debug \"^4.3.4\"\n    socks \"^2.8.3\"\n\nsocks@^2.8.3:\n  version \"2.8.7\"\n  resolved \"https://registry.yarnpkg.com/socks/-/socks-2.8.7.tgz#e2fb1d9a603add75050a2067db8c381a0b5669ea\"\n  integrity sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==\n  dependencies:\n    ip-address \"^10.0.1\"\n    smart-buffer \"^4.2.0\"\n\nsource-map@~0.6.1:\n  version \"0.6.1\"\n  resolved \"https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263\"\n  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==\n\nsprintf-js@~1.0.2:\n  version \"1.0.3\"\n  resolved \"https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c\"\n  integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==\n\nstack-trace@0.0.x:\n  version \"0.0.10\"\n  resolved \"https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0\"\n  integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==\n\nstatuses@2.0.1:\n  version \"2.0.1\"\n  resolved \"https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63\"\n  integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==\n\nstatuses@~2.0.1, statuses@~2.0.2:\n  version \"2.0.2\"\n  resolved \"https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382\"\n  integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==\n\nstreamx@^2.15.0, streamx@^2.21.0:\n  version \"2.23.0\"\n  resolved \"https://registry.yarnpkg.com/streamx/-/streamx-2.23.0.tgz#7d0f3d00d4a6c5de5728aecd6422b4008d66fd0b\"\n  integrity sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==\n  dependencies:\n    events-universal \"^1.0.0\"\n    fast-fifo \"^1.3.2\"\n    text-decoder \"^1.1.0\"\n\n\"string-width-cjs@npm:string-width@^4.2.0\":\n  version \"4.2.3\"\n  resolved \"https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010\"\n  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==\n  dependencies:\n    emoji-regex \"^8.0.0\"\n    is-fullwidth-code-point \"^3.0.0\"\n    strip-ansi \"^6.0.1\"\n\nstring-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3, string-width@^5.1.2:\n  version \"4.2.3\"\n  resolved \"https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010\"\n  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==\n  dependencies:\n    emoji-regex \"^8.0.0\"\n    is-fullwidth-code-point \"^3.0.0\"\n    strip-ansi \"^6.0.1\"\n\nstring_decoder@^1.1.1, string_decoder@^1.3.0:\n  version \"1.3.0\"\n  resolved \"https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e\"\n  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==\n  dependencies:\n    safe-buffer \"~5.2.0\"\n\nstring_decoder@~1.1.1:\n  version \"1.1.1\"\n  resolved \"https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8\"\n  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==\n  dependencies:\n    safe-buffer \"~5.1.0\"\n\n\"strip-ansi-cjs@npm:strip-ansi@^6.0.1\":\n  version \"6.0.1\"\n  resolved \"https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9\"\n  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==\n  dependencies:\n    ansi-regex \"^5.0.1\"\n\nstrip-ansi@^6.0.0, strip-ansi@^6.0.1:\n  version \"6.0.1\"\n  resolved \"https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9\"\n  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==\n  dependencies:\n    ansi-regex \"^5.0.1\"\n\nstrip-ansi@^7.0.1, strip-ansi@^7.1.2:\n  version \"7.1.2\"\n  resolved \"https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba\"\n  integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==\n  dependencies:\n    ansi-regex \"^6.0.1\"\n\nstrip-dirs@^2.0.0:\n  version \"2.1.0\"\n  resolved \"https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5\"\n  integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==\n  dependencies:\n    is-natural-number \"^4.0.1\"\n\nstrip-final-newline@^2.0.0:\n  version \"2.0.0\"\n  resolved \"https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad\"\n  integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==\n\nstrip-json-comments@^3.1.1:\n  version \"3.1.1\"\n  resolved \"https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006\"\n  integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==\n\nstrip-json-comments@~2.0.1:\n  version \"2.0.1\"\n  resolved \"https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a\"\n  integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==\n\nstrtok3@^6.2.4:\n  version \"6.3.0\"\n  resolved \"https://registry.yarnpkg.com/strtok3/-/strtok3-6.3.0.tgz#358b80ffe6d5d5620e19a073aa78ce947a90f9a0\"\n  integrity sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==\n  dependencies:\n    \"@tokenizer/token\" \"^0.3.0\"\n    peek-readable \"^4.1.0\"\n\nsupports-color@^5.5.0:\n  version \"5.5.0\"\n  resolved \"https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f\"\n  integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==\n  dependencies:\n    has-flag \"^3.0.0\"\n\nsupports-color@^7.1.0:\n  version \"7.2.0\"\n  resolved \"https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da\"\n  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==\n  dependencies:\n    has-flag \"^4.0.0\"\n\nsynckit@^0.11.12:\n  version \"0.11.12\"\n  resolved \"https://registry.yarnpkg.com/synckit/-/synckit-0.11.12.tgz#abe74124264fbc00a48011b0d98bdc1cffb64a7b\"\n  integrity sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==\n  dependencies:\n    \"@pkgr/core\" \"^0.2.9\"\n\ntar-fs@3.0.4:\n  version \"3.0.4\"\n  resolved \"https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.4.tgz#a21dc60a2d5d9f55e0089ccd78124f1d3771dbbf\"\n  integrity sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==\n  dependencies:\n    mkdirp-classic \"^0.5.2\"\n    pump \"^3.0.0\"\n    tar-stream \"^3.1.5\"\n\ntar-fs@^2.0.0:\n  version \"2.1.4\"\n  resolved \"https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.4.tgz#800824dbf4ef06ded9afea4acafe71c67c76b930\"\n  integrity sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==\n  dependencies:\n    chownr \"^1.1.1\"\n    mkdirp-classic \"^0.5.2\"\n    pump \"^3.0.0\"\n    tar-stream \"^2.1.4\"\n\ntar-fs@^3.0.4:\n  version \"3.1.1\"\n  resolved \"https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.1.1.tgz#4f164e59fb60f103d472360731e8c6bb4a7fe9ef\"\n  integrity sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==\n  dependencies:\n    pump \"^3.0.0\"\n    tar-stream \"^3.1.5\"\n  optionalDependencies:\n    bare-fs \"^4.0.1\"\n    bare-path \"^3.0.0\"\n\ntar-stream@^1.5.2:\n  version \"1.6.2\"\n  resolved \"https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555\"\n  integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==\n  dependencies:\n    bl \"^1.0.0\"\n    buffer-alloc \"^1.2.0\"\n    end-of-stream \"^1.0.0\"\n    fs-constants \"^1.0.0\"\n    readable-stream \"^2.3.0\"\n    to-buffer \"^1.1.1\"\n    xtend \"^4.0.0\"\n\ntar-stream@^2.1.4:\n  version \"2.2.0\"\n  resolved \"https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287\"\n  integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==\n  dependencies:\n    bl \"^4.0.3\"\n    end-of-stream \"^1.4.1\"\n    fs-constants \"^1.0.0\"\n    inherits \"^2.0.3\"\n    readable-stream \"^3.1.1\"\n\ntar-stream@^3.1.5:\n  version \"3.1.7\"\n  resolved \"https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b\"\n  integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==\n  dependencies:\n    b4a \"^1.6.4\"\n    fast-fifo \"^1.2.0\"\n    streamx \"^2.15.0\"\n\ntesseract.js-core@^6.0.0:\n  version \"6.0.0\"\n  resolved \"https://registry.yarnpkg.com/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz#6f25da94f70f8e8f02aff47a43be61d49e6f67c3\"\n  integrity sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==\n\ntesseract.js@^6.0.0:\n  version \"6.0.1\"\n  resolved \"https://registry.yarnpkg.com/tesseract.js/-/tesseract.js-6.0.1.tgz#5b2ff39aae92d59cef79589a43a0f3ab963801cc\"\n  integrity sha512-/sPvMvrCtgxnNRCjbTYbr7BRu0yfWDsMZQ2a/T5aN/L1t8wUQN6tTWv6p6FwzpoEBA0jrN2UD2SX4QQFRdoDbA==\n  dependencies:\n    bmp-js \"^0.1.0\"\n    idb-keyval \"^6.2.0\"\n    is-url \"^1.2.4\"\n    node-fetch \"^2.6.9\"\n    opencollective-postinstall \"^2.0.3\"\n    regenerator-runtime \"^0.13.3\"\n    tesseract.js-core \"^6.0.0\"\n    wasm-feature-detect \"^1.2.11\"\n    zlibjs \"^0.3.1\"\n\ntext-decoder@^1.1.0:\n  version \"1.2.3\"\n  resolved \"https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.3.tgz#b19da364d981b2326d5f43099c310cc80d770c65\"\n  integrity sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==\n  dependencies:\n    b4a \"^1.6.4\"\n\ntext-hex@1.0.x:\n  version \"1.0.0\"\n  resolved \"https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5\"\n  integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==\n\nthrough@^2.3.8:\n  version \"2.3.8\"\n  resolved \"https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5\"\n  integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==\n\ntlds@1.261.0:\n  version \"1.261.0\"\n  resolved \"https://registry.yarnpkg.com/tlds/-/tlds-1.261.0.tgz#055e412e92f01f84a9c8ac0504d3472d68b3c4c9\"\n  integrity sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==\n\nto-buffer@^1.1.1:\n  version \"1.2.2\"\n  resolved \"https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.2.tgz#ffe59ef7522ada0a2d1cb5dfe03bb8abc3cdc133\"\n  integrity sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==\n  dependencies:\n    isarray \"^2.0.5\"\n    safe-buffer \"^5.2.1\"\n    typed-array-buffer \"^1.0.3\"\n\nto-regex-range@^5.0.1:\n  version \"5.0.1\"\n  resolved \"https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4\"\n  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==\n  dependencies:\n    is-number \"^7.0.0\"\n\ntoidentifier@1.0.1, toidentifier@~1.0.1:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35\"\n  integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==\n\ntoken-types@^4.1.1:\n  version \"4.2.1\"\n  resolved \"https://registry.yarnpkg.com/token-types/-/token-types-4.2.1.tgz#0f897f03665846982806e138977dbe72d44df753\"\n  integrity sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==\n  dependencies:\n    \"@tokenizer/token\" \"^0.3.0\"\n    ieee754 \"^1.2.1\"\n\ntouch@^3.1.0:\n  version \"3.1.1\"\n  resolved \"https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694\"\n  integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==\n\ntr46@~0.0.3:\n  version \"0.0.3\"\n  resolved \"https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a\"\n  integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==\n\ntriple-beam@^1.3.0:\n  version \"1.4.1\"\n  resolved \"https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984\"\n  integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==\n\nts-type@>=2:\n  version \"3.0.1\"\n  resolved \"https://registry.yarnpkg.com/ts-type/-/ts-type-3.0.1.tgz#b52e7623065e0beb43c77c426347d85cf81dff84\"\n  integrity sha512-cleRydCkBGBFQ4KAvLH0ARIkciduS745prkGVVxPGvcRGhMMoSJUB7gNR1ByKhFTEYrYRg2CsMRGYnqp+6op+g==\n  dependencies:\n    \"@types/node\" \"*\"\n    tslib \">=2\"\n    typedarray-dts \"^1.0.0\"\n\ntslib@>=2, tslib@^2.0.1, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.6.2:\n  version \"2.8.1\"\n  resolved \"https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f\"\n  integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==\n\ntunnel-agent@^0.6.0:\n  version \"0.6.0\"\n  resolved \"https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd\"\n  integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==\n  dependencies:\n    safe-buffer \"^5.0.1\"\n\ntype-check@^0.4.0, type-check@~0.4.0:\n  version \"0.4.0\"\n  resolved \"https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1\"\n  integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==\n  dependencies:\n    prelude-ls \"^1.2.1\"\n\ntype-detect@^4.0.0:\n  version \"4.1.0\"\n  resolved \"https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c\"\n  integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==\n\ntype-is@~1.6.18:\n  version \"1.6.18\"\n  resolved \"https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131\"\n  integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==\n  dependencies:\n    media-typer \"0.3.0\"\n    mime-types \"~2.1.24\"\n\ntyped-array-buffer@^1.0.3:\n  version \"1.0.3\"\n  resolved \"https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536\"\n  integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==\n  dependencies:\n    call-bound \"^1.0.3\"\n    es-errors \"^1.3.0\"\n    is-typed-array \"^1.1.14\"\n\ntypedarray-dts@^1.0.0:\n  version \"1.0.0\"\n  resolved \"https://registry.yarnpkg.com/typedarray-dts/-/typedarray-dts-1.0.0.tgz#9dec9811386dbfba964c295c2606cf9a6b982d06\"\n  integrity sha512-Ka0DBegjuV9IPYFT1h0Qqk5U4pccebNIJCGl8C5uU7xtOs+jpJvKGAY4fHGK25hTmXZOEUl9Cnsg5cS6K/b5DA==\n\nuc.micro@^2.0.0:\n  version \"2.1.0\"\n  resolved \"https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee\"\n  integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==\n\nunbzip2-stream@1.4.3, unbzip2-stream@^1.0.9:\n  version \"1.4.3\"\n  resolved \"https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7\"\n  integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==\n  dependencies:\n    buffer \"^5.2.1\"\n    through \"^2.3.8\"\n\nundefsafe@^2.0.5:\n  version \"2.0.5\"\n  resolved \"https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c\"\n  integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==\n\nunderscore@^1.13.1:\n  version \"1.13.7\"\n  resolved \"https://registry.yarnpkg.com/underscore/-/underscore-1.13.7.tgz#970e33963af9a7dda228f17ebe8399e5fbe63a10\"\n  integrity sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==\n\nundici-types@~5.26.4:\n  version \"5.26.5\"\n  resolved \"https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617\"\n  integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==\n\nundici-types@~7.16.0:\n  version \"7.16.0\"\n  resolved \"https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46\"\n  integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==\n\nundici@^5.19.1:\n  version \"5.29.0\"\n  resolved \"https://registry.yarnpkg.com/undici/-/undici-5.29.0.tgz#419595449ae3f2cdcba3580a2e8903399bd1f5a3\"\n  integrity sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==\n  dependencies:\n    \"@fastify/busboy\" \"^2.0.0\"\n\nunpipe@~1.0.0:\n  version \"1.0.0\"\n  resolved \"https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec\"\n  integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==\n\nuri-js@^4.2.2:\n  version \"4.4.1\"\n  resolved \"https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e\"\n  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==\n  dependencies:\n    punycode \"^2.1.0\"\n\nurl-pattern@^1.0.3:\n  version \"1.0.3\"\n  resolved \"https://registry.yarnpkg.com/url-pattern/-/url-pattern-1.0.3.tgz#0409292471b24f23c50d65a47931793d2b5acfc1\"\n  integrity sha512-uQcEj/2puA4aq1R3A2+VNVBgaWYR24FdWjl7VNW83rnWftlhyzOZ/tBjezRiC2UkIzuxC8Top3IekN3vUf1WxA==\n\nurlpattern-polyfill@9.0.0:\n  version \"9.0.0\"\n  resolved \"https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-9.0.0.tgz#bc7e386bb12fd7898b58d1509df21d3c29ab3460\"\n  integrity sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==\n\nutil-deprecate@^1.0.1, util-deprecate@~1.0.1:\n  version \"1.0.2\"\n  resolved \"https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf\"\n  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==\n\nutils-merge@1.0.1:\n  version \"1.0.1\"\n  resolved \"https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713\"\n  integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==\n\nuuid@^10.0.0:\n  version \"10.0.0\"\n  resolved \"https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294\"\n  integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==\n\nuuid@^9.0.0:\n  version \"9.0.1\"\n  resolved \"https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30\"\n  integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==\n\nvary@^1, vary@~1.1.2:\n  version \"1.1.2\"\n  resolved \"https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc\"\n  integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==\n\nwasm-feature-detect@^1.2.11:\n  version \"1.8.0\"\n  resolved \"https://registry.yarnpkg.com/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz#4e9f55b0a64d801f372fbb0324ed11ad3abd0c78\"\n  integrity sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==\n\nwavefile@^11.0.0:\n  version \"11.0.0\"\n  resolved \"https://registry.yarnpkg.com/wavefile/-/wavefile-11.0.0.tgz#9302165874327ff63a704d00b154c753eaa1b8e7\"\n  integrity sha512-/OBiAALgWU24IG7sC84cDO/KfFuvajWc5Uec0oV2zrpOOZZDgGdOwHwgEzOrwh8jkubBk7PtZfQBIcI1OaE5Ng==\n\nweb-streams-polyfill@4.0.0-beta.3:\n  version \"4.0.0-beta.3\"\n  resolved \"https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38\"\n  integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==\n\nweb-streams-polyfill@^3.2.1:\n  version \"3.3.3\"\n  resolved \"https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b\"\n  integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==\n\nwebidl-conversions@^3.0.0:\n  version \"3.0.1\"\n  resolved \"https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871\"\n  integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==\n\nwhatwg-url@^5.0.0:\n  version \"5.0.0\"\n  resolved \"https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d\"\n  integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==\n  dependencies:\n    tr46 \"~0.0.3\"\n    webidl-conversions \"^3.0.0\"\n\nwhich-typed-array@^1.1.16:\n  version \"1.1.19\"\n  resolved \"https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956\"\n  integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==\n  dependencies:\n    available-typed-arrays \"^1.0.7\"\n    call-bind \"^1.0.8\"\n    call-bound \"^1.0.4\"\n    for-each \"^0.3.5\"\n    get-proto \"^1.0.1\"\n    gopd \"^1.2.0\"\n    has-tostringtag \"^1.0.2\"\n\nwhich@^2.0.1:\n  version \"2.0.2\"\n  resolved \"https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1\"\n  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==\n  dependencies:\n    isexe \"^2.0.0\"\n\nwinston-transport@^4.9.0:\n  version \"4.9.0\"\n  resolved \"https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.9.0.tgz#3bba345de10297654ea6f33519424560003b3bf9\"\n  integrity sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==\n  dependencies:\n    logform \"^2.7.0\"\n    readable-stream \"^3.6.2\"\n    triple-beam \"^1.3.0\"\n\nwinston@^3.13.0:\n  version \"3.18.3\"\n  resolved \"https://registry.yarnpkg.com/winston/-/winston-3.18.3.tgz#93ac10808c8e1081d723bc8811cd2f445ddfdcd1\"\n  integrity sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==\n  dependencies:\n    \"@colors/colors\" \"^1.6.0\"\n    \"@dabh/diagnostics\" \"^2.0.8\"\n    async \"^3.2.3\"\n    is-stream \"^2.0.0\"\n    logform \"^2.7.0\"\n    one-time \"^1.0.0\"\n    readable-stream \"^3.4.0\"\n    safe-stable-stringify \"^2.3.1\"\n    stack-trace \"0.0.x\"\n    triple-beam \"^1.3.0\"\n    winston-transport \"^4.9.0\"\n\nword-wrap@^1.2.5:\n  version \"1.2.5\"\n  resolved \"https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34\"\n  integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==\n\n\"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0\":\n  version \"7.0.0\"\n  resolved \"https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43\"\n  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==\n  dependencies:\n    ansi-styles \"^4.0.0\"\n    string-width \"^4.1.0\"\n    strip-ansi \"^6.0.0\"\n\nwrap-ansi@^7.0.0, wrap-ansi@^8.1.0:\n  version \"7.0.0\"\n  resolved \"https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43\"\n  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==\n  dependencies:\n    ansi-styles \"^4.0.0\"\n    string-width \"^4.1.0\"\n    strip-ansi \"^6.0.0\"\n\nwrappy@1:\n  version \"1.0.2\"\n  resolved \"https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f\"\n  integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==\n\nws@8.14.2:\n  version \"8.14.2\"\n  resolved \"https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f\"\n  integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==\n\n\"xlsx@https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz\":\n  version \"0.20.2\"\n  resolved \"https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz#0f64eeed3f1a46e64724620c3553f2dbd3cd2d7d\"\n\nxml2js@^0.6.2:\n  version \"0.6.2\"\n  resolved \"https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499\"\n  integrity sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==\n  dependencies:\n    sax \">=0.6.0\"\n    xmlbuilder \"~11.0.0\"\n\nxmlbuilder@^10.0.0:\n  version \"10.1.1\"\n  resolved \"https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-10.1.1.tgz#8cae6688cc9b38d850b7c8d3c0a4161dcaf475b0\"\n  integrity sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==\n\nxmlbuilder@~11.0.0:\n  version \"11.0.1\"\n  resolved \"https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3\"\n  integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==\n\nxtend@^4.0.0:\n  version \"4.0.2\"\n  resolved \"https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54\"\n  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==\n\ny18n@^5.0.5:\n  version \"5.0.8\"\n  resolved \"https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55\"\n  integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==\n\nyaml@^2.2.1:\n  version \"2.8.1\"\n  resolved \"https://registry.yarnpkg.com/yaml/-/yaml-2.8.1.tgz#1870aa02b631f7e8328b93f8bc574fac5d6c4d79\"\n  integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==\n\nyargs-parser@^21.1.1:\n  version \"21.1.1\"\n  resolved \"https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35\"\n  integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==\n\nyargs@17.7.2:\n  version \"17.7.2\"\n  resolved \"https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269\"\n  integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==\n  dependencies:\n    cliui \"^8.0.1\"\n    escalade \"^3.1.1\"\n    get-caller-file \"^2.0.5\"\n    require-directory \"^2.1.1\"\n    string-width \"^4.2.3\"\n    y18n \"^5.0.5\"\n    yargs-parser \"^21.1.1\"\n\nyauzl@^2.10.0, yauzl@^2.4.2:\n  version \"2.10.0\"\n  resolved \"https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9\"\n  integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==\n  dependencies:\n    buffer-crc32 \"~0.2.3\"\n    fd-slicer \"~1.1.0\"\n\nyocto-queue@^0.1.0:\n  version \"0.1.0\"\n  resolved \"https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b\"\n  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==\n\nyoutube-transcript-plus@^1.1.2:\n  version \"1.1.2\"\n  resolved \"https://registry.yarnpkg.com/youtube-transcript-plus/-/youtube-transcript-plus-1.1.2.tgz#f86851852a056088c11f4f6523ab0f8dba7d9711\"\n  integrity sha512-bLlqkA6gVVUorZpcc+THuECXyAwOpnHqW2lOav9g6gGovxAP3FCD8s9GBFVjmSl3cWWwwPPXtG/zY1nD+GvQ7A==\n\nyoutubei.js@^9.1.0:\n  version \"9.4.0\"\n  resolved \"https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-9.4.0.tgz#ccccaf4a295b96e3e17134a66730bbc82461594b\"\n  integrity sha512-8plCOZD2WabqWSEgZU3RjzigIIeR7sF028EERJENYrC9xO/6awpLMZfeoE1gNrNEbKcA+bzbMvonqlvBdxGdKg==\n  dependencies:\n    jintr \"^1.1.0\"\n    tslib \"^2.5.0\"\n    undici \"^5.19.1\"\n\nzlibjs@^0.3.1:\n  version \"0.3.1\"\n  resolved \"https://registry.yarnpkg.com/zlibjs/-/zlibjs-0.3.1.tgz#50197edb28a1c42ca659cc8b4e6a9ddd6d444554\"\n  integrity sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==\n\nzod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.5:\n  version \"3.25.0\"\n  resolved \"https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz#df504c957c4fb0feff467c74d03e6aab0b013e1c\"\n  integrity sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==\n\nzod@^3.22.3, zod@^3.22.4:\n  version \"3.25.76\"\n  resolved \"https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34\"\n  integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==\n"
  },
  {
    "path": "docker/.env.example",
    "content": "SERVER_PORT=3001\nSTORAGE_DIR=\"/app/server/storage\"\nUID='1000'\nGID='1000'\n# SIG_KEY='passphrase' # Please generate random string at least 32 chars long.\n# SIG_SALT='salt' # Please generate random string at least 32 chars long.\n# JWT_SECRET=\"my-random-string-for-seeding\" # Only needed if AUTH_TOKEN is set. Please generate random string at least 12 chars long.\n# JWT_EXPIRY=\"30d\" # (optional) https://docs.anythingllm.com/configuration#custom-ttl-for-sessions\n\n###########################################\n######## LLM API SElECTION ################\n###########################################\n# LLM_PROVIDER='openai'\n# OPEN_AI_KEY=\n# OPEN_MODEL_PREF='gpt-4o'\n\n# LLM_PROVIDER='gemini'\n# GEMINI_API_KEY=\n# GEMINI_LLM_MODEL_PREF='gemini-2.0-flash-lite'\n\n# LLM_PROVIDER='azure'\n# AZURE_OPENAI_ENDPOINT=\n# AZURE_OPENAI_KEY=\n# AZURE_OPENAI_MODEL_PREF='my-gpt35-deployment' # This is the \"deployment\" on Azure you want to use. Not the base model.\n# EMBEDDING_MODEL_PREF='embedder-model' # This is the \"deployment\" on Azure you want to use for embeddings. Not the base model. Valid base model is text-embedding-ada-002\n\n# LLM_PROVIDER='anthropic'\n# ANTHROPIC_API_KEY=sk-ant-xxxx\n# ANTHROPIC_MODEL_PREF='claude-2'\n# ANTHROPIC_CACHE_CONTROL=\"5m\" # Enable prompt caching (5m=5min cache, 1h=1hour cache). Reduces costs and improves speed by caching system prompts.\n\n# LLM_PROVIDER='lmstudio'\n# LMSTUDIO_BASE_PATH='http://your-server:1234/v1'\n# LMSTUDIO_MODEL_PREF='Loaded from Chat UI' # this is a bug in LMStudio 0.2.17\n# LMSTUDIO_MODEL_TOKEN_LIMIT=4096\n# LMSTUDIO_AUTH_TOKEN='your-lmstudio-auth-token-here'\n\n# LLM_PROVIDER='localai'\n# LOCAL_AI_BASE_PATH='http://host.docker.internal:8080/v1'\n# LOCAL_AI_MODEL_PREF='luna-ai-llama2'\n# LOCAL_AI_MODEL_TOKEN_LIMIT=4096\n# LOCAL_AI_API_KEY=\"sk-123abc\"\n\n# LLM_PROVIDER='ollama'\n# OLLAMA_BASE_PATH='http://host.docker.internal:11434'\n# OLLAMA_MODEL_PREF='llama2'\n# OLLAMA_MODEL_TOKEN_LIMIT=4096\n# OLLAMA_AUTH_TOKEN='your-ollama-auth-token-here (optional, only for ollama running behind auth - Bearer token)'\n# OLLAMA_RESPONSE_TIMEOUT=7200000 (optional, max timeout in milliseconds for ollama response to conclude. Default is 5min before aborting)\n\n# LLM_PROVIDER='togetherai'\n# TOGETHER_AI_API_KEY='my-together-ai-key'\n# TOGETHER_AI_MODEL_PREF='mistralai/Mixtral-8x7B-Instruct-v0.1'\n\n# LLM_PROVIDER='mistral'\n# MISTRAL_API_KEY='example-mistral-ai-api-key'\n# MISTRAL_MODEL_PREF='mistral-tiny'\n\n# LLM_PROVIDER='perplexity'\n# PERPLEXITY_API_KEY='my-perplexity-key'\n# PERPLEXITY_MODEL_PREF='codellama-34b-instruct'\n\n# LLM_PROVIDER='openrouter'\n# OPENROUTER_API_KEY='my-openrouter-key'\n# OPENROUTER_MODEL_PREF='openrouter/auto'\n\n# LLM_PROVIDER='huggingface'\n# HUGGING_FACE_LLM_ENDPOINT=https://uuid-here.us-east-1.aws.endpoints.huggingface.cloud\n# HUGGING_FACE_LLM_API_KEY=hf_xxxxxx\n# HUGGING_FACE_LLM_TOKEN_LIMIT=8000\n\n# LLM_PROVIDER='groq'\n# GROQ_API_KEY=gsk_abcxyz\n# GROQ_MODEL_PREF=llama3-8b-8192\n\n# LLM_PROVIDER='koboldcpp'\n# KOBOLD_CPP_BASE_PATH='http://127.0.0.1:5000/v1'\n# KOBOLD_CPP_MODEL_PREF='koboldcpp/codellama-7b-instruct.Q4_K_S'\n# KOBOLD_CPP_MODEL_TOKEN_LIMIT=4096\n\n# LLM_PROVIDER='textgenwebui'\n# TEXT_GEN_WEB_UI_BASE_PATH='http://127.0.0.1:5000/v1'\n# TEXT_GEN_WEB_UI_TOKEN_LIMIT=4096\n# TEXT_GEN_WEB_UI_API_KEY='sk-123abc'\n\n# LLM_PROVIDER='generic-openai'\n# GENERIC_OPEN_AI_BASE_PATH='http://proxy.url.openai.com/v1'\n# GENERIC_OPEN_AI_MODEL_PREF='gpt-3.5-turbo'\n# GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT=4096\n# GENERIC_OPEN_AI_API_KEY=sk-123abc\n# GENERIC_OPEN_AI_CUSTOM_HEADERS=\"X-Custom-Auth:my-secret-key,X-Custom-Header:my-value\" (useful if using a proxy that requires authentication or other headers)\n\n# LLM_PROVIDER='litellm'\n# LITE_LLM_MODEL_PREF='gpt-3.5-turbo'\n# LITE_LLM_MODEL_TOKEN_LIMIT=4096\n# LITE_LLM_BASE_PATH='http://127.0.0.1:4000'\n# LITE_LLM_API_KEY='sk-123abc'\n\n# LLM_PROVIDER='novita'\n# NOVITA_LLM_API_KEY='your-novita-api-key-here' check on https://novita.ai/settings/key-management\n# NOVITA_LLM_MODEL_PREF='deepseek/deepseek-r1'\n\n# LLM_PROVIDER='cometapi'\n# COMETAPI_LLM_API_KEY='your-cometapi-api-key-here' # Get one at https://api.cometapi.com/console/token\n# COMETAPI_LLM_MODEL_PREF='gpt-5-mini'\n# COMETAPI_LLM_TIMEOUT_MS=500 # Optional; stream idle timeout in ms (min 500ms)\n\n# LLM_PROVIDER='cohere'\n# COHERE_API_KEY=\n# COHERE_MODEL_PREF='command-r'\n\n# LLM_PROVIDER='bedrock'\n# AWS_BEDROCK_LLM_ACCESS_KEY_ID=\n# AWS_BEDROCK_LLM_ACCESS_KEY=\n# AWS_BEDROCK_LLM_REGION=us-west-2\n# AWS_BEDROCK_LLM_MODEL_PREFERENCE=meta.llama3-1-8b-instruct-v1:0\n# AWS_BEDROCK_LLM_MODEL_TOKEN_LIMIT=8191\n# AWS_BEDROCK_LLM_CONNECTION_METHOD=iam\n# AWS_BEDROCK_LLM_MAX_OUTPUT_TOKENS=4096\n# AWS_BEDROCK_LLM_SESSION_TOKEN= # Only required if CONNECTION_METHOD is 'sessionToken'\n# or even use Short and Long Term API keys\n# AWS_BEDROCK_LLM_CONNECTION_METHOD=\"apiKey\"\n# AWS_BEDROCK_LLM_API_KEY=\n\n# LLM_PROVIDER='fireworksai'\n# FIREWORKS_AI_LLM_API_KEY='my-fireworks-ai-key'\n# FIREWORKS_AI_LLM_MODEL_PREF='accounts/fireworks/models/llama-v3p1-8b-instruct'\n\n# LLM_PROVIDER='apipie'\n# APIPIE_LLM_API_KEY='sk-123abc'\n# APIPIE_LLM_MODEL_PREF='openrouter/llama-3.1-8b-instruct'\n\n# LLM_PROVIDER='xai'\n# XAI_LLM_API_KEY='xai-your-api-key-here'\n# XAI_LLM_MODEL_PREF='grok-beta'\n\n# LLM_PROVIDER='zai'\n# ZAI_API_KEY=\"your-zai-api-key-here\"\n# ZAI_MODEL_PREF=\"glm-4.5\"\n\n# LLM_PROVIDER='nvidia-nim'\n# NVIDIA_NIM_LLM_BASE_PATH='http://127.0.0.1:8000'\n# NVIDIA_NIM_LLM_MODEL_PREF='meta/llama-3.2-3b-instruct'\n\n# LLM_PROVIDER='deepseek'\n# DEEPSEEK_API_KEY='your-deepseek-api-key-here'\n# DEEPSEEK_MODEL_PREF='deepseek-chat'\n\n# LLM_PROVIDER='ppio'\n# PPIO_API_KEY='your-ppio-api-key-here'\n# PPIO_MODEL_PREF=deepseek/deepseek-v3/community\n\n# LLM_PROVIDER='moonshotai'\n# MOONSHOT_AI_API_KEY='your-moonshot-api-key-here'\n# MOONSHOT_AI_MODEL_PREF='moonshot-v1-32k'\n\n# LLM_PROVIDER='foundry'\n# FOUNDRY_BASE_PATH='http://127.0.0.1:55776'\n# FOUNDRY_MODEL_PREF='phi-3.5-mini'\n# FOUNDRY_MODEL_TOKEN_LIMIT=4096\n\n# LLM_PROVIDER='giteeai'\n# GITEE_AI_API_KEY=\n# GITEE_AI_MODEL_PREF=\n# GITEE_AI_MODEL_TOKEN_LIMIT=\n\n# LLM_PROVIDER='docker-model-runner'\n# DOCKER_MODEL_RUNNER_BASE_PATH='http://127.0.0.1:12434'\n# DOCKER_MODEL_RUNNER_LLM_MODEL_PREF='phi-3.5-mini'\n# DOCKER_MODEL_RUNNER_LLM_MODEL_TOKEN_LIMIT=4096\n\n# LLM_PROVIDER='privatemode'\n# PRIVATEMODE_LLM_BASE_PATH='http://127.0.0.1:8080'\n# PRIVATEMODE_LLM_MODEL_PREF='gemma-3-27b'\n\n# LLM_PROVIDER='sambanova'\n# SAMBANOVA_LLM_API_KEY='xxx-xxx-xxx'\n# SAMBANOVA_LLM_MODEL_PREF='gpt-oss-120b'\n\n###########################################\n######## Embedding API SElECTION ##########\n###########################################\n# This will be the assumed default embedding seleciton and model\n# EMBEDDING_ENGINE='native'\n# EMBEDDING_MODEL_PREF='Xenova/all-MiniLM-L6-v2'\n\n# Only used if you are using an LLM that does not natively support embedding (openai or Azure)\n# EMBEDDING_ENGINE='openai'\n# OPEN_AI_KEY=sk-xxxx\n# EMBEDDING_MODEL_PREF='text-embedding-ada-002'\n\n# EMBEDDING_ENGINE='azure'\n# AZURE_OPENAI_ENDPOINT=\n# AZURE_OPENAI_KEY=\n# EMBEDDING_MODEL_PREF='my-embedder-model' # This is the \"deployment\" on Azure you want to use for embeddings. Not the base model. Valid base model is text-embedding-ada-002\n\n# EMBEDDING_ENGINE='localai'\n# EMBEDDING_BASE_PATH='http://localhost:8080/v1'\n# EMBEDDING_MODEL_PREF='text-embedding-ada-002'\n# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=1000 # The max chunk size in chars a string to embed can be\n\n# EMBEDDING_ENGINE='ollama'\n# EMBEDDING_BASE_PATH='http://host.docker.internal:11434'\n# EMBEDDING_MODEL_PREF='nomic-embed-text:latest'\n# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192\n\n# EMBEDDING_ENGINE='lmstudio'\n# EMBEDDING_BASE_PATH='https://host.docker.internal:1234/v1'\n# EMBEDDING_MODEL_PREF='nomic-ai/nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.Q4_0.gguf'\n# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192\n\n# EMBEDDING_ENGINE='cohere'\n# COHERE_API_KEY=\n# EMBEDDING_MODEL_PREF='embed-english-v3.0'\n\n# EMBEDDING_ENGINE='voyageai'\n# VOYAGEAI_API_KEY=\n# EMBEDDING_MODEL_PREF='voyage-large-2-instruct'\n\n# EMBEDDING_ENGINE='litellm'\n# EMBEDDING_MODEL_PREF='text-embedding-ada-002'\n# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192\n# LITE_LLM_BASE_PATH='http://127.0.0.1:4000'\n# LITE_LLM_API_KEY='sk-123abc'\n\n# EMBEDDING_ENGINE='generic-openai'\n# EMBEDDING_MODEL_PREF='text-embedding-ada-002'\n# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192\n# EMBEDDING_BASE_PATH='http://127.0.0.1:4000'\n# GENERIC_OPEN_AI_EMBEDDING_API_KEY='sk-123abc'\n# GENERIC_OPEN_AI_EMBEDDING_MAX_CONCURRENT_CHUNKS=500\n# GENERIC_OPEN_AI_EMBEDDING_API_DELAY_MS=1000\n\n# EMBEDDING_ENGINE='gemini'\n# GEMINI_EMBEDDING_API_KEY=\n# EMBEDDING_MODEL_PREF='text-embedding-004'\n\n# EMBEDDING_ENGINE='openrouter'\n# EMBEDDING_MODEL_PREF='baai/bge-m3'\n# OPENROUTER_API_KEY=''\n\n# EMBEDDING_ENGINE='lemonade'\n# EMBEDDING_BASE_PATH='http://127.0.0.1:8000'\n# EMBEDDING_MODEL_PREF='Qwen3-embedder'\n# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192\n\n###########################################\n######## Vector Database Selection ########\n###########################################\n# Enable all below if you are using vector database: LanceDB.\n# VECTOR_DB=\"lancedb\"\n\n# Enable all below if you are using vector database: Weaviate.\n# VECTOR_DB=\"pgvector\"\n# PGVECTOR_CONNECTION_STRING=\"postgresql://dbuser:dbuserpass@localhost:5432/yourdb\"\n# PGVECTOR_TABLE_NAME=\"anythingllm_vectors\" # optional, but can be defined\n\n# Enable all below if you are using vector database: Chroma.\n# VECTOR_DB=\"chroma\"\n# CHROMA_ENDPOINT='http://host.docker.internal:8000'\n# CHROMA_API_HEADER=\"X-Api-Key\"\n# CHROMA_API_KEY=\"sk-123abc\"\n\n# Enable all below if you are using vector database: Chroma Cloud.\n# VECTOR_DB=\"chromacloud\"\n# CHROMACLOUD_API_KEY=\"ck-your-api-key\"\n# CHROMACLOUD_TENANT=\n# CHROMACLOUD_DATABASE=\n\n# Enable all below if you are using vector database: Pinecone.\n# VECTOR_DB=\"pinecone\"\n# PINECONE_API_KEY=\n# PINECONE_INDEX=\n\n# Enable all below if you are using vector database: Weaviate.\n# VECTOR_DB=\"weaviate\"\n# WEAVIATE_ENDPOINT=\"http://localhost:8080\"\n# WEAVIATE_API_KEY=\n\n# Enable all below if you are using vector database: Qdrant.\n# VECTOR_DB=\"qdrant\"\n# QDRANT_ENDPOINT=\"http://localhost:6333\"\n# QDRANT_API_KEY=\n\n# Enable all below if you are using vector database: Milvus.\n# VECTOR_DB=\"milvus\"\n# MILVUS_ADDRESS=\"http://localhost:19530\"\n# MILVUS_USERNAME=\n# MILVUS_PASSWORD=\n\n# Enable all below if you are using vector database: Zilliz Cloud.\n# VECTOR_DB=\"zilliz\"\n# ZILLIZ_ENDPOINT=\"https://sample.api.gcp-us-west1.zillizcloud.com\"\n# ZILLIZ_API_TOKEN=api-token-here\n\n# Enable all below if you are using vector database: Astra DB.\n# VECTOR_DB=\"astra\"\n# ASTRA_DB_APPLICATION_TOKEN=\n# ASTRA_DB_ENDPOINT=\n\n###########################################\n######## Audio Model Selection ############\n###########################################\n# (default) use built-in whisper-small model.\n# WHISPER_PROVIDER=\"local\"\n\n# use openai hosted whisper model.\n# WHISPER_PROVIDER=\"openai\"\n# OPEN_AI_KEY=sk-xxxxxxxx\n\n###########################################\n######## TTS/STT Model Selection ##########\n###########################################\n# TTS_PROVIDER=\"native\"\n\n# TTS_PROVIDER=\"openai\"\n# TTS_OPEN_AI_KEY=sk-example\n# TTS_OPEN_AI_VOICE_MODEL=nova\n\n# TTS_PROVIDER=\"generic-openai\"\n# TTS_OPEN_AI_COMPATIBLE_KEY=sk-example\n# TTS_OPEN_AI_COMPATIBLE_MODEL=tts-1\n# TTS_OPEN_AI_COMPATIBLE_VOICE_MODEL=nova\n# TTS_OPEN_AI_COMPATIBLE_ENDPOINT=\"https://api.openai.com/v1\"\n\n# TTS_PROVIDER=\"elevenlabs\"\n# TTS_ELEVEN_LABS_KEY=\n# TTS_ELEVEN_LABS_VOICE_MODEL=21m00Tcm4TlvDq8ikWAM # Rachel\n\n# CLOUD DEPLOYMENT VARIRABLES ONLY\n# AUTH_TOKEN=\"hunter2\" # This is the password to your application if remote hosting.\n# DISABLE_TELEMETRY=\"false\"\n\n###########################################\n######## PASSWORD COMPLEXITY ##############\n###########################################\n# Enforce a password schema for your organization users.\n# Documentation on how to use https://github.com/kamronbatman/joi-password-complexity\n# Default is only 8 char minimum\n# PASSWORDMINCHAR=8\n# PASSWORDMAXCHAR=250\n# PASSWORDLOWERCASE=1\n# PASSWORDUPPERCASE=1\n# PASSWORDNUMERIC=1\n# PASSWORDSYMBOL=1\n# PASSWORDREQUIREMENTS=4\n\n###########################################\n######## ENABLE HTTPS SERVER ##############\n###########################################\n# By enabling this and providing the path/filename for the key and cert,\n# the server will use HTTPS instead of HTTP.\n#ENABLE_HTTPS=\"true\"\n#HTTPS_CERT_PATH=\"sslcert/cert.pem\"\n#HTTPS_KEY_PATH=\"sslcert/key.pem\"\n\n###########################################\n######## AGENT SERVICE KEYS ###############\n###########################################\n\n#------ SEARCH ENGINES -------\n#=============================\n#------ Google Search -------- https://programmablesearchengine.google.com/controlpanel/create\n# AGENT_GSE_KEY=\n# AGENT_GSE_CTX=\n\n#------ SearchApi.io ----------- https://www.searchapi.io/\n# AGENT_SEARCHAPI_API_KEY=\n# AGENT_SEARCHAPI_ENGINE=google\n\n#------ SerpApi ----------- https://serpapi.com/\n# AGENT_SERPAPI_API_KEY=\n# AGENT_SERPAPI_ENGINE=google\n\n#------ Serper.dev ----------- https://serper.dev/\n# AGENT_SERPER_DEV_KEY=\n\n#------ Bing Search ----------- https://portal.azure.com/\n# AGENT_BING_SEARCH_API_KEY=\n\n#------ Serply.io ----------- https://serply.io/\n# AGENT_SERPLY_API_KEY=\n\n#------ SearXNG ----------- https://github.com/searxng/searxng\n# AGENT_SEARXNG_API_URL=\n\n#------ Tavily ----------- https://www.tavily.com/\n# AGENT_TAVILY_API_KEY=\n\n#------ Exa Search ----------- https://www.exa.ai/\n# AGENT_EXA_API_KEY=\n\n#------ Perplexity Search ----------- [https://console.perplexity.ai](https://console.perplexity.ai)\n# AGENT_PERPLEXITY_API_KEY=\n\n###########################################\n######## Other Configurations ############\n###########################################\n\n# Disable viewing chat history from the UI and frontend APIs.\n# See https://docs.anythingllm.com/configuration#disable-view-chat-history for more information.\n# DISABLE_VIEW_CHAT_HISTORY=1\n\n# Enable simple SSO passthrough to pre-authenticate users from a third party service.\n# See https://docs.anythingllm.com/configuration#simple-sso-passthrough for more information.\n# SIMPLE_SSO_ENABLED=1\n# SIMPLE_SSO_NO_LOGIN=1\n# SIMPLE_SSO_NO_LOGIN_REDIRECT=https://your-custom-login-url.com (optional)\n\n# Allow scraping of any IP address in collector - must be string \"true\" to be enabled\n# See https://docs.anythingllm.com/configuration#local-ip-address-scraping for more information.\n# COLLECTOR_ALLOW_ANY_IP=\"true\"\n\n# Specify the target languages for when using OCR to parse images and PDFs.\n# This is a comma separated list of language codes as a string. Unsupported languages will be ignored.\n# Default is English. See https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html for a list of valid language codes.\n# TARGET_OCR_LANG=eng,deu,ita,spa,fra,por,rus,nld,tur,hun,pol,ita,spa,fra,por,rus,nld,tur,hun,pol\n\n# Runtime flags for built-in pupeeteer Chromium instance\n# This is only required on Linux machines running AnythingLLM via Docker\n# and do not want to use the --cap-add=SYS_ADMIN docker argument\n# ANYTHINGLLM_CHROMIUM_ARGS=\"--no-sandbox,--disable-setuid-sandbox\"\n\n# Disable Swagger API documentation endpoint.\n# Set to \"true\" to disable the /api/docs endpoint (recommended for production deployments).\n# DISABLE_SWAGGER_DOCS=\"true\"\n\n# Disable MCP cooldown timer for agent calls\n# this can lead to infinite recursive calls of the same function\n# for some model/provider combinations\n# MCP_NO_COOLDOWN=\"true\n\n# Allow native tool calling for specific providers.\n# This can VASTLY improve performance and speed of agent calls.\n# Check code for supported providers who can be enabled here via this flag\n# PROVIDER_SUPPORTS_NATIVE_TOOL_CALLING=\"generic-openai,bedrock,localai,groq,litellm,openrouter\"\n\n# (optional) Maximum number of tools an agent can chain for a single response.\n# This prevents some lower-end models from infinite recursive tool calls.\n# AGENT_MAX_TOOL_CALLS=10\n\n# Enable agent tool reranking to reduce token usage by selecting only the most relevant tools\n# for each query. Uses the native embedding reranker to score tools against the user's prompt.\n# Set to \"true\" to enable. This can reduce token costs by 80% when you have\n# many tools/MCP servers enabled.\n# AGENT_SKILL_RERANKER_ENABLED=\"true\"\n# AGENT_SKILL_RERANKER_TOP_N=15 # (optional) Number of top tools to keep after reranking (default: 15)"
  },
  {
    "path": "docker/Dockerfile",
    "content": "# Setup base image\nFROM ubuntu:noble-20251013 AS base\n\n# Build arguments\nARG ARG_UID=1000\nARG ARG_GID=1000\n\nFROM base AS build-arm64\nRUN echo \"Preparing build of AnythingLLM image for arm64 architecture\"\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# Install system dependencies\n# hadolint ignore=DL3008,DL3013\nRUN DEBIAN_FRONTEND=noninteractive apt-get update && \\\n    DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \\\n        unzip curl gnupg libgfortran5 libgbm1 tzdata netcat-openbsd \\\n        libasound2t64 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 \\\n        libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libx11-6 libx11-xcb1 libxcb1 \\\n        libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 \\\n        libxss1 libxtst6 ca-certificates fonts-liberation libappindicator3-1 libnss3 lsb-release \\\n        xdg-utils git build-essential ffmpeg && \\\n    mkdir -p /etc/apt/keyrings && \\\n    curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \\\n    echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main\" | tee /etc/apt/sources.list.d/nodesource.list && \\\n    apt-get update && \\\n    # Install node and yarn\n    apt-get install -yq --no-install-recommends nodejs && \\\n    curl -LO https://github.com/yarnpkg/yarn/releases/download/v1.22.19/yarn_1.22.19_all.deb \\\n        && dpkg -i yarn_1.22.19_all.deb \\\n        && rm yarn_1.22.19_all.deb && \\\n    # Install uvx (pinned to 0.6.10) for MCP support\n    curl -LsSf https://astral.sh/uv/0.6.10/install.sh | sh && \\\n        mv /root/.local/bin/uv /usr/local/bin/uv && \\\n        mv /root/.local/bin/uvx /usr/local/bin/uvx && \\\n        echo \"Installed uvx! $(uv --version)\" && \\\n    apt-get clean && \\\n    rm -rf /var/lib/apt/lists/*\n\n# Create a group and user with specific UID and GID\n# First, remove any existing user/group with the target UID/GID to avoid conflicts\nRUN (getent passwd \"$ARG_UID\" && userdel -f \"$(getent passwd \"$ARG_UID\" | cut -d: -f1)\") || true && \\\n    (getent group \"$ARG_GID\" && groupdel \"$(getent group \"$ARG_GID\" | cut -d: -f1)\") || true && \\\n    groupadd -g \"$ARG_GID\" anythingllm && \\\n    useradd -l -u \"$ARG_UID\" -m -d /app -s /bin/bash -g anythingllm anythingllm && \\\n    mkdir -p /app/frontend/ /app/server/ /app/collector/ && chown -R anythingllm:anythingllm /app\n\n# Copy docker helper scripts\nCOPY ./docker/docker-entrypoint.sh /usr/local/bin/\nCOPY ./docker/docker-healthcheck.sh /usr/local/bin/\nCOPY --chown=anythingllm:anythingllm ./docker/.env.example /app/server/.env\n\n# Ensure the scripts are executable\nRUN chmod +x /usr/local/bin/docker-entrypoint.sh && \\\n    chmod +x /usr/local/bin/docker-healthcheck.sh\n\nUSER anythingllm\nWORKDIR /app\n\n# Puppeteer does not ship with an ARM86 compatible build for Chromium\n# so web-scraping would be broken in arm docker containers unless we patch it\n# by manually installing a compatible chromedriver.\nRUN echo \"Need to patch Puppeteer x Chromium support for ARM86 - installing dep!\" && \\\n    curl -fSL https://webassets.anythingllm.com/chromium-1088-linux-arm64.zip -o chrome-linux.zip && \\\n    unzip chrome-linux.zip && \\\n    rm -rf chrome-linux.zip\n\nENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true\nENV CHROME_PATH=/app/chrome-linux/chrome\nENV PUPPETEER_EXECUTABLE_PATH=/app/chrome-linux/chrome\n\nRUN echo \"Done running arm64 specific installation steps\"\n\n#############################################\n\n# amd64-specific stage\nFROM base AS build-amd64\nRUN echo \"Preparing build of AnythingLLM image for non-ARM architecture\"\n\nSHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n\n# Install system dependencies\n# hadolint ignore=DL3008,DL3013\nRUN DEBIAN_FRONTEND=noninteractive apt-get update && \\\n    DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \\\n        curl gnupg libgfortran5 libgbm1 tzdata netcat-openbsd \\\n        libasound2t64 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 \\\n        libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libx11-6 libx11-xcb1 libxcb1 \\\n        libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 \\\n        libxss1 libxtst6 ca-certificates fonts-liberation libappindicator3-1 libnss3 lsb-release \\\n        xdg-utils git build-essential ffmpeg && \\\n    mkdir -p /etc/apt/keyrings && \\\n    curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \\\n    echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main\" | tee /etc/apt/sources.list.d/nodesource.list && \\\n    apt-get update && \\\n    # Install node and yarn\n    apt-get install -yq --no-install-recommends nodejs && \\\n    curl -LO https://github.com/yarnpkg/yarn/releases/download/v1.22.19/yarn_1.22.19_all.deb \\\n        && dpkg -i yarn_1.22.19_all.deb \\\n        && rm yarn_1.22.19_all.deb && \\\n    # Install uvx (pinned to 0.6.10) for MCP support\n    curl -LsSf https://astral.sh/uv/0.6.10/install.sh | sh && \\\n        mv /root/.local/bin/uv /usr/local/bin/uv && \\\n        mv /root/.local/bin/uvx /usr/local/bin/uvx && \\\n        echo \"Installed uvx! $(uv --version)\" && \\\n    apt-get clean && \\\n    rm -rf /var/lib/apt/lists/*\n\n# Create a group and user with specific UID and GID\n# First, remove any existing user/group with the target UID/GID to avoid conflicts\nRUN (getent passwd \"$ARG_UID\" && userdel -f \"$(getent passwd \"$ARG_UID\" | cut -d: -f1)\") || true && \\\n    (getent group \"$ARG_GID\" && groupdel \"$(getent group \"$ARG_GID\" | cut -d: -f1)\") || true && \\\n    groupadd -g \"$ARG_GID\" anythingllm && \\\n    useradd -l -u \"$ARG_UID\" -m -d /app -s /bin/bash -g anythingllm anythingllm && \\\n    mkdir -p /app/frontend/ /app/server/ /app/collector/ && chown -R anythingllm:anythingllm /app\n\n# Copy docker helper scripts\nCOPY ./docker/docker-entrypoint.sh /usr/local/bin/\nCOPY ./docker/docker-healthcheck.sh /usr/local/bin/\nCOPY --chown=anythingllm:anythingllm ./docker/.env.example /app/server/.env\n\n# Ensure the scripts are executable\nRUN chmod +x /usr/local/bin/docker-entrypoint.sh && \\\n    chmod +x /usr/local/bin/docker-healthcheck.sh\n\n#############################################\n# COMMON BUILD FLOW FOR ALL ARCHS\n#############################################\n\n# hadolint ignore=DL3006\nFROM build-${TARGETARCH} AS build\nRUN echo \"Running common build flow of AnythingLLM image for all architectures\"\n\nUSER anythingllm\nWORKDIR /app\n\n# Install & Build frontend layer\n# Use BUILDPLATFORM to run on the native host architecture (not emulated).\n# This avoids esbuild crashing under QEMU when cross-compiling.\n# The output (static HTML/CSS/JS) is platform-independent.\nFROM --platform=$BUILDPLATFORM node:18-slim AS frontend-build\nWORKDIR /app/frontend\nCOPY ./frontend/package.json ./frontend/yarn.lock ./\nRUN yarn install --network-timeout 100000 && yarn cache clean\nCOPY ./frontend/ ./\nRUN yarn build\nWORKDIR /app\n\n# Install server layer\n# Also pull and build collector deps (chromium issues prevent bad bindings)\nFROM build AS backend-build\nCOPY --chown=anythingllm:anythingllm ./server /app/server/\nWORKDIR /app/server\nRUN yarn install --production --network-timeout 100000 && yarn cache clean\nWORKDIR /app\n\n# Install collector dependencies\nCOPY --chown=anythingllm:anythingllm ./collector/ ./collector/\nWORKDIR /app/collector\nENV PUPPETEER_DOWNLOAD_BASE_URL=https://storage.googleapis.com/chrome-for-testing-public\nRUN yarn install --production --network-timeout 100000 && yarn cache clean\n\nWORKDIR /app\nUSER anythingllm\n\n# Since we are building from backend-build we just need to move built frontend into server/public\nFROM backend-build AS production-build\nWORKDIR /app\nCOPY --chown=anythingllm:anythingllm --from=frontend-build /app/frontend/dist /app/server/public\n\n# Setup the environment\nENV NODE_ENV=production\nENV ANYTHING_LLM_RUNTIME=docker\nENV DEPLOYMENT_VERSION=1.11.2\n\n# Setup the healthcheck\nHEALTHCHECK --interval=1m --timeout=10s --start-period=1m \\\n  CMD /bin/bash /usr/local/bin/docker-healthcheck.sh || exit 1\n\n# Run the server\n# CMD [\"sh\", \"-c\", \"tail -f /dev/null\"] # For development: keep container open\nENTRYPOINT [\"/bin/bash\", \"/usr/local/bin/docker-entrypoint.sh\"]\n"
  },
  {
    "path": "docker/HOW_TO_USE_DOCKER.md",
    "content": "# How to use Dockerized Anything LLM\n\nUse the Dockerized version of AnythingLLM for a much faster and complete startup of AnythingLLM.\n\n### Minimum Requirements\n\n> [!TIP]\n> Running AnythingLLM on AWS/GCP/Azure?\n> You should aim for at least 2GB of RAM. Disk storage is proportional to however much data\n> you will be storing (documents, vectors, models, etc). Minimum 10GB recommended.\n\n- `docker` installed on your machine\n- `yarn` and `node` on your machine\n- access to an LLM running locally or remotely\n\n\\*AnythingLLM by default uses a built-in vector database powered by [LanceDB](https://github.com/lancedb/lancedb)\n\n\\*AnythingLLM by default embeds text on instance privately [Learn More](../server/storage/models/README.md)\n\n## Recommend way to run dockerized AnythingLLM!\n\n> [!IMPORTANT]\n> If you are running another service on localhost like Chroma, LocalAi, or LMStudio\n> you will need to use http://host.docker.internal:xxxx to access the service from within\n> the docker container using AnythingLLM as `localhost:xxxx` will not resolve for the host system.\n>\n> **Requires** Docker v18.03+ on Win/Mac and 20.10+ on Linux/Ubuntu for host.docker.internal to resolve!\n>\n> _Linux_: add `--add-host=host.docker.internal:host-gateway` to docker run command for this to resolve.\n>\n> eg: Chroma host URL running on localhost:8000 on host machine needs to be http://host.docker.internal:8000\n> when used in AnythingLLM.\n\n> [!TIP]\n> It is best to mount the containers storage volume to a folder on your host machine\n> so that you can pull in future updates without deleting your existing data!\n\nPull in the latest image from docker. Supports both `amd64` and `arm64` CPU architectures.\n\n```shell\ndocker pull mintplexlabs/anythingllm\n```\n\n<table>\n<tr>\n<th colspan=\"2\">Mount the storage locally and run AnythingLLM in Docker</th>\n</tr>\n<tr>\n<td>\n  Linux/MacOs\n</td>\n<td>\n\n```shell\nexport STORAGE_LOCATION=$HOME/anythingllm && \\\nmkdir -p $STORAGE_LOCATION && \\\ntouch \"$STORAGE_LOCATION/.env\" && \\\ndocker run -d -p 3001:3001 \\\n--cap-add SYS_ADMIN \\\n-v ${STORAGE_LOCATION}:/app/server/storage \\\n-v ${STORAGE_LOCATION}/.env:/app/server/.env \\\n-e STORAGE_DIR=\"/app/server/storage\" \\\nmintplexlabs/anythingllm\n```\n\n</td>\n</tr>\n<tr>\n<td>\n  Windows\n</td>\n<td>\n\n```powershell\n# Run this in powershell terminal\n$env:STORAGE_LOCATION=\"$HOME\\Documents\\anythingllm\"; `\nIf(!(Test-Path $env:STORAGE_LOCATION)) {New-Item $env:STORAGE_LOCATION -ItemType Directory}; `\nIf(!(Test-Path \"$env:STORAGE_LOCATION\\.env\")) {New-Item \"$env:STORAGE_LOCATION\\.env\" -ItemType File}; `\ndocker run -d -p 3001:3001 `\n--cap-add SYS_ADMIN `\n-v \"$env:STORAGE_LOCATION`:/app/server/storage\" `\n-v \"$env:STORAGE_LOCATION\\.env:/app/server/.env\" `\n-e STORAGE_DIR=\"/app/server/storage\" `\nmintplexlabs/anythingllm;\n```\n\n</td>\n</tr>\n<tr>\n<td> Docker Compose</td>\n<td>\n\n\n```yaml\nversion: '3.8'\nservices:\n  anythingllm:\n    image: mintplexlabs/anythingllm\n    container_name: anythingllm\n    ports:\n    - \"3001:3001\"\n    cap_add:\n      - SYS_ADMIN\n    environment:\n    # Adjust for your environment\n      - STORAGE_DIR=/app/server/storage\n      - JWT_SECRET=\"make this a large list of random numbers and letters 20+\"\n      - LLM_PROVIDER=ollama\n      - OLLAMA_BASE_PATH=http://127.0.0.1:11434\n      - OLLAMA_MODEL_PREF=llama2\n      - OLLAMA_MODEL_TOKEN_LIMIT=4096\n      - EMBEDDING_ENGINE=ollama\n      - EMBEDDING_BASE_PATH=http://127.0.0.1:11434\n      - EMBEDDING_MODEL_PREF=nomic-embed-text:latest\n      - EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192\n      - VECTOR_DB=lancedb\n      - WHISPER_PROVIDER=local\n      - TTS_PROVIDER=native\n      - PASSWORDMINCHAR=8\n      # Add any other keys here for services or settings\n      # you can find in the docker/.env.example file\n    volumes:\n      - anythingllm_storage:/app/server/storage\n    restart: always\n\nvolumes:\n  anythingllm_storage:\n    driver: local\n    driver_opts:\n      type: none\n      o: bind\n      device: /path/on/local/disk\n```\n\n  </td>\n</tr>\n</table>\n\nGo to `http://localhost:3001` and you are now using AnythingLLM! All your data and progress will persist between\ncontainer rebuilds or pulls from Docker Hub.\n\n## How to use the user interface\n\n- To access the full application, visit `http://localhost:3001` in your browser.\n\n## About UID and GID in the ENV\n\n- The UID and GID are set to 1000 by default. This is the default user in the Docker container and on most host operating systems. If there is a mismatch between your host user UID and GID and what is set in the `.env` file, you may experience permission issues.\n\n## Build locally from source _not recommended for casual use_\n\n- `git clone` this repo and `cd anything-llm` to get to the root directory.\n- `touch server/storage/anythingllm.db` to create empty SQLite DB file.\n- `cd docker/`\n- `cp .env.example .env` **you must do this before building**\n- `docker-compose up -d --build` to build the image - this will take a few moments.\n\nYour docker host will show the image as online once the build process is completed. This will build the app to `http://localhost:3001`.\n\n## Integrations and one-click setups\n\nThe integrations below are templates or tooling built by the community to make running the docker experience of AnythingLLM easier.\n\n### Use the Midori AI Subsystem to Manage AnythingLLM\n\nFollow the setup found on [Midori AI Subsystem Site](https://io.midori-ai.xyz/subsystem/manager/) for your host OS\nAfter setting that up install the AnythingLLM docker backend to the Midori AI Subsystem.\n\nOnce that is done, you are all set!\n\n## Common questions and fixes\n\n### Cannot connect to service running on localhost!\n\nIf you are in docker and cannot connect to a service running on your host machine running on a local interface or loopback:\n\n- `localhost`\n- `127.0.0.1`\n- `0.0.0.0`\n\n> [!IMPORTANT]\n> On linux `http://host.docker.internal:xxxx` does not work.\n> Use `http://172.17.0.1:xxxx` instead to emulate this functionality.\n\nThen in docker you need to replace that localhost part with `host.docker.internal`. For example, if running Ollama on the host machine, bound to http://127.0.0.1:11434 you should put `http://host.docker.internal:11434` into the connection URL in AnythingLLM.\n\n\n### API is not working, cannot login, LLM is \"offline\"?\n\nYou are likely running the docker container on a remote machine like EC2 or some other instance where the reachable URL\nis not `http://localhost:3001` and instead is something like `http://193.xx.xx.xx:3001` - in this case all you need to do is add the following to your `frontend/.env.production` before running `docker-compose up -d --build`\n\n```\n# frontend/.env.production\nGENERATE_SOURCEMAP=false\nVITE_API_BASE=\"http://<YOUR_REACHABLE_IP_ADDRESS>:3001/api\"\n```\n\nFor example, if the docker instance is available on `192.186.1.222` your `VITE_API_BASE` would look like `VITE_API_BASE=\"http://192.186.1.222:3001/api\"` in `frontend/.env.production`.\n\n### Having issues with Ollama?\n\nIf you are getting errors like `llama:streaming - could not stream chat. Error: connect ECONNREFUSED 172.17.0.1:11434` then visit the README below.\n\n[Fix common issues with Ollama](../server/utils/AiProviders/ollama/README.md)\n\n### Still not working?\n\n[Ask for help on Discord](https://discord.gg/6UyHPeGZAC)\n"
  },
  {
    "path": "docker/docker-compose.yml",
    "content": "name: anythingllm\n\nnetworks:\n  anything-llm:\n    driver: bridge\n\nservices:\n  anything-llm:\n    container_name: anythingllm\n    build:\n      context: ../.\n      dockerfile: ./docker/Dockerfile\n      args:\n        ARG_UID: ${UID:-1000}\n        ARG_GID: ${GID:-1000}\n    cap_add:\n      - SYS_ADMIN\n    volumes:\n      - \"./.env:/app/server/.env\"\n      - \"../server/storage:/app/server/storage\"\n      - \"../collector/hotdir/:/app/collector/hotdir\"\n      - \"../collector/outputs/:/app/collector/outputs\"\n    user: \"${UID:-1000}:${GID:-1000}\"\n    ports:\n      - \"3001:3001\"\n    env_file:\n      - .env\n    networks:\n      - anything-llm\n    extra_hosts:\n      - \"host.docker.internal:host-gateway\"\n"
  },
  {
    "path": "docker/docker-entrypoint.sh",
    "content": "#!/bin/bash\n\n# Check if STORAGE_DIR is set\nif [ -z \"$STORAGE_DIR\" ]; then\n    echo \"================================================================\"\n    echo \"⚠️  ⚠️  ⚠️  WARNING: STORAGE_DIR environment variable is not set! ⚠️  ⚠️  ⚠️\"\n    echo \"\"\n    echo \"Not setting this will result in data loss on container restart since\"\n    echo \"the application will not have a persistent storage location.\"\n    echo \"It can also result in weird errors in various parts of the application.\"\n    echo \"\"\n    echo \"Please run the container with the official docker command at\"\n    echo \"https://docs.anythingllm.com/installation-docker/quickstart\"\n    echo \"\"\n    echo \"⚠️  ⚠️  ⚠️  WARNING: STORAGE_DIR environment variable is not set! ⚠️  ⚠️  ⚠️\"\n    echo \"================================================================\"\nfi\n\n{\n  cd /app/server/ &&\n    # Disable Prisma CLI telemetry (https://www.prisma.io/docs/orm/tools/prisma-cli#how-to-opt-out-of-data-collection)\n    export CHECKPOINT_DISABLE=1 &&\n    npx prisma generate --schema=./prisma/schema.prisma &&\n    npx prisma migrate deploy --schema=./prisma/schema.prisma &&\n    node /app/server/index.js\n} &\n{ node /app/collector/index.js; } &\nwait -n\nexit $?"
  },
  {
    "path": "docker/docker-healthcheck.sh",
    "content": "#!/bin/bash\n\n# Send a request to the specified URL\nresponse=$(curl --write-out '%{http_code}' --silent --output /dev/null http://localhost:3001/api/ping)\n\n# If the HTTP response code is 200 (OK), the server is up\nif [ \"$response\" -eq 200 ]; then\n  echo \"Server is up\"\n  exit 0\nelse\n  echo \"Server is down\"\n  exit 1\nfi\n"
  },
  {
    "path": "docker/vex/CVE-2019-10790.vex.json",
    "content": "{\n  \"@context\": \"https://openvex.dev/ns/v0.2.0\",\n  \"@id\": \"https://openvex.dev/docs/public/vex-6750d79bb005487e11d10f81d0b3ac92c47e6e259292c6b2d02558f9f4bca52d\",\n  \"author\": \"tim@mintplexlabs.com\",\n  \"timestamp\": \"2024-07-22T13:49:12.883675-07:00\",\n  \"version\": 1,\n  \"statements\": [\n    {\n      \"vulnerability\": {\n        \"name\": \"CVE-2019-10790\"\n      },\n      \"timestamp\": \"2024-07-22T13:49:12.883678-07:00\",\n      \"products\": [\n        {\n          \"@id\": \"pkg:npm/taffydb@2.6.2\"\n        }\n      ],\n      \"status\": \"not_affected\",\n      \"justification\": \"vulnerable_code_not_in_execute_path\"\n    }\n  ]\n}"
  },
  {
    "path": "docker/vex/CVE-2024-29415.vex.json",
    "content": "{\n  \"@context\": \"https://openvex.dev/ns/v0.2.0\",\n  \"@id\": \"https://openvex.dev/docs/public/vex-939548c125c5bfebd3fd91e64c1c53bffacbde06b3611b4474ea90fa58045004\",\n  \"author\": \"tim@mintplexlabs.com\",\n  \"timestamp\": \"2024-07-19T16:08:47.147169-07:00\",\n  \"version\": 1,\n  \"statements\": [\n    {\n      \"vulnerability\": {\n        \"name\": \"CVE-2024-29415\"\n      },\n      \"timestamp\": \"2024-07-19T16:08:47.147172-07:00\",\n      \"products\": [\n        {\n          \"@id\": \"pkg:npm/ip@2.0.0\"\n        }\n      ],\n      \"status\": \"not_affected\",\n      \"justification\": \"vulnerable_code_not_present\"\n    }\n  ]\n}"
  },
  {
    "path": "docker/vex/CVE-2024-37890.vex.json",
    "content": "{\n  \"@context\": \"https://openvex.dev/ns/v0.2.0\",\n  \"@id\": \"https://openvex.dev/docs/public/vex-939548c125c5bfebd3fd91e64c1c53bffacbde06b3611b4474ea90fa58045004\",\n  \"author\": \"tim@mintplexlabs.com\",\n  \"timestamp\": \"2024-07-19T16:08:47.147169-07:00\",\n  \"version\": 1,\n  \"statements\": [\n    {\n      \"vulnerability\": {\n        \"name\": \"CVE-2024-37890\"\n      },\n      \"timestamp\": \"2024-07-19T16:08:47.147172-07:00\",\n      \"products\": [\n        {\n          \"@id\": \"pkg:npm/ws@8.14.2\"\n        }\n      ],\n      \"status\": \"not_affected\",\n      \"justification\": \"vulnerable_code_not_in_execute_path\"\n    }\n  ]\n}"
  },
  {
    "path": "docker/vex/CVE-2024-4068.vex.json",
    "content": "{\n  \"@context\": \"https://openvex.dev/ns/v0.2.0\",\n  \"@id\": \"https://openvex.dev/docs/public/vex-939548c125c5bfebd3fd91e64c1c53bffacbde06b3611b4474ea90fa58045004\",\n  \"author\": \"tim@mintplexlabs.com\",\n  \"timestamp\": \"2024-07-19T16:08:47.147169-07:00\",\n  \"version\": 1,\n  \"statements\": [\n    {\n      \"vulnerability\": {\n        \"name\": \"CVE-2024-4068\"\n      },\n      \"timestamp\": \"2024-07-19T16:08:47.147172-07:00\",\n      \"products\": [\n        {\n          \"@id\": \"pkg:npm/braces@3.0.2\"\n        }\n      ],\n      \"status\": \"not_affected\",\n      \"justification\": \"vulnerable_code_not_present\"\n    }\n  ]\n}"
  },
  {
    "path": "eslint.config.js",
    "content": "import globals from \"./server/node_modules/globals/index.js\"\nimport eslintRecommended from \"./server/node_modules/@eslint/js/src/index.js\"\nimport eslintConfigPrettier from \"./server/node_modules/eslint-config-prettier/index.js\"\nimport prettier from \"./server/node_modules/eslint-plugin-prettier/eslint-plugin-prettier.js\"\nimport react from \"./server/node_modules/eslint-plugin-react/index.js\"\nimport reactRefresh from \"./server/node_modules/eslint-plugin-react-refresh/index.js\"\nimport reactHooks from \"./server/node_modules/eslint-plugin-react-hooks/index.js\"\nimport ftFlow from \"./server/node_modules/eslint-plugin-ft-flow/dist/index.js\"\nimport hermesParser from \"./server/node_modules/hermes-eslint/dist/index.js\"\n\nconst reactRecommended = react.configs.recommended\nconst jsxRuntime = react.configs[\"jsx-runtime\"]\n\nexport default [\n  eslintRecommended.configs.recommended,\n  eslintConfigPrettier,\n  {\n    ignores: [\"**/*.test.js\"],\n    languageOptions: {\n      parser: hermesParser,\n      parserOptions: {\n        ecmaFeatures: { jsx: true }\n      },\n      ecmaVersion: 2020,\n      sourceType: \"module\",\n      globals: {\n        ...globals.browser,\n        ...globals.es2020,\n        ...globals.node\n      }\n    },\n    linterOptions: { reportUnusedDisableDirectives: true },\n    settings: { react: { version: \"18.2\" } },\n    plugins: {\n      ftFlow,\n      react,\n      \"jsx-runtime\": jsxRuntime,\n      \"react-hooks\": reactHooks,\n      prettier\n    },\n    rules: {\n      ...reactRecommended.rules,\n      ...reactHooks.configs.recommended.rules,\n      ...ftFlow.recommended,\n      \"no-unused-vars\": \"warn\",\n      \"no-undef\": \"warn\",\n      \"no-empty\": \"warn\",\n      \"no-extra-boolean-cast\": \"warn\",\n      \"no-prototype-builtins\": \"off\",\n      \"prettier/prettier\": \"warn\"\n    }\n  },\n  {\n    files: [\"frontend/src/**/*.js\"],\n    plugins: {\n      ftFlow,\n      prettier\n    },\n    rules: {\n      \"prettier/prettier\": \"warn\"\n    }\n  },\n  {\n    files: [\n      \"server/endpoints/**/*.js\",\n      \"server/models/**/*.js\",\n      \"server/swagger/**/*.js\",\n      \"server/utils/**/*.js\",\n      \"server/index.js\"\n    ],\n    rules: {\n      \"no-undef\": \"warn\"\n    }\n  },\n  {\n    files: [\"frontend/src/**/*.jsx\"],\n    plugins: {\n      ftFlow,\n      react,\n      \"jsx-runtime\": jsxRuntime,\n      \"react-hooks\": reactHooks,\n      \"react-refresh\": reactRefresh,\n      prettier\n    },\n    rules: {\n      ...jsxRuntime.rules,\n      \"react/prop-types\": \"off\", // FIXME\n      \"react-refresh/only-export-components\": \"warn\"\n    }\n  }\n]\n"
  },
  {
    "path": "extras/scripts/verifyPackageVersions.mjs",
    "content": "import serverPackageJson from '../../server/package.json' assert { type: 'json' };\nimport collectorPackageJson from '../../collector/package.json' assert { type: 'json' };\nconst { dependencies: serverDependencies } = serverPackageJson;\nconst { dependencies: collectorDependencies } = collectorPackageJson;\n\nconst serverDependenciesKeys = Object.keys(serverDependencies);\nconst collectorDependenciesKeys = Object.keys(collectorDependencies);\nconst commonDependencies = Array.from(new Set([\n  ...serverDependenciesKeys.filter((key) => collectorDependenciesKeys.includes(key)),\n  ...collectorDependenciesKeys.filter((key) => serverDependenciesKeys.includes(key)),\n]));\n\nconst ignores = [\n  \"@langchain/community\" // We are slowly removing this dependency from the app - its use is not critical\n]\n\nconsole.log(`${commonDependencies.length} common dependencies found`, commonDependencies);\nconsole.log(`Verifying (serverVersion == collectorVersion) for each common dependency`);\n\nconst failed = [];\ncommonDependencies.forEach((dependency) => {\n  console.log(`Verifying ${dependency}: ${serverDependencies[dependency]} == ${collectorDependencies[dependency]}`);\n  if (serverDependencies[dependency] !== collectorDependencies[dependency]) {\n    if (ignores.includes(dependency)) console.log(`${dependency} is in ignore list.`);\n    else failed.push({ dependency, serverVersion: serverDependencies[dependency], collectorVersion: collectorDependencies[dependency] });\n  }\n});\n\nif (failed.length > 0) {\n  console.log(`❌ ${failed.length} dependencies failed to verify`, JSON.stringify(failed, null, 2));\n  throw new Error(`${failed.length} dependencies failed to verify!`);\n}\n\nconsole.log(`👍 All dependencies match between server and collector!`);\n"
  },
  {
    "path": "extras/support/announcements/2025-04-08.json",
    "content": "[\n  {\n    \"thumbnail_url\": \"https://cdn.anythingllm.com/support/announcements/assets/mcp.jpg\",\n    \"title\": \"MCP Support\",\n    \"short_description\": \"Import and leverage MCP tools using AnythingLLM.\",\n    \"goto\": \"https://docs.anythingllm.com/mcp-compatibility/overview\",\n    \"author\": \"AnythingLLM\",\n    \"date\": \"April 8, 2025\"\n  },\n  {\n    \"thumbnail_url\": \"https://blogs.nvidia.com/wp-content/uploads/2025/03/nv-raig-032525-nv-blog-1280x680-1-scaled.jpg\",\n    \"title\": \"NVIDIA NIM Support\",\n    \"short_description\": \"Unlock the power of NVIDIA NIM on Windows with RTX GPU in our latest update via the NVIDIA NIM LLM provider.\",\n    \"goto\": \"https://blogs.nvidia.com/blog/rtx-ai-garage-nim-blueprints-g-assist\",\n    \"author\": \"NVIDIA\",\n    \"date\": \"March 25, 2025\"\n  },\n  {\n    \"thumbnail_url\": null,\n    \"title\": \"Community Hub Updates\",\n    \"short_description\": \"We refreshed the Community Hub with a new look and feel. Check it out!\",\n    \"goto\": \"https://hub.anythingllm.com\",\n    \"author\": \"AnythingLLM\",\n    \"date\": \"March 12, 2025\"\n  }\n]"
  },
  {
    "path": "extras/support/announcements/2025-07-08.json",
    "content": "[\n  {\n    \"thumbnail_url\": \"https://cdn.anythingllm.com/support/announcements/assets/private-browsing.png\",\n    \"title\": \"Private Web Scraping\",\n    \"short_description\": \"Scrape private, gated, or authenticated websites using the browser tool.\",\n    \"goto\": \"https://docs.anythingllm.com/features/browser-tool\",\n    \"author\": \"AnythingLLM\",\n    \"date\": \"July 8, 2025\"\n  },\n  {\n    \"thumbnail_url\": \"https://cdn.anythingllm.com/support/announcements/assets/mobile.png\",\n    \"title\": \"AnythingLLM Mobile Beta\",\n    \"short_description\": \"AnythingLLM on-device, offline, and private. Syncs with AnythingLLM.\",\n    \"goto\": \"https://docs.google.com/forms/d/e/1FAIpQLSdrMRCUXVDWKrtNTcHzVcHQk_SRoiT8X8tiawTFtAhjMq_L6Q/viewform\",\n    \"author\": \"AnythingLLM\",\n    \"date\": \"July 3, 2025\"\n  },\n  {\n    \"title\": \"Community Hub updates\",\n    \"short_description\": \"You can now easily push and pull Agent Flows, System Prompts, and more to the AnythingLLM Community Hub.\",\n    \"goto\": \"https://hub.anythingllm.com\",\n    \"author\": \"AnythingLLM\",\n    \"date\": \"June 25, 2025\"\n  }\n]"
  },
  {
    "path": "extras/support/announcements/2026-01-12.json",
    "content": "[\n  {\n    \"thumbnail_url\": \"https://cdn.anythingllm.com/support/announcements/assets/meeting-assistant.png\",\n    \"title\": \"Meeting Assistant\",\n    \"short_description\": \"Transcribe meetings and generate meeting notes entirely on device.\",\n    \"goto\": \"https://docs.anythingllm.com/meeting-assistant/introduction\",\n    \"author\": \"AnythingLLM\",\n    \"date\": \"January 21, 2026\"\n  },\n  {\n    \"thumbnail_url\": \"https://cdn.anythingllm.com/support/announcements/assets/mobile.png\",\n    \"title\": \"AnythingLLM Mobile\",\n    \"short_description\": \"AnythingLLM Mobile is now available on the Google Play Store.\",\n    \"goto\": \"https://play.google.com/store/apps/details?id=com.anythingllm\",\n    \"author\": \"AnythingLLM\",\n    \"date\": \"January 5, 2026\"\n  },\n  {\n    \"title\": \"50K Stars on Github\",\n    \"short_description\": \"AnythingLLM broke 50K stars on Github!\",\n    \"goto\": \"https://github.com/mintplex-labs/anything-llm\",\n    \"author\": \"AnythingLLM\",\n    \"date\": \"October 21, 2025\"\n  }\n]"
  },
  {
    "path": "extras/support/announcements/list.txt",
    "content": "2026-01-12.json\n2025-07-08.json\n2025-04-08.json"
  },
  {
    "path": "extras/translator/.env.example",
    "content": "DOCKER_MODEL_RUNNER_BASE_PATH='http://127.0.0.1:12434/engines/llama.cpp/v1'\nDOCKER_MODEL_RUNNER_LLM_MODEL_TOKEN_LIMIT=128000"
  },
  {
    "path": "extras/translator/README.md",
    "content": "# AnythingLLM Auto-translater\n\nThe AnythingLLM Auto-translator is a way for us to translate our locales but at no cost or overhead. However these are expected to be run manually and while they may not be 100% accurate improvement in models have made this work trivial without the need for an api.\n\n## Getting started\n\n- Install Ollama [w/Docker Model Runner](https://ollama.com)\n- `ollama pull translategemma:4b`\n- `cd extras/translator && cp .env.example .env`\n\n## Run the script\n\nAll translations are based on english dictionary. So the English dictionary must have an entry for it to be supported.\n\n`cd extras/translator`\n\n**Target a specific language**\n`node index.mjs <lang-code>` eg: `node index.mjs es` for Spanish.\n\n**Do all languages**\n`node index.mjs --all` _this is NOT recommended_\n\n\n## Gotchas\n\n- Sometimes translations go on for a while until the token window is exceeded - you can see this by the massive lines extending beyond the page.\n- Some languages operate different with single words or special symbols like \"@\" and will go off the rails. If the English text is one word and the translated text is 100 words, you dont need to be linguist to know that it is probably wrong.\n- You should always review every line for discrepencies or removal of `{{}}` inputs or brand name hallunications eg: `AnyLLM` instead of `AnythingLLM`"
  },
  {
    "path": "extras/translator/index.mjs",
    "content": "import fs from 'fs';\nimport {resources} from '../../frontend/src/locales/resources.js';\nimport \"../../server/node_modules/dotenv/lib/main.js\";\n\nfunction getNestedValue(obj, path) {\n    const keys = path.split('.');\n    let result = obj;\n    for (const key of keys) {\n      if (result == null) return undefined;\n      result = result[key];\n    }\n    return result;\n  }\n\nfunction setNestedValue(obj, path, value) {\n    const keys = path.split('.');\n    let result = obj;\n    for (let i = 0; i < keys.length - 1; i++) {\n      const key = keys[i];\n      if (result[key] == null) result[key] = {};\n      result = result[key];\n    }\n    result[keys[keys.length - 1]] = value;\n}\n\n/**\n * Extract {{variableName}} placeholders from text and replace with tokens.\n * Returns the modified text and a map to restore the originals.\n * @param {string} text\n * @returns {{ text: string, placeholders: string[] }}\n */\nfunction extractPlaceholders(text) {\n    const placeholders = [];\n    const modifiedText = text.replace(/\\{\\{([^}]+)\\}\\}/g, (match) => {\n        const index = placeholders.length;\n        placeholders.push(match);\n        return `__PLACEHOLDER_${index}__`;\n    });\n    return { text: modifiedText, placeholders };\n}\n\n/**\n * Restore original {{variableName}} placeholders from tokens.\n * @param {string} text\n * @param {string[]} placeholders\n * @returns {string}\n */\nfunction restorePlaceholders(text, placeholders) {\n    return text.replace(/__PLACEHOLDER_(\\d+)__/g, (_, index) => {\n        return placeholders[parseInt(index, 10)] || `__PLACEHOLDER_${index}__`;\n    });\n}\n\n/**\n * Extract Trans component tags like <italic>, </italic>, <bold>, </bold>, etc.\n * These are used by react-i18next Trans component for rich text formatting.\n * @param {string} text\n * @returns {{ text: string, tags: string[] }}\n */\nfunction extractTransTags(text) {\n    const tags = [];\n    // Match opening tags <tagName> and closing tags </tagName>\n    // Also matches self-closing tags <tagName />\n    const modifiedText = text.replace(/<\\/?([a-zA-Z][a-zA-Z0-9]*)\\s*\\/?>/g, (match) => {\n        const index = tags.length;\n        tags.push(match);\n        return `__TAG_${index}__`;\n    });\n    return { text: modifiedText, tags };\n}\n\n/**\n * Restore original Trans component tags from tokens.\n * @param {string} text\n * @param {string[]} tags\n * @returns {string}\n */\nfunction restoreTransTags(text, tags) {\n    return text.replace(/__TAG_(\\d+)__/g, (_, index) => {\n        return tags[parseInt(index, 10)] || `__TAG_${index}__`;\n    });\n}\n\n/**\n * Validate that all placeholders from source exist in translated text.\n * @param {string} sourceText\n * @param {string} translatedText\n * @returns {{ valid: boolean, missing: string[] }}\n */\nfunction validatePlaceholders(sourceText, translatedText) {\n    const sourceMatches = sourceText.match(/\\{\\{([^}]+)\\}\\}/g) || [];\n    const translatedMatches = translatedText.match(/\\{\\{([^}]+)\\}\\}/g) || [];\n    const missing = sourceMatches.filter(p => !translatedMatches.includes(p));\n    return { valid: missing.length === 0, missing };\n}\n\n/**\n * Validate that all Trans component tags from source exist in translated text.\n * @param {string} sourceText\n * @param {string} translatedText\n * @returns {{ valid: boolean, missing: string[] }}\n */\nfunction validateTransTags(sourceText, translatedText) {\n    const sourceMatches = sourceText.match(/<\\/?([a-zA-Z][a-zA-Z0-9]*)\\s*\\/?>/g) || [];\n    const translatedMatches = translatedText.match(/<\\/?([a-zA-Z][a-zA-Z0-9]*)\\s*\\/?>/g) || [];\n    const missing = sourceMatches.filter(t => !translatedMatches.includes(t));\n    return { valid: missing.length === 0, missing };\n}\n\nclass Translator {\n    static modelTag = 'translategemma:4b'\n    constructor() {\n        this.localeObj = new Intl.DisplayNames(Object.keys(resources), { type: 'language' });\n    }\n\n    getLanguageName(localeCode) {\n        try {\n          return this.localeObj.of(localeCode);\n        } catch (error) {\n          console.error(\"Error getting language name:\", error);\n          return null;\n        }\n      }\n   \n    #log(text, ...args) {\n        console.log(`\\x1b[32m[Translator]\\x1b[0m ${text}`, ...args);\n    }\n\n    static slog(text, ...args) {\n        console.log(`\\x1b[32m[Translator]\\x1b[0m ${text}`, ...args);\n    }\n\n    buildPrompt(text, sourceLangCode, targetLangCode, { hasPlaceholders = false, hasTags = false } = {}) {\n        const sourceLanguage = this.getLanguageName(sourceLangCode);\n        const targetLanguage = this.getLanguageName(targetLangCode);\n        \n        let specialInstructions = '';\n        if (hasPlaceholders || hasTags) {\n            const items = [];\n            if (hasPlaceholders) items.push('__PLACEHOLDER_0__, __PLACEHOLDER_1__');\n            if (hasTags) items.push('__TAG_0__, __TAG_1__');\n            specialInstructions = `\\nIMPORTANT: The text contains tokens like ${items.join(', ')}, etc. You MUST keep these tokens exactly as they are in the translation - do not translate, modify, or remove them.`;\n        }\n        \n        return `You are a professional ${sourceLanguage} (${sourceLangCode.toLowerCase()}) to ${targetLanguage} (${targetLangCode.toLowerCase()}) translator. Your goal is to accurately convey the meaning and nuances of the original ${sourceLanguage} text while adhering to ${targetLanguage} grammar, vocabulary, and cultural sensitivities.${specialInstructions}\nProduce only the ${targetLanguage} translation, without any additional explanations or commentary. Please translate the following ${sourceLanguage} text into ${targetLanguage}:\n\n\n${text}`\n    }\n\n    /**\n     * Clean the output text from the model\n     * Output text: `在助手回复中呈现 HTML 响应。这可以显著提高回复的质量，但也可能带来潜在的安全风险。<|im_end|>`\n     * We want to remove the <|im_end|> or im_start tags\n     * @param {*} text \n     * @returns \n     */\n    cleanOutputText(text) {\n        return text.replace(/<\\|im_end\\|>|<\\|im_start\\|>/g, '').trim();\n    }\n\n    async translate(text, sourceLangCode, targetLangCode) {\n        // Extract placeholders like {{variableName}} and replace with tokens\n        const { text: textWithPlaceholders, placeholders } = extractPlaceholders(text);\n        const hasPlaceholders = placeholders.length > 0;\n        \n        // Extract Trans component tags like <italic>, </italic>, etc.\n        const { text: textWithTokens, tags } = extractTransTags(textWithPlaceholders);\n        const hasTags = tags.length > 0;\n        \n        const prompt = this.buildPrompt(textWithTokens, sourceLangCode, targetLangCode, { hasPlaceholders, hasTags });\n        const response = await fetch(`http://127.0.0.1:11434/api/chat`, {\n            method: 'POST',\n            headers: { 'Content-Type': 'application/json' },\n            body: JSON.stringify({\n                model: Translator.modelTag,\n                messages: [{ role: 'user', content: prompt }],\n                temperature: 0.1,\n                stream: false,\n            }),\n        });\n        \n        if(!response.ok) throw new Error(`Failed to translate: ${response.statusText}`);\n        const data = await response.json();\n        let translatedText = this.cleanOutputText(data.message.content);\n        \n        // Restore Trans component tags first (order matters since tags may contain placeholders)\n        if (hasTags) {\n            translatedText = restoreTransTags(translatedText, tags);\n            \n            // Validate all tags were preserved\n            const tagValidation = validateTransTags(text, translatedText);\n            if (!tagValidation.valid) {\n                console.warn(`Warning: Missing Trans tags in translation: ${tagValidation.missing.join(', ')}`);\n                for (let i = 0; i < tags.length; i++) {\n                    if (!translatedText.includes(tags[i])) {\n                        console.warn(`  Tag ${tags[i]} was lost in translation`);\n                    }\n                }\n            }\n        }\n        \n        // Restore original placeholders\n        if (hasPlaceholders) {\n            translatedText = restorePlaceholders(translatedText, placeholders);\n            \n            // Validate all placeholders were preserved\n            const validation = validatePlaceholders(text, translatedText);\n            if (!validation.valid) {\n                console.warn(`Warning: Missing placeholders in translation: ${validation.missing.join(', ')}`);\n                // Attempt to fix by checking if tokens remain untranslated\n                for (let i = 0; i < placeholders.length; i++) {\n                    if (!translatedText.includes(placeholders[i])) {\n                        console.warn(`  Placeholder ${placeholders[i]} was lost in translation`);\n                    }\n                }\n            }\n        }\n        \n        return translatedText;\n    }\n\n    writeTranslations(langCode, translations) {\n        let langFilename = langCode.toLowerCase();\n        // Special cases\n        if(langCode === 'pt') langFilename = 'pt_BR';\n        if(langCode === 'zh-tw') langFilename = 'zh_TW';\n        if(langCode === 'vi') langFilename = 'vn';\n\n        fs.writeFileSync(\n            `../../frontend/src/locales/${langFilename}/common.js`,\n            `// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = ${JSON.stringify(translations, null, 2)}\n\nexport default TRANSLATIONS;`\n        );\n        console.log(`Updated ${langCode} translations file`);\n    }\n}\n\n\n// Deep traverse the english translations and get all the path to any all keys\nconst translator = new Translator();\nconst englishTranslations = resources.en.common;\nconst allKeys = [];\nfunction traverseTranslations(translations, parentKey = '') {\n    for(const [key, value] of Object.entries(translations)) {\n        const fullKey = !parentKey ? key : `${parentKey}.${key}`;\n        if(typeof value === 'object' && value !== null) {\n            traverseTranslations(value, fullKey);\n        } else {\n            allKeys.push(fullKey);\n        }\n    }\n}\ntraverseTranslations(englishTranslations);\ndelete resources.en;\n\nasync function translateAllLanguages() {\n    for(const [langCode, { common }] of Object.entries(resources)) {\n        console.log(`Translating ${translator.getLanguageName(langCode)}(${langCode}) to all languages`);\n        await translateSingleLanguage(langCode);\n    }\n}\n\nasync function translateSingleLanguage(langCode) {\n    let totalTranslations = 0;\n    for(const key of allKeys) {\n        const sourceText = getNestedValue(englishTranslations, key);\n        if(!sourceText) continue;\n\n        // If the source text is @agent, set the translation to @agent - this has no\n        // direct translation and must be handled manually\n        if(sourceText === '@agent') {\n            setNestedValue(resources[langCode].common, key, '@agent');\n            continue;\n        }\n        if(sourceText === '/reset') {\n            setNestedValue(resources[langCode].common, key, '/reset');\n            continue;\n        }\n\n        const value = getNestedValue(resources[langCode].common, key);\n        if(!!value) continue; // If the translation is already present, skip it\n\n        console.log(`Translation not found for ${translator.getLanguageName(langCode)}(${langCode})`, {\n            key,\n            sourceText,\n        });\n        const outputText = await translator.translate(sourceText, 'en', langCode);\n        if(!outputText) {\n            console.log('No output text - skipping');\n            continue;\n        }\n\n        console.log(`Output text: ${outputText}`);\n        setNestedValue(resources[langCode].common, key, outputText);\n        console.log(`--------------------------------`);\n        totalTranslations++;\n    }\n\n    if(totalTranslations === 0) return console.log('No translations performed!');\n    console.log(`--------------------------------`);\n    console.log(`Translated ${totalTranslations} translations for ${langCode}`);\n    translator.writeTranslations(langCode, resources[langCode].common);\n    console.log(`--------------------------------`);\n}\n\nlet langArg = process.argv[2];\nif(langArg) {\n    if(langArg.toLowerCase() === '--all') await translateAllLanguages();\n    else {\n        if(!Object.keys(resources).includes(langArg)) throw new Error(`Language ${langArg} not found in resources`);\n        await translateSingleLanguage(langArg);\n    }\n} else {\n    throw new Error('Please provide a language code as an argument or --all to translate all languages');\n}"
  },
  {
    "path": "frontend/.env.example",
    "content": "VITE_API_BASE='http://localhost:3001/api' # Use this URL when developing locally\n# VITE_API_BASE=\"https://$CODESPACE_NAME-3001.$GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN/api\" # for GitHub Codespaces\n# VITE_API_BASE='/api' # Use this URL deploying on non-localhost address OR in docker.\n"
  },
  {
    "path": "frontend/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\nbundleinspector.html\n.env.production\nflow-typed\n"
  },
  {
    "path": "frontend/.nvmrc",
    "content": "v18.18.0"
  },
  {
    "path": "frontend/eslint.config.js",
    "content": "import js from \"@eslint/js\"\nimport globals from \"globals\"\nimport pluginReact from \"eslint-plugin-react\"\nimport pluginReactHooks from \"eslint-plugin-react-hooks\"\nimport pluginPrettier from \"eslint-plugin-prettier\"\nimport configPrettier from \"eslint-config-prettier\"\nimport unusedImports from \"eslint-plugin-unused-imports\"\n\nexport default [\n  {\n    ignores: [\"**/*.min.js\", \"src/media/**/*\"]\n  },\n\n  // Base JS recommended rules\n  js.configs.recommended,\n\n  // Your React/JSX files\n  {\n    files: [\"src/**/*.{js,jsx,mjs,cjs}\"],\n    languageOptions: {\n      ecmaVersion: \"latest\",\n      sourceType: \"module\",\n      parserOptions: {\n        ecmaFeatures: { jsx: true }\n      },\n      globals: globals.browser\n    },\n    plugins: {\n      react: pluginReact,\n      \"react-hooks\": pluginReactHooks,\n      \"unused-imports\": unusedImports,\n      prettier: pluginPrettier\n    },\n    settings: {\n      react: { version: \"detect\" }\n    },\n    rules: {\n      // React recommended rules (inline, since we're not \"extending\" in flat config)\n      ...pluginReact.configs.flat.recommended.rules,\n\n      // If you want hooks rules, add these (recommended)\n      ...pluginReactHooks.configs.recommended.rules,\n\n      // Prettier: disable conflicting stylistic rules + optionally enforce formatting\n      ...configPrettier.rules,\n      \"prettier/prettier\": \"error\",\n\n      // Your overrides\n      \"react/react-in-jsx-scope\": \"off\",\n      \"react-hooks/exhaustive-deps\": \"off\",\n      \"react/prop-types\": \"off\",\n      \"react-hooks/set-state-in-effect\": \"off\",\n      \"react/jsx-no-target-blank\": \"error\",\n      \"react/no-unescaped-entities\": \"off\",\n      \"react/display-name\": \"off\",\n      \"react-hooks/immutability\": \"off\",\n      \"react-hooks/preserve-manual-memoization\": \"off\",\n      \"no-extra-boolean-cast\": \"off\",\n      \"no-prototype-builtins\": \"off\",\n      \"no-empty\": \"off\",\n      \"no-useless-escape\": \"off\",\n      \"no-undef\": \"error\",\n      \"no-unsafe-optional-chaining\": \"off\",\n      \"no-constant-binary-expression\": \"off\",\n\n      // Unused cleanup\n      \"no-unused-vars\": \"off\",\n      \"unused-imports/no-unused-imports\": \"error\",\n      \"unused-imports/no-unused-vars\": [\n        \"warn\",\n        {\n          vars: \"all\",\n          varsIgnorePattern: \"^_\",\n          args: \"after-used\",\n          argsIgnorePattern: \"^_\"\n        }\n      ]\n    }\n  }\n]\n"
  },
  {
    "path": "frontend/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.png\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>AnythingLLM | Your personal LLM trained on anything</title>\n\n    <meta name=\"title\" content=\"AnythingLLM | Your personal LLM trained on anything\">\n    <meta name=\"description\" content=\"AnythingLLM | Your personal LLM trained on anything\">\n\n    <!-- Facebook -->\n    <meta property=\"og:type\" content=\"website\">\n    <meta property=\"og:url\" content=\"https://anythingllm.com\">\n    <meta property=\"og:title\" content=\"AnythingLLM | Your personal LLM trained on anything\">\n    <meta property=\"og:description\" content=\"AnythingLLM | Your personal LLM trained on anything\">\n    <meta property=\"og:image\"\n      content=\"https://raw.githubusercontent.com/Mintplex-Labs/anything-llm/master/images/promo.png\">\n\n    <!-- Twitter -->\n    <meta property=\"twitter:card\" content=\"summary_large_image\">\n    <meta property=\"twitter:url\" content=\"https://anythingllm.com\">\n    <meta property=\"twitter:title\" content=\"AnythingLLM | Your personal LLM trained on anything\">\n    <meta property=\"twitter:description\" content=\"AnythingLLM | Your personal LLM trained on anything\">\n    <meta property=\"twitter:image\"\n      content=\"https://raw.githubusercontent.com/Mintplex-Labs/anything-llm/master/images/promo.png\">\n\n    <link rel=\"icon\" href=\"/favicon.png\" />\n    <link rel=\"apple-touch-icon\" href=\"/favicon.png\" />\n\n    <!-- PWA -->\n    <meta name=\"mobile-web-app-capable\" content=\"yes\">\n    <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">\n    <link rel=\"manifest\" href=\"/manifest.json\" />\n  </head>\n\n  <body>\n    <div id=\"root\" class=\"h-screen\"></div>\n    <script type=\"module\" src=\"/src/main.jsx\"></script>\n  </body>\n\n</html>\n"
  },
  {
    "path": "frontend/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"target\": \"esnext\",\n    \"jsx\": \"react\",\n    \"paths\": {\n      \"@/*\": [\n        \"./src/*\"\n      ]\n    }\n  }\n}"
  },
  {
    "path": "frontend/package.json",
    "content": "{\n  \"name\": \"anything-llm-frontend\",\n  \"private\": false,\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"start\": \"vite --open\",\n    \"dev\": \"cross-env NODE_ENV=development vite --debug --host=0.0.0.0\",\n    \"build\": \"vite build && node scripts/postbuild.js\",\n    \"lint:check\": \"eslint src\",\n    \"lint\": \"eslint --fix src\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@lobehub/icons\": \"^4.0.3\",\n    \"@microsoft/fetch-event-source\": \"^2.0.1\",\n    \"@mintplex-labs/piper-tts-web\": \"^1.0.4\",\n    \"@phosphor-icons/react\": \"^2.1.7\",\n    \"@tremor/react\": \"^3.15.1\",\n    \"dompurify\": \"^3.0.8\",\n    \"file-saver\": \"^2.0.5\",\n    \"he\": \"^1.2.0\",\n    \"highlight.js\": \"^11.9.0\",\n    \"i18next\": \"^23.11.3\",\n    \"i18next-browser-languagedetector\": \"^7.2.1\",\n    \"js-levenshtein\": \"^1.1.6\",\n    \"katex\": \"^0.6.0\",\n    \"lodash.debounce\": \"^4.0.8\",\n    \"markdown-it\": \"^13.0.1\",\n    \"moment\": \"^2.30.1\",\n    \"onnxruntime-web\": \"^1.18.0\",\n    \"pluralize\": \"^8.0.0\",\n    \"qrcode.react\": \"^4.2.0\",\n    \"react\": \"^18.2.0\",\n    \"react-beautiful-dnd\": \"13.1.1\",\n    \"react-confetti-explosion\": \"^2.1.2\",\n    \"react-device-detect\": \"^2.2.2\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-dropzone\": \"^14.2.3\",\n    \"react-error-boundary\": \"^6.0.0\",\n    \"react-highlight-words\": \"^0.21.0\",\n    \"react-i18next\": \"^14.1.1\",\n    \"react-loading-skeleton\": \"^3.1.0\",\n    \"react-router-dom\": \"^6.3.0\",\n    \"react-speech-recognition\": \"^3.10.0\",\n    \"react-tag-input-component\": \"^2.0.2\",\n    \"react-toastify\": \"^9.1.3\",\n    \"react-tooltip\": \"^5.25.2\",\n    \"recharts\": \"^2.12.5\",\n    \"recharts-to-png\": \"^2.3.1\",\n    \"text-case\": \"^1.0.9\",\n    \"truncate\": \"^3.0.0\",\n    \"uuid\": \"^9.0.0\"\n  },\n  \"devDependencies\": {\n    \"@esbuild-plugins/node-globals-polyfill\": \"^0.1.1\",\n    \"@eslint/js\": \"^9.39.2\",\n    \"@types/react\": \"^18.2.23\",\n    \"@types/react-dom\": \"^18.2.8\",\n    \"@types/react-router-dom\": \"^5.3.3\",\n    \"@vitejs/plugin-react\": \"^4.0.0-beta.0\",\n    \"autoprefixer\": \"^10.4.14\",\n    \"buffer\": \"^6.0.3\",\n    \"cross-env\": \"^7.0.3\",\n    \"eslint\": \"^9.39.2\",\n    \"eslint-config-prettier\": \"^9.0.0\",\n    \"eslint-plugin-ft-flow\": \"^3.0.0\",\n    \"eslint-plugin-prettier\": \"^5.0.0\",\n    \"eslint-plugin-react\": \"^7.37.5\",\n    \"eslint-plugin-react-hooks\": \"^7.0.1\",\n    \"eslint-plugin-react-refresh\": \"^0.4.3\",\n    \"eslint-plugin-unused-imports\": \"^4.3.0\",\n    \"flow-bin\": \"^0.217.0\",\n    \"flow-remove-types\": \"^2.217.1\",\n    \"globals\": \"^16.5.0\",\n    \"hermes-eslint\": \"^0.15.0\",\n    \"postcss\": \"^8.4.23\",\n    \"prettier\": \"^3.0.3\",\n    \"rollup-plugin-visualizer\": \"^5.9.0\",\n    \"tailwindcss\": \"^3.3.1\",\n    \"vite\": \"^4.3.0\"\n  }\n}\n"
  },
  {
    "path": "frontend/postcss.config.js",
    "content": "import tailwind from 'tailwindcss'\nimport autoprefixer from 'autoprefixer'\nimport tailwindConfig from './tailwind.config.js'\n\nexport default {\n  plugins: [tailwind(tailwindConfig), autoprefixer],\n}"
  },
  {
    "path": "frontend/public/manifest.json",
    "content": "{\n  \"name\": \"AnythingLLM\",\n  \"short_name\": \"AnythingLLM\",\n  \"display\": \"standalone\",\n  \"orientation\": \"portrait\",\n  \"start_url\": \"/\",\n  \"icons\": [\n    {\n      \"src\": \"/favicon.png\",\n      \"sizes\": \"any\"\n    }\n  ]\n}"
  },
  {
    "path": "frontend/public/robots.txt",
    "content": "User-agent: *\nDisallow: /"
  },
  {
    "path": "frontend/public/service-workers/push-notifications.js",
    "content": "function parseEventData(event) {\n    try {\n      return event.data.json();\n    } catch (e) {\n      console.error('Failed to parse event data - is payload valid? .text():\\n', event.data.text());\n      return null\n    }\n  }\n  \n  self.addEventListener('push', function (event) {\n    const payload = parseEventData(event);\n    if (!payload) return;\n  \n    // options: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#options\n    self.registration.showNotification(payload.title || 'AnythingLLM', {\n      ...payload,\n      icon: '/favicon.png',\n    });\n  });\n  \n  self.addEventListener('notificationclick', function (event) {\n    event.notification.close();\n    const { onClickUrl = null } = event.notification.data || {};\n    if (!onClickUrl) return;\n    event.waitUntil(clients.openWindow(onClickUrl));\n  });"
  },
  {
    "path": "frontend/scripts/postbuild.js",
    "content": "import { renameSync } from 'fs';\nimport { fileURLToPath } from 'url';\nimport path from 'path';\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconsole.log(`Running frontend post build script...`)\nrenameSync(path.resolve(__dirname, '../dist/index.html'), path.resolve(__dirname, '../dist/_index.html'));\nconsole.log(`index.html renamed to _index.html so SSR of the index page can be assumed.`);"
  },
  {
    "path": "frontend/src/App.jsx",
    "content": "import React, { Suspense } from \"react\";\nimport { Outlet, useLocation } from \"react-router-dom\";\nimport { I18nextProvider } from \"react-i18next\";\nimport { AuthProvider } from \"@/AuthContext\";\nimport { ToastContainer } from \"react-toastify\";\nimport \"react-toastify/dist/ReactToastify.css\";\nimport i18n from \"./i18n\";\n\nimport { PfpProvider } from \"./PfpContext\";\nimport { LogoProvider } from \"./LogoContext\";\nimport { FullScreenLoader } from \"./components/Preloader\";\nimport { ThemeProvider } from \"./ThemeContext\";\nimport { PWAModeProvider } from \"./PWAContext\";\nimport KeyboardShortcutsHelp from \"@/components/KeyboardShortcutsHelp\";\nimport { ErrorBoundary } from \"react-error-boundary\";\nimport ErrorBoundaryFallback from \"./components/ErrorBoundaryFallback\";\n\nexport default function App() {\n  const location = useLocation();\n  return (\n    <ErrorBoundary\n      FallbackComponent={ErrorBoundaryFallback}\n      onError={console.error}\n      resetKeys={[location.pathname]}\n    >\n      <ThemeProvider>\n        <PWAModeProvider>\n          <Suspense fallback={<FullScreenLoader />}>\n            <AuthProvider>\n              <LogoProvider>\n                <PfpProvider>\n                  <I18nextProvider i18n={i18n}>\n                    <Outlet />\n                    <ToastContainer />\n                    <KeyboardShortcutsHelp />\n                  </I18nextProvider>\n                </PfpProvider>\n              </LogoProvider>\n            </AuthProvider>\n          </Suspense>\n        </PWAModeProvider>\n      </ThemeProvider>\n    </ErrorBoundary>\n  );\n}\n"
  },
  {
    "path": "frontend/src/AuthContext.jsx",
    "content": "import React, { useState, createContext, useEffect } from \"react\";\nimport {\n  AUTH_TIMESTAMP,\n  AUTH_TOKEN,\n  AUTH_USER,\n  USER_PROMPT_INPUT_MAP,\n} from \"@/utils/constants\";\nimport System from \"./models/system\";\nimport { useNavigate } from \"react-router-dom\";\nimport { safeJsonParse } from \"@/utils/request\";\n\nexport const AuthContext = createContext(null);\nexport function AuthProvider(props) {\n  const localUser = localStorage.getItem(AUTH_USER);\n  const localAuthToken = localStorage.getItem(AUTH_TOKEN);\n  const [store, setStore] = useState({\n    user: localUser ? safeJsonParse(localUser, null) : null,\n    authToken: localAuthToken ? localAuthToken : null,\n  });\n\n  const navigate = useNavigate();\n\n  /* NOTE:\n   * 1. There's no reason for these helper functions to be stateful. They could\n   * just be regular funcs or methods on a basic object.\n   * 2. These actions are not being invoked anywhere in the\n   * codebase, dead code.\n   */\n  const [actions] = useState({\n    updateUser: (user, authToken = \"\") => {\n      localStorage.setItem(AUTH_USER, JSON.stringify(user));\n      localStorage.setItem(AUTH_TOKEN, authToken);\n      setStore({ user, authToken });\n    },\n    unsetUser: () => {\n      localStorage.removeItem(AUTH_USER);\n      localStorage.removeItem(AUTH_TOKEN);\n      localStorage.removeItem(AUTH_TIMESTAMP);\n      localStorage.removeItem(USER_PROMPT_INPUT_MAP);\n      setStore({ user: null, authToken: null });\n    },\n  });\n\n  /*\n   * On initial mount and whenever the token changes, fetch a new user object\n   * If the user is suspended, (success === false and data === null) logout the user and redirect to the login page\n   * If success is true and data is not null, update the user object in the store (multi-user mode only)\n   * If success is true and data is null, do nothing (single-user mode only) with or without password protection\n   */\n  useEffect(() => {\n    async function refreshUser() {\n      const { success, user: refreshedUser } = await System.refreshUser();\n      if (success && refreshedUser === null) return;\n\n      if (!success) {\n        localStorage.removeItem(AUTH_USER);\n        localStorage.removeItem(AUTH_TOKEN);\n        localStorage.removeItem(AUTH_TIMESTAMP);\n        localStorage.removeItem(USER_PROMPT_INPUT_MAP);\n        setStore({ user: null, authToken: null });\n        navigate(\"/login\");\n        return;\n      }\n\n      localStorage.setItem(AUTH_USER, JSON.stringify(refreshedUser));\n      setStore((prev) => ({\n        ...prev,\n        user: refreshedUser,\n      }));\n    }\n    if (store.authToken) refreshUser();\n  }, [store.authToken]);\n\n  return (\n    <AuthContext.Provider value={{ store, actions }}>\n      {props.children}\n    </AuthContext.Provider>\n  );\n}\n"
  },
  {
    "path": "frontend/src/LogoContext.jsx",
    "content": "import { createContext, useEffect, useState } from \"react\";\nimport AnythingLLM from \"./media/logo/anything-llm.png\";\nimport AnythingLLMDark from \"./media/logo/anything-llm-dark.png\";\nimport DefaultLoginLogoLight from \"./media/illustrations/login-logo.svg\";\nimport DefaultLoginLogoDark from \"./media/illustrations/login-logo-light.svg\";\nimport System from \"./models/system\";\n\nexport const REFETCH_LOGO_EVENT = \"refetch-logo\";\n\nfunction isLightMode() {\n  return document.documentElement.getAttribute(\"data-theme\") === \"light\";\n}\nexport const LogoContext = createContext();\n\nexport function LogoProvider({ children }) {\n  const [logo, setLogo] = useState(\"\");\n  const [loginLogo, setLoginLogo] = useState(\"\");\n  const [isCustomLogo, setIsCustomLogo] = useState(false);\n\n  async function fetchInstanceLogo() {\n    const DefaultLoginLogo = isLightMode()\n      ? DefaultLoginLogoDark\n      : DefaultLoginLogoLight;\n    try {\n      const { isCustomLogo, logoURL } = await System.fetchLogo();\n      if (logoURL) {\n        setLogo(logoURL);\n        setLoginLogo(isCustomLogo ? logoURL : DefaultLoginLogo);\n        setIsCustomLogo(isCustomLogo);\n      } else {\n        isLightMode() ? setLogo(AnythingLLMDark) : setLogo(AnythingLLM);\n        setLoginLogo(DefaultLoginLogo);\n        setIsCustomLogo(false);\n      }\n    } catch (err) {\n      isLightMode() ? setLogo(AnythingLLMDark) : setLogo(AnythingLLM);\n      setLoginLogo(DefaultLoginLogo);\n      setIsCustomLogo(false);\n      console.error(\"Failed to fetch logo:\", err);\n    }\n  }\n\n  useEffect(() => {\n    fetchInstanceLogo();\n    window.addEventListener(REFETCH_LOGO_EVENT, fetchInstanceLogo);\n    return () => {\n      window.removeEventListener(REFETCH_LOGO_EVENT, fetchInstanceLogo);\n    };\n  }, []);\n\n  return (\n    <LogoContext.Provider value={{ logo, setLogo, loginLogo, isCustomLogo }}>\n      {children}\n    </LogoContext.Provider>\n  );\n}\n"
  },
  {
    "path": "frontend/src/PWAContext.jsx",
    "content": "import React, {\n  createContext,\n  useContext,\n  useEffect,\n  useMemo,\n  useState,\n} from \"react\";\n\n/**\n * Detects if the application is running as a standalone PWA\n * @returns {boolean} True if running as standalone PWA\n */\nfunction isStandalonePWA() {\n  if (typeof window === \"undefined\") return false;\n\n  const matchesStandaloneDisplayMode =\n    typeof window.matchMedia === \"function\"\n      ? window.matchMedia(\"(display-mode: standalone)\")?.matches\n      : false;\n\n  const isIOSStandalone = window.navigator?.standalone === true; // iOS Safari\n  const androidReferrer =\n    typeof document !== \"undefined\" && document?.referrer\n      ? document.referrer.includes(\"android-app://\")\n      : false;\n\n  return Boolean(\n    matchesStandaloneDisplayMode || isIOSStandalone || androidReferrer\n  );\n}\n\nconst PWAModeContext = createContext({ isPWA: false });\nexport function PWAModeProvider({ children }) {\n  const [isPWA, setIsPWA] = useState(() => isStandalonePWA());\n\n  useEffect(() => {\n    if (typeof window === \"undefined\") return undefined;\n\n    const mediaQuery =\n      typeof window.matchMedia === \"function\"\n        ? window.matchMedia(\"(display-mode: standalone)\")\n        : null;\n\n    const updateStatus = () => setIsPWA(isStandalonePWA());\n\n    updateStatus();\n\n    if (mediaQuery?.addEventListener) {\n      mediaQuery.addEventListener(\"change\", updateStatus);\n    } else if (mediaQuery?.addListener) {\n      mediaQuery.addListener(updateStatus);\n    }\n\n    window.addEventListener(\"appinstalled\", updateStatus);\n    window.addEventListener(\"visibilitychange\", updateStatus);\n\n    return () => {\n      if (mediaQuery?.removeEventListener) {\n        mediaQuery.removeEventListener(\"change\", updateStatus);\n      } else if (mediaQuery?.removeListener) {\n        mediaQuery.removeListener(updateStatus);\n      }\n\n      window.removeEventListener(\"appinstalled\", updateStatus);\n      window.removeEventListener(\"visibilitychange\", updateStatus);\n    };\n  }, []);\n\n  useEffect(() => {\n    if (typeof document === \"undefined\") return undefined;\n\n    document.body.classList.toggle(\"pwa\", isPWA);\n    document.documentElement?.setAttribute(\n      \"data-pwa\",\n      isPWA ? \"true\" : \"false\"\n    );\n\n    return () => {\n      document.body.classList.remove(\"pwa\");\n      document.documentElement?.removeAttribute(\"data-pwa\");\n    };\n  }, [isPWA]);\n\n  const value = useMemo(() => ({ isPWA }), [isPWA]);\n\n  return (\n    <PWAModeContext.Provider value={value}>{children}</PWAModeContext.Provider>\n  );\n}\n\nexport function usePWAMode() {\n  return useContext(PWAModeContext);\n}\n"
  },
  {
    "path": "frontend/src/PfpContext.jsx",
    "content": "import React, { createContext, useState, useEffect } from \"react\";\nimport useUser from \"./hooks/useUser\";\nimport System from \"./models/system\";\n\nexport const PfpContext = createContext();\n\nexport function PfpProvider({ children }) {\n  const [pfp, setPfp] = useState(null);\n  const { user } = useUser();\n\n  useEffect(() => {\n    async function fetchPfp() {\n      if (!user?.id) return;\n      try {\n        const pfpUrl = await System.fetchPfp(user.id);\n        setPfp(pfpUrl);\n      } catch (err) {\n        setPfp(null);\n        console.error(\"Failed to fetch pfp:\", err);\n      }\n    }\n    fetchPfp();\n  }, [user?.id]);\n\n  return (\n    <PfpContext.Provider value={{ pfp, setPfp }}>\n      {children}\n    </PfpContext.Provider>\n  );\n}\n"
  },
  {
    "path": "frontend/src/ThemeContext.jsx",
    "content": "import React, { createContext, useContext } from \"react\";\nimport { useTheme } from \"./hooks/useTheme\";\n\nconst ThemeContext = createContext();\n\nexport function ThemeProvider({ children }) {\n  const themeValue = useTheme();\n\n  return (\n    <ThemeContext.Provider value={themeValue}>{children}</ThemeContext.Provider>\n  );\n}\n\nexport function useThemeContext() {\n  return useContext(ThemeContext);\n}\n"
  },
  {
    "path": "frontend/src/components/CanViewChatHistory/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { FullScreenLoader } from \"@/components/Preloader\";\nimport System from \"@/models/system\";\nimport paths from \"@/utils/paths\";\n\n/**\n * Protects the view from system set ups who cannot view chat history.\n * If the user cannot view chat history, they are redirected to the home page.\n * @param {React.ReactNode} children\n */\nexport function CanViewChatHistory({ children }) {\n  const { loading, viewable } = useCanViewChatHistory();\n  if (loading) return <FullScreenLoader />;\n  if (!viewable) {\n    window.location.href = paths.home();\n    return <FullScreenLoader />;\n  }\n\n  return <>{children}</>;\n}\n\n/**\n * Provides the `viewable` state to the children.\n * @returns {React.ReactNode}\n */\nexport function CanViewChatHistoryProvider({ children }) {\n  const { loading, viewable } = useCanViewChatHistory();\n  if (loading) return null;\n  return <>{children({ viewable })}</>;\n}\n\n/**\n * Hook that fetches the can view chat history state from local storage or the system settings.\n * @returns {Promise<{viewable: boolean, error: string | null}>}\n */\nexport function useCanViewChatHistory() {\n  const [loading, setLoading] = useState(true);\n  const [viewable, setViewable] = useState(false);\n\n  useEffect(() => {\n    async function fetchViewable() {\n      const { viewable } = await System.fetchCanViewChatHistory();\n      setViewable(viewable);\n      setLoading(false);\n    }\n    fetchViewable();\n  }, []);\n\n  return { loading, viewable };\n}\n"
  },
  {
    "path": "frontend/src/components/ChangeWarning/index.jsx",
    "content": "import { Warning, X } from \"@phosphor-icons/react\";\n\nexport default function ChangeWarningModal({\n  warningText = \"\",\n  onClose,\n  onConfirm,\n}) {\n  return (\n    <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden z-9999\">\n      <div className=\"relative px-6 py-5 border-b rounded-t border-theme-modal-border\">\n        <div className=\"w-full flex gap-x-2 items-center\">\n          <Warning className=\"text-red-500 w-6 h-6\" weight=\"fill\" />\n          <h3 className=\"text-xl font-semibold text-red-500 overflow-hidden overflow-ellipsis whitespace-nowrap\">\n            WARNING - This action is irreversible\n          </h3>\n        </div>\n        <button\n          onClick={onClose}\n          type=\"button\"\n          className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n        >\n          <X size={24} weight=\"bold\" className=\"text-white\" />\n        </button>\n      </div>\n      <div\n        className=\"h-full w-full overflow-y-auto\"\n        style={{ maxHeight: \"calc(100vh - 200px)\" }}\n      >\n        <div className=\"py-7 px-9 space-y-2 flex-col\">\n          <p className=\"text-white\">\n            {warningText.split(\"\\\\n\").map((line, index) => (\n              <span key={index}>\n                {line}\n                <br />\n              </span>\n            ))}\n            <br />\n            <br />\n            Are you sure you want to proceed?\n          </p>\n        </div>\n      </div>\n      <div className=\"flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b\">\n        <button\n          onClick={onClose}\n          type=\"button\"\n          className=\"transition-all duration-300 bg-transparent text-white hover:opacity-60 px-4 py-2 rounded-lg text-sm border-none\"\n        >\n          Cancel\n        </button>\n        <button\n          onClick={onConfirm}\n          type=\"submit\"\n          className=\"transition-all duration-300 bg-red-500 light:text-white text-white hover:opacity-60 px-4 py-2 rounded-lg text-sm border-none\"\n        >\n          Confirm\n        </button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/ChatBubble/index.jsx",
    "content": "import React from \"react\";\nimport UserIcon from \"../UserIcon\";\nimport { userFromStorage } from \"@/utils/request\";\nimport renderMarkdown from \"@/utils/chat/markdown\";\nimport DOMPurify from \"@/utils/chat/purify\";\n\nexport default function ChatBubble({ message, type }) {\n  const isUser = type === \"user\";\n\n  return (\n    <div\n      className={`flex justify-center items-end w-full bg-theme-bg-secondary`}\n    >\n      <div className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}>\n        <div className=\"flex gap-x-5\">\n          <UserIcon\n            user={{ uid: isUser ? userFromStorage()?.username : \"system\" }}\n            role={type}\n          />\n\n          <div\n            className={`markdown whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}\n            dangerouslySetInnerHTML={{\n              __html: DOMPurify.sanitize(renderMarkdown(message)),\n            }}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/CommunityHub/PublishEntityModal/AgentFlows/index.jsx",
    "content": "import { useState, useRef } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport CommunityHub from \"@/models/communityHub\";\nimport showToast from \"@/utils/toast\";\nimport paths from \"@/utils/paths\";\nimport { X, CaretRight } from \"@phosphor-icons/react\";\nimport { BLOCK_INFO } from \"@/pages/Admin/AgentBuilder/BlockList\";\nimport { Link } from \"react-router-dom\";\n\nexport default function AgentFlows({ entity }) {\n  const { t } = useTranslation();\n  const formRef = useRef(null);\n  const [isSubmitting, setIsSubmitting] = useState(false);\n  const [tags, setTags] = useState([]);\n  const [tagInput, setTagInput] = useState(\"\");\n  const [isSuccess, setIsSuccess] = useState(false);\n  const [itemId, setItemId] = useState(null);\n  const [expandedStep, setExpandedStep] = useState(null);\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setIsSubmitting(true);\n    try {\n      const form = new FormData(formRef.current);\n      const data = {\n        name: form.get(\"name\"),\n        description: form.get(\"description\"),\n        tags: tags,\n        visibility: \"private\",\n        flow: JSON.stringify({\n          name: form.get(\"name\"),\n          description: form.get(\"description\"),\n          steps: entity.steps,\n          tags: tags,\n          visibility: \"private\",\n        }),\n      };\n      const { success, error, itemId } =\n        await CommunityHub.createAgentFlow(data);\n      if (!success) throw new Error(error);\n      setItemId(itemId);\n      setIsSuccess(true);\n    } catch (error) {\n      console.error(\"Failed to publish agent flow:\", error);\n      showToast(`Failed to publish agent flow: ${error.message}`, \"error\", {\n        clear: true,\n      });\n    } finally {\n      setIsSubmitting(false);\n    }\n  };\n\n  const handleKeyDown = (e) => {\n    if (e.key === \"Enter\" || e.key === \",\") {\n      e.preventDefault();\n      const value = tagInput.trim();\n      if (value.length > 20) return;\n      if (value && !tags.includes(value)) {\n        setTags((prevTags) => [...prevTags, value].slice(0, 5)); // Limit to 5 tags\n        setTagInput(\"\");\n      }\n    }\n  };\n\n  const removeTag = (tagToRemove) => {\n    setTags(tags.filter((tag) => tag !== tagToRemove));\n  };\n\n  if (isSuccess) {\n    return (\n      <div className=\"p-6 -mt-12 w-[400px]\">\n        <div className=\"flex flex-col items-center justify-center gap-y-2\">\n          <h3 className=\"text-lg font-semibold text-theme-text-primary\">\n            {t(\"community_hub.publish.agent_flow.success_title\")}\n          </h3>\n          <p className=\"text-lg text-theme-text-primary text-center max-w-2xl\">\n            {t(\"community_hub.publish.agent_flow.success_description\")}\n          </p>\n          <p className=\"text-theme-text-secondary text-center text-sm\">\n            {t(\"community_hub.publish.agent_flow.success_thank_you\")}\n          </p>\n          <Link\n            to={paths.communityHub.viewItem(\"agent-flow\", itemId)}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"w-[265px] bg-theme-bg-secondary hover:bg-theme-sidebar-item-hover text-theme-text-primary py-2 px-4 rounded-lg transition-colors mt-4 text-sm font-semibold text-center\"\n          >\n            {t(\"community_hub.publish.agent_flow.view_on_hub\")}\n          </Link>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <>\n      <div className=\"w-full flex gap-x-2 items-center mb-3 -mt-8\">\n        <h3 className=\"text-xl font-semibold text-theme-text-primary px-6 py-3\">\n          {t(\"community_hub.publish.agent_flow.modal_title\")}\n        </h3>\n      </div>\n      <form ref={formRef} className=\"flex\" onSubmit={handleSubmit}>\n        <div className=\"w-1/2 p-6 pt-0 space-y-4\">\n          <div>\n            <label className=\"block text-sm font-semibold text-theme-text-primary mb-1\">\n              {t(\"community_hub.publish.agent_flow.name_label\")}\n            </label>\n            <div className=\"text-xs text-theme-text-secondary mb-2\">\n              {t(\"community_hub.publish.agent_flow.name_description\")}\n            </div>\n            <input\n              type=\"text\"\n              name=\"name\"\n              required\n              minLength={3}\n              maxLength={300}\n              defaultValue={entity.name}\n              placeholder={t(\n                \"community_hub.publish.agent_flow.name_placeholder\"\n              )}\n              className=\"border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-theme-text-primary text-sm focus:outline-primary-button active:outline-primary-button outline-none placeholder:text-theme-text-placeholder\"\n            />\n          </div>\n\n          <div>\n            <label className=\"block text-sm font-semibold text-theme-text-primary mb-1\">\n              {t(\"community_hub.publish.agent_flow.description_label\")}\n            </label>\n            <div className=\"text-xs text-white/60 mb-2\">\n              {t(\"community_hub.publish.agent_flow.description_description\")}\n            </div>\n            <textarea\n              name=\"description\"\n              required\n              minLength={10}\n              maxLength={1000}\n              defaultValue={entity.description}\n              placeholder={t(\n                \"community_hub.publish.agent_flow.description_description\"\n              )}\n              className=\"border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-white text-sm focus:outline-primary-button active:outline-primary-button outline-none min-h-[80px] placeholder:text-theme-text-placeholder\"\n            />\n          </div>\n          <div>\n            <label className=\"block text-sm font-semibold text-white mb-1\">\n              {t(\"community_hub.publish.agent_flow.tags_label\")}\n            </label>\n            <div className=\"text-xs text-white/60 mb-2\">\n              {t(\"community_hub.publish.agent_flow.tags_description\")}\n            </div>\n            <div className=\"flex flex-wrap gap-2 p-2 bg-theme-bg-secondary rounded-lg min-h-[42px]\">\n              {tags.map((tag, index) => (\n                <span\n                  key={index}\n                  className=\"flex items-center gap-1 px-2 py-1 text-sm text-theme-text-primary bg-white/10 light:bg-black/10 rounded-md\"\n                >\n                  {tag}\n                  <button\n                    type=\"button\"\n                    onClick={() => removeTag(tag)}\n                    className=\"border-none text-theme-text-primary hover:text-theme-text-secondary cursor-pointer\"\n                  >\n                    <X size={14} />\n                  </button>\n                </span>\n              ))}\n              <input\n                type=\"text\"\n                value={tagInput}\n                onChange={(e) => setTagInput(e.target.value)}\n                onKeyDown={handleKeyDown}\n                placeholder={t(\n                  \"community_hub.publish.agent_flow.tags_placeholder\"\n                )}\n                className=\"flex-1 min-w-[200px] border-none text-sm bg-transparent text-theme-text-primary placeholder:text-theme-text-placeholder p-0 h-[24px] focus:outline-none\"\n              />\n            </div>\n          </div>\n          <div>\n            <label className=\"block text-sm font-semibold text-white\">\n              {t(\"community_hub.publish.agent_flow.visibility_label\")}\n            </label>\n            <span className=\"text-xs text-theme-text-secondary\">\n              {t(\"community_hub.publish.agent_flow.privacy_note\")}\n            </span>\n          </div>\n        </div>\n        <div className=\"w-1/2 p-6 pt-0 flex flex-col gap-y-4\">\n          <div>\n            <label className=\"block text-sm font-semibold text-theme-text-primary mb-1\">\n              Flow Steps\n            </label>\n            <div className=\"text-xs text-white/60\">\n              The steps the agent will follow when the flow is triggered.\n            </div>\n          </div>\n          <div className=\"flex flex-col gap-y-0.5\">\n            {entity.steps && entity.steps.length > 0 ? (\n              entity.steps.map((step, idx) => {\n                const info = BLOCK_INFO[step.type];\n                const isExpanded = expandedStep === idx;\n                const summary = info?.getSummary\n                  ? info.getSummary(step.config)\n                  : \"\";\n                return (\n                  <div key={idx} className=\"flex flex-col items-center w-full\">\n                    <div\n                      className=\"flex flex-col bg-theme-bg-secondary rounded-lg px-3 py-2 w-full cursor-pointer group\"\n                      onClick={() => setExpandedStep(isExpanded ? null : idx)}\n                    >\n                      <div className=\"flex items-center gap-x-3 w-full\">\n                        <span>{info?.icon}</span>\n                        <span className=\"text-theme-text-primary text-sm font-medium flex-1\">\n                          {info?.label || step.type}\n                        </span>\n                        {!isExpanded && (\n                          <span className=\"text-theme-text-secondary text-xs ml-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-[120px] min-w-0\">\n                            {summary}\n                          </span>\n                        )}\n                        <span\n                          className={`ml-2 text-theme-text-secondary transition-transform duration-200 ${isExpanded ? \"rotate-90\" : \"\"}`}\n                        >\n                          <CaretRight size={16} />\n                        </span>\n                      </div>\n                      {isExpanded && summary && (\n                        <div className=\"w-full text-theme-text-secondary text-xs mt-1 whitespace-pre-line break-words\">\n                          {summary}\n                        </div>\n                      )}\n                    </div>\n                    {idx < entity.steps.length - 1 && (\n                      <span className=\"text-theme-text-secondary text-lg my-1\">\n                        ↓\n                      </span>\n                    )}\n                  </div>\n                );\n              })\n            ) : (\n              <div className=\"text-theme-text-secondary text-xs\">\n                No steps defined.\n              </div>\n            )}\n          </div>\n          <button\n            type=\"submit\"\n            disabled={isSubmitting}\n            className=\"border-none mt-4 w-full bg-cta-button hover:opacity-80 text-theme-text-primary font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n          >\n            {isSubmitting\n              ? t(\"community_hub.publish.agent_flow.submitting\")\n              : t(\"community_hub.publish.agent_flow.submit\")}\n          </button>\n        </div>\n      </form>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/CommunityHub/PublishEntityModal/SlashCommands/index.jsx",
    "content": "import { useState, useRef } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport CommunityHub from \"@/models/communityHub\";\nimport showToast from \"@/utils/toast\";\nimport paths from \"@/utils/paths\";\nimport { X } from \"@phosphor-icons/react\";\nimport { Link } from \"react-router-dom\";\n\nexport default function SlashCommands({ entity }) {\n  const { t } = useTranslation();\n  const formRef = useRef(null);\n  const [isSubmitting, setIsSubmitting] = useState(false);\n  const [tags, setTags] = useState([]);\n  const [tagInput, setTagInput] = useState(\"\");\n  const [visibility, setVisibility] = useState(\"public\");\n  const [isSuccess, setIsSuccess] = useState(false);\n  const [itemId, setItemId] = useState(null);\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setIsSubmitting(true);\n    try {\n      const form = new FormData(formRef.current);\n      const data = {\n        name: form.get(\"name\"),\n        description: form.get(\"description\"),\n        command: entity.command,\n        prompt: form.get(\"prompt\"),\n        tags: tags,\n        visibility: visibility,\n      };\n\n      const { success, error, itemId } =\n        await CommunityHub.createSlashCommand(data);\n      if (!success) throw new Error(error);\n      setItemId(itemId);\n      setIsSuccess(true);\n    } catch (error) {\n      console.error(\"Failed to publish slash command:\", error);\n      showToast(`Failed to publish slash command: ${error.message}`, \"error\", {\n        clear: true,\n      });\n    } finally {\n      setIsSubmitting(false);\n    }\n  };\n\n  const handleKeyDown = (e) => {\n    if (e.key === \"Enter\" || e.key === \",\") {\n      e.preventDefault();\n      const value = tagInput.trim();\n      if (value.length > 20) return;\n      if (value && !tags.includes(value)) {\n        setTags((prevTags) => [...prevTags, value].slice(0, 5)); // Limit to 5 tags\n        setTagInput(\"\");\n      }\n    }\n  };\n\n  const removeTag = (tagToRemove) => {\n    setTags(tags.filter((tag) => tag !== tagToRemove));\n  };\n\n  if (isSuccess) {\n    return (\n      <div className=\"p-6 -mt-12 w-[400px]\">\n        <div className=\"flex flex-col items-center justify-center gap-y-2\">\n          <h3 className=\"text-lg font-semibold text-theme-text-primary\">\n            {t(\"community_hub.publish.slash_command.success_title\")}\n          </h3>\n          <p className=\"text-lg text-theme-text-primary text-center max-w-2xl\">\n            {t(\"community_hub.publish.slash_command.success_description\")}\n          </p>\n          <p className=\"text-theme-text-secondary text-center text-sm\">\n            {t(\"community_hub.publish.slash_command.success_thank_you\")}\n          </p>\n          <Link\n            to={paths.communityHub.viewItem(\"slash-command\", itemId)}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"w-[265px] bg-theme-bg-secondary hover:bg-theme-sidebar-item-hover text-theme-text-primary py-2 px-4 rounded-lg transition-colors mt-4 text-sm font-semibold text-center\"\n          >\n            {t(\"community_hub.publish.slash_command.view_on_hub\")}\n          </Link>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <>\n      <div className=\"w-full flex gap-x-2 items-center mb-3 -mt-8\">\n        <h3 className=\"text-xl font-semibold text-theme-text-primary px-6 py-3 flex items-center gap-x-2\">\n          {t(\"community_hub.publish.slash_command.modal_title\")}\n          <code className=\"bg-theme-bg-secondary rounded-lg px-1 text-theme-text-secondary text-lg font-mono\">\n            {entity.command}\n          </code>\n        </h3>\n      </div>\n      <form ref={formRef} className=\"flex\" onSubmit={handleSubmit}>\n        <div className=\"w-1/2 p-6 pt-0 space-y-4\">\n          <div>\n            <label className=\"block text-sm font-semibold text-theme-text-primary mb-1\">\n              {t(\"community_hub.publish.slash_command.name_label\")}\n            </label>\n            <div className=\"text-xs text-theme-text-secondary mb-2\">\n              {t(\"community_hub.publish.slash_command.name_description\")}\n            </div>\n            <input\n              type=\"text\"\n              name=\"name\"\n              required\n              minLength={3}\n              maxLength={300}\n              defaultValue={entity.name}\n              placeholder={t(\n                \"community_hub.publish.slash_command.name_placeholder\"\n              )}\n              className=\"border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-theme-text-primary text-sm focus:outline-primary-button active:outline-primary-button outline-none placeholder:text-theme-text-placeholder\"\n            />\n          </div>\n\n          <div>\n            <label className=\"block text-sm font-semibold text-theme-text-primary mb-1\">\n              {t(\"community_hub.publish.slash_command.description_label\")}\n            </label>\n            <div className=\"text-xs text-white/60 mb-2\">\n              {t(\"community_hub.publish.slash_command.description_description\")}\n            </div>\n            <textarea\n              name=\"description\"\n              required\n              minLength={10}\n              maxLength={1000}\n              defaultValue={entity.description}\n              placeholder={t(\n                \"community_hub.publish.slash_command.description_description\"\n              )}\n              className=\"border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-white text-sm focus:outline-primary-button active:outline-primary-button outline-none min-h-[80px] placeholder:text-theme-text-placeholder\"\n            />\n          </div>\n          <div>\n            <label className=\"block text-sm font-semibold text-white mb-1\">\n              {t(\"community_hub.publish.slash_command.tags_label\")}\n            </label>\n            <div className=\"text-xs text-white/60 mb-2\">\n              {t(\"community_hub.publish.slash_command.tags_description\")}\n            </div>\n            <div className=\"flex flex-wrap gap-2 p-2 bg-theme-bg-secondary rounded-lg min-h-[42px]\">\n              {tags.map((tag, index) => (\n                <span\n                  key={index}\n                  className=\"flex items-center gap-1 px-2 py-1 text-sm text-theme-text-primary bg-white/10 light:bg-black/10 rounded-md\"\n                >\n                  {tag}\n                  <button\n                    type=\"button\"\n                    onClick={() => removeTag(tag)}\n                    className=\"border-none text-theme-text-primary hover:text-theme-text-secondary cursor-pointer\"\n                  >\n                    <X size={14} />\n                  </button>\n                </span>\n              ))}\n              <input\n                type=\"text\"\n                value={tagInput}\n                onChange={(e) => setTagInput(e.target.value)}\n                onKeyDown={handleKeyDown}\n                placeholder={t(\n                  \"community_hub.publish.slash_command.tags_placeholder\"\n                )}\n                className=\"flex-1 min-w-[200px] border-none text-sm bg-transparent text-theme-text-primary placeholder:text-theme-text-placeholder p-0 h-[24px] focus:outline-none\"\n              />\n            </div>\n          </div>\n\n          <div>\n            <label className=\"block text-sm font-semibold text-white mb-1\">\n              {t(\"community_hub.publish.slash_command.visibility_label\")}\n            </label>\n            <div className=\"text-xs text-white/60 mb-2\">\n              {visibility === \"public\"\n                ? t(\"community_hub.publish.slash_command.public_description\")\n                : t(\"community_hub.publish.slash_command.private_description\")}\n            </div>\n            <div className=\"w-fit h-[42px] bg-theme-bg-secondary rounded-lg p-0.5\">\n              <div className=\"flex items-center\" role=\"group\">\n                <input\n                  type=\"radio\"\n                  id=\"public\"\n                  name=\"visibility\"\n                  value=\"public\"\n                  className=\"peer/public hidden\"\n                  defaultChecked\n                  onChange={(e) => setVisibility(e.target.value)}\n                />\n                <input\n                  type=\"radio\"\n                  id=\"private\"\n                  name=\"visibility\"\n                  value=\"private\"\n                  className=\"peer/private hidden\"\n                  onChange={(e) => setVisibility(e.target.value)}\n                />\n                <label\n                  htmlFor=\"public\"\n                  className=\"h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-theme-text-primary hover:text-theme-text-secondary peer-checked/public:bg-theme-sidebar-item-hover peer-checked/public:text-theme-primary-button flex items-center justify-center\"\n                >\n                  Public\n                </label>\n                <label\n                  htmlFor=\"private\"\n                  className=\"h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-theme-text-primary hover:text-theme-text-secondary peer-checked/private:bg-theme-sidebar-item-hover peer-checked/private:text-theme-primary-button flex items-center justify-center\"\n                >\n                  Private\n                </label>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"w-1/2 p-6 pt-0 space-y-4\">\n          <div>\n            <label className=\"block text-sm font-semibold text-white mb-1\">\n              {t(\"community_hub.publish.slash_command.prompt_label\")}\n            </label>\n            <div className=\"text-xs text-white/60 mb-2\">\n              {t(\"community_hub.publish.slash_command.prompt_description\")}\n            </div>\n            <textarea\n              name=\"prompt\"\n              required\n              minLength={10}\n              defaultValue={entity.prompt}\n              placeholder={t(\n                \"community_hub.publish.slash_command.prompt_placeholder\"\n              )}\n              className=\"border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-white text-sm focus:outline-primary-button active:outline-primary-button outline-none min-h-[300px] placeholder:text-theme-text-placeholder\"\n            />\n          </div>\n\n          <button\n            type=\"submit\"\n            disabled={isSubmitting}\n            className=\"border-none w-full bg-cta-button hover:opacity-80 text-theme-text-primary font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n          >\n            {isSubmitting\n              ? t(\"community_hub.publish.slash_command.submitting\")\n              : t(\"community_hub.publish.slash_command.publish_button\")}\n          </button>\n        </div>\n      </form>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/CommunityHub/PublishEntityModal/SystemPrompts/index.jsx",
    "content": "import { useState, useRef } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport CommunityHub from \"@/models/communityHub\";\nimport showToast from \"@/utils/toast\";\nimport paths from \"@/utils/paths\";\nimport { X } from \"@phosphor-icons/react\";\nimport { Link } from \"react-router-dom\";\n\nexport default function SystemPrompts({ entity }) {\n  const { t } = useTranslation();\n  const formRef = useRef(null);\n  const [isSubmitting, setIsSubmitting] = useState(false);\n  const [tags, setTags] = useState([]);\n  const [tagInput, setTagInput] = useState(\"\");\n  const [visibility, setVisibility] = useState(\"public\");\n  const [isSuccess, setIsSuccess] = useState(false);\n  const [itemId, setItemId] = useState(null);\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setIsSubmitting(true);\n    try {\n      const form = new FormData(formRef.current);\n      const data = {\n        name: form.get(\"name\"),\n        description: form.get(\"description\"),\n        prompt: form.get(\"prompt\"),\n        tags: tags,\n        visibility: visibility,\n      };\n\n      const { success, error, itemId } =\n        await CommunityHub.createSystemPrompt(data);\n      if (!success) throw new Error(error);\n      setItemId(itemId);\n      setIsSuccess(true);\n    } catch (error) {\n      console.error(\"Failed to publish prompt:\", error);\n      showToast(`Failed to publish prompt: ${error.message}`, \"error\", {\n        clear: true,\n      });\n    } finally {\n      setIsSubmitting(false);\n    }\n  };\n\n  const handleKeyDown = (e) => {\n    if (e.key === \"Enter\" || e.key === \",\") {\n      e.preventDefault();\n      const value = tagInput.trim();\n      if (value.length > 20) return;\n      if (value && !tags.includes(value)) {\n        setTags((prevTags) => [...prevTags, value].slice(0, 5)); // Limit to 5 tags\n        setTagInput(\"\");\n      }\n    }\n  };\n\n  const removeTag = (tagToRemove) => {\n    setTags(tags.filter((tag) => tag !== tagToRemove));\n  };\n\n  if (isSuccess) {\n    return (\n      <div className=\"p-6 -mt-12 w-[400px]\">\n        <div className=\"flex flex-col items-center justify-center gap-y-2\">\n          <h3 className=\"text-lg font-semibold text-theme-text-primary\">\n            {t(\"community_hub.publish.system_prompt.success_title\")}\n          </h3>\n          <p className=\"text-lg text-theme-text-primary text-center max-w-2xl\">\n            {t(\"community_hub.publish.system_prompt.success_description\")}\n          </p>\n          <p className=\"text-theme-text-secondary text-center text-sm\">\n            {t(\"community_hub.publish.system_prompt.success_thank_you\")}\n          </p>\n          <Link\n            to={paths.communityHub.viewItem(\"system-prompt\", itemId)}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"w-[265px] bg-theme-bg-secondary hover:bg-theme-sidebar-item-hover text-theme-text-primary py-2 px-4 rounded-lg transition-colors mt-4 text-sm font-semibold text-center\"\n          >\n            {t(\"community_hub.publish.system_prompt.view_on_hub\")}\n          </Link>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <>\n      <div className=\"w-full flex gap-x-2 items-center mb-3 -mt-8\">\n        <h3 className=\"text-xl font-semibold text-theme-text-primary px-6 py-3\">\n          {t(`community_hub.publish.system_prompt.modal_title`)}\n        </h3>\n      </div>\n      <form ref={formRef} className=\"flex\" onSubmit={handleSubmit}>\n        <div className=\"w-1/2 p-6 pt-0 space-y-4\">\n          <div>\n            <label className=\"block text-sm font-semibold text-theme-text-primary mb-1\">\n              {t(\"community_hub.publish.system_prompt.name_label\")}\n            </label>\n            <div className=\"text-xs text-theme-text-secondary mb-2\">\n              {t(\"community_hub.publish.system_prompt.name_description\")}\n            </div>\n            <input\n              type=\"text\"\n              name=\"name\"\n              required\n              minLength={3}\n              maxLength={300}\n              placeholder={t(\n                \"community_hub.publish.system_prompt.name_placeholder\"\n              )}\n              className=\"border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-theme-text-primary text-sm focus:outline-primary-button active:outline-primary-button outline-none placeholder:text-theme-text-placeholder\"\n            />\n          </div>\n\n          <div>\n            <label className=\"block text-sm font-semibold text-theme-text-primary mb-1\">\n              {t(\"community_hub.publish.system_prompt.description_label\")}\n            </label>\n            <div className=\"text-xs text-white/60 mb-2\">\n              {t(\"community_hub.publish.system_prompt.description_description\")}\n            </div>\n            <textarea\n              name=\"description\"\n              required\n              minLength={10}\n              maxLength={1000}\n              placeholder={t(\n                \"community_hub.publish.system_prompt.description_description\"\n              )}\n              className=\"border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-white text-sm focus:outline-primary-button active:outline-primary-button outline-none min-h-[80px] placeholder:text-theme-text-placeholder\"\n            />\n          </div>\n          <div>\n            <label className=\"block text-sm font-semibold text-white mb-1\">\n              {t(\"community_hub.publish.system_prompt.tags_label\")}\n            </label>\n            <div className=\"text-xs text-white/60 mb-2\">\n              {t(\"community_hub.publish.system_prompt.tags_description\")}\n            </div>\n            <div className=\"flex flex-wrap gap-2 p-2 bg-theme-bg-secondary rounded-lg min-h-[42px]\">\n              {tags.map((tag, index) => (\n                <span\n                  key={index}\n                  className=\"flex items-center gap-1 px-2 py-1 text-sm text-theme-text-primary bg-white/10 light:bg-black/10 rounded-md\"\n                >\n                  {tag}\n                  <button\n                    type=\"button\"\n                    onClick={() => removeTag(tag)}\n                    className=\"border-none text-theme-text-primary hover:text-theme-text-secondary cursor-pointer\"\n                  >\n                    <X size={14} />\n                  </button>\n                </span>\n              ))}\n              <input\n                type=\"text\"\n                value={tagInput}\n                onChange={(e) => setTagInput(e.target.value)}\n                onKeyDown={handleKeyDown}\n                placeholder={t(\n                  \"community_hub.publish.system_prompt.tags_placeholder\"\n                )}\n                className=\"flex-1 min-w-[200px] border-none text-sm bg-transparent text-theme-text-primary placeholder:text-theme-text-placeholder p-0 h-[24px] focus:outline-none\"\n              />\n            </div>\n          </div>\n\n          <div>\n            <label className=\"block text-sm font-semibold text-white mb-1\">\n              {t(\"community_hub.publish.system_prompt.visibility_label\")}\n            </label>\n            <div className=\"text-xs text-white/60 mb-2\">\n              {visibility === \"public\"\n                ? t(\"community_hub.publish.system_prompt.public_description\")\n                : t(\"community_hub.publish.system_prompt.private_description\")}\n            </div>\n            <div className=\"w-fit h-[42px] bg-theme-bg-secondary rounded-lg p-0.5\">\n              <div className=\"flex items-center\" role=\"group\">\n                <input\n                  type=\"radio\"\n                  id=\"public\"\n                  name=\"visibility\"\n                  value=\"public\"\n                  className=\"peer/public hidden\"\n                  defaultChecked\n                  onChange={(e) => setVisibility(e.target.value)}\n                />\n                <input\n                  type=\"radio\"\n                  id=\"private\"\n                  name=\"visibility\"\n                  value=\"private\"\n                  className=\"peer/private hidden\"\n                  onChange={(e) => setVisibility(e.target.value)}\n                />\n                <label\n                  htmlFor=\"public\"\n                  className=\"h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-theme-text-primary hover:text-theme-text-secondary peer-checked/public:bg-theme-sidebar-item-hover peer-checked/public:text-theme-primary-button flex items-center justify-center\"\n                >\n                  Public\n                </label>\n                <label\n                  htmlFor=\"private\"\n                  className=\"h-[36px] px-4 rounded-lg text-sm font-medium transition-all duration-200 cursor-pointer text-theme-text-primary hover:text-theme-text-secondary peer-checked/private:bg-theme-sidebar-item-hover peer-checked/private:text-theme-primary-button flex items-center justify-center\"\n                >\n                  Private\n                </label>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <div className=\"w-1/2 p-6 pt-0 space-y-4\">\n          <div>\n            <label className=\"block text-sm font-semibold text-white mb-1\">\n              {t(\"community_hub.publish.system_prompt.prompt_label\")}\n            </label>\n            <div className=\"text-xs text-white/60 mb-2\">\n              {t(\"community_hub.publish.system_prompt.prompt_description\")}\n            </div>\n            <textarea\n              name=\"prompt\"\n              required\n              minLength={10}\n              defaultValue={entity}\n              placeholder={t(\n                \"community_hub.publish.system_prompt.prompt_placeholder\"\n              )}\n              className=\"border-none w-full bg-theme-bg-secondary rounded-lg p-2 text-white text-sm focus:outline-primary-button active:outline-primary-button outline-none min-h-[300px] placeholder:text-theme-text-placeholder\"\n            />\n          </div>\n\n          <button\n            type=\"submit\"\n            disabled={isSubmitting}\n            className=\"border-none w-full bg-cta-button hover:opacity-80 text-theme-text-primary font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n          >\n            {isSubmitting\n              ? t(\"community_hub.publish.system_prompt.submitting\")\n              : t(\"community_hub.publish.system_prompt.publish_button\")}\n          </button>\n        </div>\n      </form>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/CommunityHub/PublishEntityModal/index.jsx",
    "content": "import { X } from \"@phosphor-icons/react\";\nimport { useCommunityHubAuth } from \"@/hooks/useCommunityHubAuth\";\nimport UnauthenticatedHubModal from \"@/components/CommunityHub/UnauthenticatedHubModal\";\nimport SystemPrompts from \"./SystemPrompts\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport AgentFlows from \"./AgentFlows\";\nimport SlashCommands from \"./SlashCommands\";\n\nexport default function PublishEntityModal({\n  show,\n  onClose,\n  entityType,\n  entity,\n}) {\n  const { isAuthenticated, loading } = useCommunityHubAuth();\n  if (!show || loading) return null;\n  if (!isAuthenticated)\n    return <UnauthenticatedHubModal show={show} onClose={onClose} />;\n\n  const renderEntityForm = () => {\n    switch (entityType) {\n      case \"system-prompt\":\n        return <SystemPrompts entity={entity} />;\n      case \"agent-flow\":\n        return <AgentFlows entity={entity} />;\n      case \"slash-command\":\n        return <SlashCommands entity={entity} />;\n      default:\n        return null;\n    }\n  };\n\n  return (\n    <ModalWrapper isOpen={show}>\n      <div className=\"relative max-w-[900px] bg-theme-bg-primary rounded-lg shadow border border-theme-modal-border\">\n        <div className=\"relative p-6\">\n          <button\n            onClick={onClose}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={18} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        {renderEntityForm()}\n      </div>\n    </ModalWrapper>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/CommunityHub/UnauthenticatedHubModal/index.jsx",
    "content": "import { X } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\nimport paths from \"@/utils/paths\";\nimport { Link } from \"react-router-dom\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\n\nexport default function UnauthenticatedHubModal({ show, onClose }) {\n  const { t } = useTranslation();\n  if (!show) return null;\n\n  return (\n    <ModalWrapper isOpen={show}>\n      <div className=\"relative w-[400px] max-w-full bg-theme-bg-primary rounded-lg shadow border border-theme-modal-border\">\n        <div className=\"p-6\">\n          <button\n            onClick={onClose}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={18} weight=\"bold\" className=\"text-white\" />\n          </button>\n          <div className=\"flex flex-col items-center justify-center gap-y-4\">\n            <h3 className=\"text-lg font-semibold text-white\">\n              {t(\"community_hub.publish.generic.unauthenticated.title\")}\n            </h3>\n            <p className=\"text-lg text-white text-center max-w-[300px]\">\n              {t(\"community_hub.publish.generic.unauthenticated.description\")}\n            </p>\n            <Link\n              to={paths.communityHub.authentication()}\n              className=\"w-[265px] bg-theme-bg-secondary hover:bg-theme-sidebar-item-hover text-theme-text-primary py-2 px-4 rounded-lg transition-colors mt-4 text-sm font-semibold text-center\"\n            >\n              {t(\"community_hub.publish.generic.unauthenticated.button\")}\n            </Link>\n          </div>\n        </div>\n      </div>\n    </ModalWrapper>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/ContextualSaveBar/index.jsx",
    "content": "import { Warning } from \"@phosphor-icons/react\";\n\nexport default function ContextualSaveBar({\n  showing = false,\n  onSave,\n  onCancel,\n}) {\n  if (!showing) return null;\n\n  return (\n    <div className=\"fixed top-0 left-0 right-0 h-14 bg-dark-input flex items-center justify-end px-4 z-[999]\">\n      <div className=\"absolute ml-4 left-0 md:left-1/2 transform md:-translate-x-1/2 flex items-center gap-x-2\">\n        <Warning size={18} className=\"text-[#FFFFFF]\" />\n        <p className=\"text-[#FFFFFF] font-medium text-xs\">Unsaved Changes</p>\n      </div>\n      <div className=\"flex items-center gap-x-2\">\n        <button\n          className=\"border-none text-theme-text-primary font-medium text-sm px-[10px] py-[6px] rounded-md bg-theme-bg-secondary hover:bg-theme-bg-primary\"\n          onClick={onCancel}\n        >\n          Cancel\n        </button>\n        <button\n          className=\"border-none text-theme-text-primary font-medium text-sm px-[10px] py-[6px] rounded-md bg-primary-button hover:bg-primary-button-hover\"\n          onClick={onSave}\n        >\n          Save\n        </button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/DataConnectorOption/media/index.js",
    "content": "import GitHub from \"./github.svg\";\nimport GitLab from \"./gitlab.svg\";\nimport YouTube from \"./youtube.svg\";\nimport Link from \"./link.svg\";\nimport Confluence from \"./confluence.jpeg\";\nimport DrupalWiki from \"./drupalwiki.jpg\";\nimport Obsidian from \"./obsidian.png\";\nimport PaperlessNgx from \"./paperless-ngx.jpeg\";\n\nconst ConnectorImages = {\n  github: GitHub,\n  gitlab: GitLab,\n  youtube: YouTube,\n  websiteDepth: Link,\n  confluence: Confluence,\n  drupalwiki: DrupalWiki,\n  obsidian: Obsidian,\n  paperlessNgx: PaperlessNgx,\n};\n\nexport default ConnectorImages;\n"
  },
  {
    "path": "frontend/src/components/DefaultChat/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport paths from \"@/utils/paths\";\nimport { isMobile } from \"react-device-detect\";\nimport useUser from \"@/hooks/useUser\";\nimport Appearance from \"@/models/appearance\";\nimport useLogo from \"@/hooks/useLogo\";\nimport Workspace from \"@/models/workspace\";\nimport { NavLink } from \"react-router-dom\";\nimport { LAST_VISITED_WORKSPACE } from \"@/utils/constants\";\nimport { useTranslation } from \"react-i18next\";\nimport { safeJsonParse } from \"@/utils/request\";\n\nexport default function DefaultChatContainer() {\n  const { t } = useTranslation();\n  const { user } = useUser();\n  const { logo } = useLogo();\n  const [lastVisitedWorkspace, setLastVisitedWorkspace] = useState(null);\n  const [{ workspaces, loading }, setWorkspaces] = useState({\n    workspaces: [],\n    loading: true,\n  });\n\n  useEffect(() => {\n    async function fetchWorkspaces() {\n      const availableWorkspaces = await Workspace.all();\n      const serializedLastVisitedWorkspace = localStorage.getItem(\n        LAST_VISITED_WORKSPACE\n      );\n      if (!serializedLastVisitedWorkspace)\n        return setWorkspaces({\n          workspaces: availableWorkspaces,\n          loading: false,\n        });\n\n      try {\n        const lastVisitedWorkspace = safeJsonParse(\n          serializedLastVisitedWorkspace,\n          null\n        );\n        if (lastVisitedWorkspace == null) throw new Error(\"Non-parseable!\");\n        const isValid = availableWorkspaces.some(\n          (ws) => ws.slug === lastVisitedWorkspace?.slug\n        );\n        if (!isValid) throw new Error(\"Invalid value!\");\n        setLastVisitedWorkspace(lastVisitedWorkspace);\n      } catch {\n        localStorage.removeItem(LAST_VISITED_WORKSPACE);\n      } finally {\n        setWorkspaces({ workspaces: availableWorkspaces, loading: false });\n      }\n    }\n    fetchWorkspaces();\n  }, []);\n\n  if (loading) {\n    return (\n      <Layout>\n        <div className=\"w-full h-full flex flex-col items-center justify-center overflow-y-auto no-scroll\">\n          {/* Logo skeleton */}\n          <div className=\"w-[140px] h-[140px] mb-5 rounded-lg bg-theme-bg-primary animate-pulse\" />\n          {/* Title skeleton */}\n          <div className=\"w-48 h-6 mb-4 rounded bg-theme-bg-primary animate-pulse\" />\n          {/* Paragraph skeleton */}\n          <div className=\"w-80 h-4 mb-2 rounded bg-theme-bg-primary animate-pulse\" />\n          <div className=\"w-64 h-4 rounded bg-theme-bg-primary animate-pulse\" />\n          {/* Button skeleton */}\n          <div className=\"mt-[29px] w-40 h-[34px] rounded-lg bg-theme-bg-primary animate-pulse\" />\n        </div>\n      </Layout>\n    );\n  }\n\n  const hasWorkspaces = workspaces.length > 0;\n  return (\n    <Layout>\n      <div className=\"w-full h-full flex flex-col items-center justify-center overflow-y-auto no-scroll\">\n        <img\n          src={logo}\n          alt=\"Custom Logo\"\n          className=\" w-[200px] h-fit mb-5 rounded-lg\"\n        />\n        <h1 className=\"text-white text-2xl font-semibold\">\n          {t(\"home.welcome\")}, {user.username}!\n        </h1>\n        <p className=\"text-theme-home-text-secondary text-base text-center whitespace-pre-line\">\n          {hasWorkspaces ? t(\"home.chooseWorkspace\") : t(\"home.notAssigned\")}\n        </p>\n        {hasWorkspaces && (\n          <NavLink\n            to={paths.workspace.chat(\n              lastVisitedWorkspace?.slug || workspaces[0].slug\n            )}\n            className=\"text-sm font-medium mt-[10px] w-fit px-4 h-[34px] flex items-center justify-center rounded-lg cursor-pointer bg-theme-home-button-secondary hover:bg-theme-home-button-secondary-hover text-theme-home-button-secondary-text hover:text-theme-home-button-secondary-hover-text transition-all duration-200\"\n          >\n            {t(\"home.goToWorkspace\", {\n              workspace: lastVisitedWorkspace?.name || workspaces[0].name,\n            })}{\" \"}\n            &rarr;\n          </NavLink>\n        )}\n      </div>\n    </Layout>\n  );\n}\n\nconst Layout = ({ children }) => {\n  const { showScrollbar } = Appearance.getSettings();\n  return (\n    <div\n      style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n      className={`relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary light:border-[1px] light:border-theme-sidebar-border w-full h-full overflow-y-scroll ${showScrollbar ? \"show-scrollbar\" : \"no-scroll\"}`}\n    >\n      {children}\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/AzureAiOptions/index.jsx",
    "content": "export default function AzureAiOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-4\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Azure Service Endpoint\n          </label>\n          <input\n            type=\"url\"\n            name=\"AzureOpenAiEndpoint\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"https://my-azure.openai.azure.com\"\n            defaultValue={settings?.AzureOpenAiEndpoint}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"AzureOpenAiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Azure OpenAI API Key\"\n            defaultValue={settings?.AzureOpenAiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Embedding Deployment Name\n          </label>\n          <input\n            type=\"text\"\n            name=\"AzureOpenAiEmbeddingModelPref\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Azure OpenAI embedding model deployment name\"\n            defaultValue={settings?.AzureOpenAiEmbeddingModelPref}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/CohereOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function CohereEmbeddingOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.CohereApiKey);\n  const [cohereApiKey, setCohereApiKey] = useState(settings?.CohereApiKey);\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-4\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"CohereApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Cohere API Key\"\n            defaultValue={settings?.CohereApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={(e) => setInputValue(e.target.value)}\n            onBlur={() => setCohereApiKey(inputValue)}\n          />\n        </div>\n        <CohereModelSelection settings={settings} apiKey={cohereApiKey} />\n      </div>\n    </div>\n  );\n}\n\nfunction CohereModelSelection({ apiKey, settings }) {\n  const [models, setModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!apiKey) {\n        setModels([]);\n        setLoading(true);\n        return;\n      }\n\n      setLoading(true);\n      const { models } = await System.customModels(\n        \"cohere-embedder\",\n        typeof apiKey === \"boolean\" ? null : apiKey\n      );\n      setModels(models || []);\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Model Preference\n        </label>\n        <select\n          name=\"EmbeddingModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Model Preference\n      </label>\n      <select\n        name=\"EmbeddingModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {models.map((model) => (\n          <option\n            key={model.id}\n            value={model.id}\n            selected={settings?.EmbeddingModelPref === model.id}\n          >\n            {model.name}\n          </option>\n        ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/EmbedderItem/index.jsx",
    "content": "export default function EmbedderItem({\n  name,\n  value,\n  image,\n  description,\n  checked,\n  onClick,\n}) {\n  return (\n    <div\n      onClick={() => onClick(value)}\n      className={`w-full p-2 rounded-md hover:cursor-pointer hover:bg-theme-bg-secondary ${\n        checked ? \"bg-theme-bg-secondary\" : \"\"\n      }`}\n    >\n      <input\n        type=\"checkbox\"\n        value={value}\n        className=\"peer hidden\"\n        checked={checked}\n        readOnly={true}\n        formNoValidate={true}\n      />\n      <div className=\"flex gap-x-4 items-center\">\n        <img\n          src={image}\n          alt={`${name} logo`}\n          className=\"w-10 h-10 rounded-md\"\n        />\n        <div className=\"flex flex-col\">\n          <div className=\"text-sm font-semibold text-white\">{name}</div>\n          <div className=\"mt-1 text-xs text-description\">{description}</div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/GeminiOptions/index.jsx",
    "content": "import { Info } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\n\nconst DEFAULT_MODELS = [\n  {\n    id: \"gemini-embedding-001\",\n    name: \"Gemini Embedding 001\",\n  },\n];\n\nexport default function GeminiOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-6\">\n      <div className=\"w-full flex flex-col gap-y-4\">\n        <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n          <div className=\"flex flex-col w-60\">\n            <label className=\"text-white text-sm font-semibold block mb-3\">\n              API Key\n            </label>\n            <input\n              type=\"password\"\n              name=\"GeminiEmbeddingApiKey\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"Gemini API Key\"\n              defaultValue={\n                settings?.GeminiEmbeddingApiKey ? \"*\".repeat(20) : \"\"\n              }\n              required={true}\n              autoComplete=\"off\"\n              spellCheck={false}\n            />\n          </div>\n          <div className=\"flex flex-col w-60\">\n            <label className=\"text-white text-sm font-semibold block mb-3\">\n              Model Preference\n            </label>\n            <select\n              name=\"EmbeddingModelPref\"\n              required={true}\n              className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n            >\n              <optgroup label=\"Available embedding models\">\n                {DEFAULT_MODELS.map((model) => {\n                  return (\n                    <option\n                      key={model.id}\n                      value={model.id}\n                      selected={settings?.EmbeddingModelPref === model.id}\n                    >\n                      {model.name}\n                    </option>\n                  );\n                })}\n              </optgroup>\n            </select>\n          </div>\n        </div>\n      </div>\n      <div className=\"flex flex-col w-60\">\n        <div\n          data-tooltip-id=\"embedding-output-dimensions-tooltip\"\n          className=\"flex gap-x-1 items-center mb-3\"\n        >\n          <label className=\"text-white text-sm font-semibold block\">\n            Output dimensions\n          </label>\n          <Info\n            size={16}\n            className=\"text-theme-text-secondary cursor-pointer\"\n          />\n          <Tooltip\n            id=\"embedding-output-dimensions-tooltip\"\n            place=\"top\"\n            delayShow={300}\n            className=\"tooltip !text-xs !opacity-100\"\n            style={{\n              maxWidth: \"250px\",\n              whiteSpace: \"normal\",\n              wordWrap: \"break-word\",\n            }}\n          >\n            The number of dimensions the resulting output embeddings should have\n            if it supports multiple dimensions output.\n            <br />\n            <br /> Leave blank to use the default dimensions for the selected\n            model.\n          </Tooltip>\n        </div>\n        <input\n          type=\"number\"\n          name=\"EmbeddingOutputDimensions\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"Assume default dimensions\"\n          min={1}\n          onScroll={(e) => e.target.blur()}\n          defaultValue={settings?.EmbeddingOutputDimensions}\n          required={false}\n          autoComplete=\"off\"\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/GenericOpenAiOptions/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport { CaretDown, CaretUp, Info } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\n\nexport default function GenericOpenAiEmbeddingOptions({ settings }) {\n  const [showAdvancedControls, setShowAdvancedControls] = useState(false);\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5 flex-wrap\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Base URL\n          </label>\n          <input\n            type=\"url\"\n            name=\"EmbeddingBasePath\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"https://api.openai.com/v1\"\n            defaultValue={settings?.EmbeddingBasePath}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Embedding Model\n          </label>\n          <input\n            type=\"text\"\n            name=\"EmbeddingModelPref\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"text-embedding-ada-002\"\n            defaultValue={settings?.EmbeddingModelPref}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <div\n            data-tooltip-place=\"top\"\n            data-tooltip-id=\"max-embedding-chunk-length-tooltip\"\n            className=\"flex gap-x-1 items-center mb-3\"\n          >\n            <Info\n              size={16}\n              className=\"text-theme-text-secondary cursor-pointer\"\n            />\n            <label className=\"text-white text-sm font-semibold block\">\n              Max embedding chunk length\n            </label>\n            <Tooltip id=\"max-embedding-chunk-length-tooltip\">\n              Maximum length of text chunks, in characters, for embedding.\n            </Tooltip>\n          </div>\n          <input\n            type=\"number\"\n            name=\"EmbeddingModelMaxChunkLength\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"8192\"\n            min={1}\n            onScroll={(e) => e.target.blur()}\n            defaultValue={settings?.EmbeddingModelMaxChunkLength}\n            required={false}\n            autoComplete=\"off\"\n          />\n        </div>\n      </div>\n      <div className=\"w-full flex items-center gap-[36px]\">\n        <div className=\"flex flex-col w-60\">\n          <div className=\"flex flex-col gap-y-1 mb-4\">\n            <label className=\"text-white text-sm font-semibold flex items-center gap-x-2\">\n              API Key <p className=\"!text-xs !italic !font-thin\">optional</p>\n            </label>\n          </div>\n          <input\n            type=\"password\"\n            name=\"GenericOpenAiEmbeddingApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"sk-mysecretkey\"\n            defaultValue={\n              settings?.GenericOpenAiEmbeddingApiKey ? \"*\".repeat(20) : \"\"\n            }\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n      <div className=\"flex justify-start mt-4\">\n        <button\n          onClick={(e) => {\n            e.preventDefault();\n            setShowAdvancedControls(!showAdvancedControls);\n          }}\n          className=\"border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm\"\n        >\n          {showAdvancedControls ? \"Hide\" : \"Show\"} advanced settings\n          {showAdvancedControls ? (\n            <CaretUp size={14} className=\"ml-1\" />\n          ) : (\n            <CaretDown size={14} className=\"ml-1\" />\n          )}\n        </button>\n      </div>\n      <div hidden={!showAdvancedControls}>\n        <div className=\"w-full flex items-start gap-4\">\n          <div className=\"flex flex-col w-60\">\n            <div className=\"flex flex-col gap-y-1 mb-4\">\n              <label className=\"text-white text-sm font-semibold flex items-center gap-x-2\">\n                Max concurrent Chunks\n                <p className=\"!text-xs !italic !font-thin\">optional</p>\n              </label>\n            </div>\n            <input\n              type=\"number\"\n              name=\"GenericOpenAiEmbeddingMaxConcurrentChunks\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"500\"\n              min={1}\n              onScroll={(e) => e.target.blur()}\n              defaultValue={settings?.GenericOpenAiEmbeddingMaxConcurrentChunks}\n              required={false}\n              autoComplete=\"off\"\n              spellCheck={false}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/LMStudioOptions/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport System from \"@/models/system\";\nimport { LMSTUDIO_COMMON_URLS } from \"@/utils/constants\";\nimport {\n  CaretDown,\n  CaretUp,\n  Info,\n  CircleNotch,\n  Warning,\n} from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\nimport useProviderEndpointAutoDiscovery from \"@/hooks/useProviderEndpointAutoDiscovery\";\n\nexport default function LMStudioEmbeddingOptions({ settings }) {\n  const {\n    autoDetecting: loading,\n    basePath,\n    basePathValue,\n    authToken,\n    authTokenValue,\n    showAdvancedControls,\n    setShowAdvancedControls,\n    handleAutoDetectClick,\n  } = useProviderEndpointAutoDiscovery({\n    provider: \"lmstudio\",\n    initialBasePath: settings?.EmbeddingBasePath,\n    ENDPOINTS: LMSTUDIO_COMMON_URLS,\n  });\n\n  const [maxChunkLength, setMaxChunkLength] = useState(\n    settings?.EmbeddingModelMaxChunkLength || 8192\n  );\n\n  const handleMaxChunkLengthChange = (e) => {\n    setMaxChunkLength(Number(e.target.value));\n  };\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-start gap-[36px] mt-1.5\">\n        <LMStudioModelSelection\n          settings={settings}\n          basePath={basePath.value}\n          apiKey={authTokenValue.value}\n        />\n        <div className=\"flex flex-col w-60\">\n          <div\n            data-tooltip-place=\"top\"\n            data-tooltip-id=\"max-embedding-chunk-length-tooltip\"\n            className=\"flex gap-x-1 items-center mb-3\"\n          >\n            <label className=\"text-white text-sm font-semibold block\">\n              Max embedding chunk length\n            </label>\n            <Info\n              size={16}\n              className=\"text-theme-text-secondary cursor-pointer\"\n            />\n            <Tooltip id=\"max-embedding-chunk-length-tooltip\">\n              Maximum length of text chunks, in characters, for embedding.\n            </Tooltip>\n          </div>\n          <input\n            type=\"number\"\n            name=\"EmbeddingModelMaxChunkLength\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"8192\"\n            min={1}\n            value={maxChunkLength}\n            onChange={handleMaxChunkLengthChange}\n            onScroll={(e) => e.target.blur()}\n            required={true}\n            autoComplete=\"off\"\n          />\n        </div>\n      </div>\n      <div className=\"flex justify-start mt-4\">\n        <button\n          onClick={(e) => {\n            e.preventDefault();\n            setShowAdvancedControls(!showAdvancedControls);\n          }}\n          className=\"border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm\"\n        >\n          {showAdvancedControls ? \"Hide\" : \"Show\"} Manual Endpoint Input\n          {showAdvancedControls ? (\n            <CaretUp size={14} className=\"ml-1\" />\n          ) : (\n            <CaretDown size={14} className=\"ml-1\" />\n          )}\n        </button>\n      </div>\n\n      <div hidden={!showAdvancedControls}>\n        <div className=\"w-full flex items-start gap-4\">\n          <div className=\"flex flex-col w-[300px]\">\n            <div className=\"flex justify-between items-center mb-2\">\n              <div className=\"flex items-center gap-1\">\n                <label className=\"text-white text-sm font-semibold\">\n                  LM Studio Base URL\n                </label>\n                <Info\n                  size={18}\n                  className=\"text-theme-text-secondary cursor-pointer\"\n                  data-tooltip-id=\"lmstudio-base-url\"\n                  data-tooltip-content=\"Enter the URL where LM Studio is running.\"\n                />\n                <Tooltip\n                  id=\"lmstudio-base-url\"\n                  place=\"top\"\n                  delayShow={300}\n                  className=\"tooltip !text-xs !opacity-100\"\n                  style={{\n                    maxWidth: \"250px\",\n                    whiteSpace: \"normal\",\n                    wordWrap: \"break-word\",\n                  }}\n                />\n              </div>\n              {loading ? (\n                <CircleNotch\n                  size={16}\n                  className=\"text-theme-text-secondary animate-spin\"\n                />\n              ) : (\n                <>\n                  {!basePathValue.value && (\n                    <button\n                      onClick={handleAutoDetectClick}\n                      className=\"border-none bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]\"\n                    >\n                      Auto-Detect\n                    </button>\n                  )}\n                </>\n              )}\n            </div>\n            <input\n              type=\"url\"\n              name=\"EmbeddingBasePath\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"http://localhost:1234/v1\"\n              value={basePathValue.value}\n              required={true}\n              autoComplete=\"off\"\n              spellCheck={false}\n              onChange={basePath.onChange}\n              onBlur={basePath.onBlur}\n            />\n          </div>\n          <div className=\"flex flex-col w-60\">\n            <div className=\"flex items-center mb-2 gap-x-1\">\n              <label className=\"text-white text-sm font-semibold\">\n                Authentication Token\n              </label>\n              <Info\n                size={18}\n                className=\"text-theme-text-secondary cursor-pointer\"\n                data-tooltip-id=\"lmstudio-authentication-token\"\n              />\n              <Tooltip\n                id=\"lmstudio-authentication-token\"\n                place=\"top\"\n                delayShow={300}\n                delayHide={400}\n                clickable={true}\n                className=\"tooltip !text-xs !opacity-100\"\n                style={{\n                  maxWidth: \"250px\",\n                  whiteSpace: \"normal\",\n                  wordWrap: \"break-word\",\n                }}\n              >\n                <p className=\"text-xs leading-[18px] font-base\">\n                  Enter a <code>Bearer</code> Auth Token for interacting with\n                  your LM Studio server.\n                  <br /> <br />\n                  Useful if running LM Studio behind an authentication or proxy.\n                </p>\n              </Tooltip>\n            </div>\n            <input\n              type=\"password\"\n              name=\"LMStudioAuthToken\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg outline-none block w-full p-2.5 focus:outline-primary-button active:outline-primary-button\"\n              placeholder=\"LM Studio Auth Token\"\n              defaultValue={settings?.LMStudioAuthToken ? \"*\".repeat(20) : \"\"}\n              value={authTokenValue.value}\n              onChange={authToken.onChange}\n              onBlur={authToken.onBlur}\n              required={false}\n              autoComplete=\"off\"\n              spellCheck={false}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction LMStudioModelSelection({ settings, basePath = null, apiKey = null }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!basePath) {\n        setCustomModels([]);\n        setLoading(false);\n        return;\n      }\n      setLoading(true);\n      try {\n        const { models } = await System.customModels(\n          \"lmstudio\",\n          apiKey,\n          basePath\n        );\n        setCustomModels(models || []);\n      } catch (error) {\n        console.error(\"Failed to fetch custom models:\", error);\n        setCustomModels([]);\n      }\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [basePath, apiKey]);\n\n  if (loading || customModels.length == 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <div className=\"flex items-center mb-2 gap-x-1\">\n          <label className=\"text-white text-sm font-semibold\">\n            Embedding Model\n          </label>\n          {!loading && !!basePath && (\n            <>\n              <Warning\n                size={18}\n                className=\"text-red-400 cursor-pointer\"\n                data-tooltip-id=\"lmstudio-embedding-model\"\n              />\n              <Tooltip\n                id=\"lmstudio-embedding-model\"\n                place=\"top\"\n                delayShow={300}\n                delayHide={400}\n                clickable={true}\n                className=\"tooltip !text-xs !opacity-100\"\n                style={{\n                  maxWidth: \"250px\",\n                  whiteSpace: \"normal\",\n                  wordWrap: \"break-word\",\n                }}\n              >\n                <p className=\"text-xs leading-[18px] font-base\">\n                  Could not reach LM Studio. Verify the URL is correct and the\n                  LMStudio server is running and accessible.\n                </p>\n              </Tooltip>\n            </>\n          )}\n        </div>\n        <select\n          name=\"EmbeddingModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {loading\n              ? \"--loading available models--\"\n              : !!basePath\n                ? \"No models found\"\n                : \"Enter LM Studio URL first\"}\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-2\">\n        LM Studio Embedding Model\n      </label>\n      <select\n        name=\"EmbeddingModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Your loaded models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings.EmbeddingModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n      <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n        Choose the LM Studio model you want to use for generating embeddings.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/LemonadeOptions/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport System from \"@/models/system\";\nimport { LEMONADE_COMMON_URLS } from \"@/utils/constants\";\nimport { CaretDown, CaretUp, Info, CircleNotch } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\nimport useProviderEndpointAutoDiscovery from \"@/hooks/useProviderEndpointAutoDiscovery\";\nimport { cleanBasePath } from \"@/components/LLMSelection/LemonadeOptions\";\n\nexport default function LemonadeEmbeddingOptions({ settings }) {\n  const {\n    autoDetecting: loading,\n    basePath,\n    basePathValue,\n    showAdvancedControls,\n    setShowAdvancedControls,\n    handleAutoDetectClick,\n  } = useProviderEndpointAutoDiscovery({\n    provider: \"lemonade\",\n    initialBasePath: settings?.EmbeddingBasePath,\n    ENDPOINTS: LEMONADE_COMMON_URLS,\n  });\n\n  const [maxChunkLength, setMaxChunkLength] = useState(\n    settings?.EmbeddingModelMaxChunkLength || 8192\n  );\n\n  const handleMaxChunkLengthChange = (e) => {\n    setMaxChunkLength(Number(e.target.value));\n  };\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-start gap-[36px] mt-1.5\">\n        <LemonadeModelSelection settings={settings} basePath={basePath.value} />\n        <div className=\"flex flex-col w-60\">\n          <div\n            data-tooltip-place=\"top\"\n            data-tooltip-id=\"max-embedding-chunk-length-tooltip\"\n            className=\"flex gap-x-1 items-center mb-3\"\n          >\n            <label className=\"text-white text-sm font-semibold block\">\n              Max embedding chunk length\n            </label>\n            <Info\n              size={16}\n              className=\"text-theme-text-secondary cursor-pointer\"\n            />\n            <Tooltip id=\"max-embedding-chunk-length-tooltip\">\n              Maximum length of text chunks, in characters, for embedding.\n            </Tooltip>\n          </div>\n          <input\n            type=\"number\"\n            name=\"EmbeddingModelMaxChunkLength\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"8192\"\n            min={1}\n            value={maxChunkLength}\n            onChange={handleMaxChunkLengthChange}\n            onScroll={(e) => e.target.blur()}\n            required={true}\n            autoComplete=\"off\"\n          />\n        </div>\n      </div>\n      <div className=\"flex justify-start mt-4\">\n        <button\n          onClick={(e) => {\n            e.preventDefault();\n            setShowAdvancedControls(!showAdvancedControls);\n          }}\n          className=\"border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm\"\n        >\n          {showAdvancedControls ? \"Hide\" : \"Show\"} Manual Endpoint Input\n          {showAdvancedControls ? (\n            <CaretUp size={14} className=\"ml-1\" />\n          ) : (\n            <CaretDown size={14} className=\"ml-1\" />\n          )}\n        </button>\n      </div>\n\n      <div hidden={!showAdvancedControls}>\n        <div className=\"w-full flex items-start gap-4\">\n          <div className=\"flex flex-col w-[300px]\">\n            <div className=\"flex justify-between items-center mb-2\">\n              <div className=\"flex items-center gap-1\">\n                <label className=\"text-white text-sm font-semibold\">\n                  Lemonade Base URL\n                </label>\n                <Info\n                  size={18}\n                  className=\"text-theme-text-secondary cursor-pointer\"\n                  data-tooltip-id=\"lemonade-base-url\"\n                  data-tooltip-content=\"Enter the URL where Lemonade is running.\"\n                />\n                <Tooltip\n                  id=\"lemonade-base-url\"\n                  place=\"top\"\n                  delayShow={300}\n                  className=\"tooltip !text-xs !opacity-100\"\n                  style={{\n                    maxWidth: \"250px\",\n                    whiteSpace: \"normal\",\n                    wordWrap: \"break-word\",\n                  }}\n                />\n              </div>\n              {loading ? (\n                <CircleNotch\n                  size={16}\n                  className=\"text-theme-text-secondary animate-spin\"\n                />\n              ) : (\n                <>\n                  {!basePathValue.value && (\n                    <button\n                      onClick={handleAutoDetectClick}\n                      className=\"border-none bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]\"\n                    >\n                      Auto-Detect\n                    </button>\n                  )}\n                </>\n              )}\n            </div>\n            <input\n              type=\"url\"\n              name=\"EmbeddingBasePath\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"http://localhost:8000/live\"\n              value={cleanBasePath(basePathValue.value)}\n              required={true}\n              autoComplete=\"off\"\n              spellCheck={false}\n              onChange={basePath.onChange}\n              onBlur={basePath.onBlur}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction LemonadeModelSelection({ settings, basePath = null }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!basePath) {\n        setCustomModels([]);\n        setLoading(false);\n        return;\n      }\n      setLoading(true);\n      try {\n        const { models } = await System.customModels(\n          \"lemonade-embedder\",\n          null,\n          basePath\n        );\n        setCustomModels(models || []);\n      } catch (error) {\n        console.error(\"Failed to fetch custom models:\", error);\n        setCustomModels([]);\n      }\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [basePath]);\n\n  if (loading || customModels.length == 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-2\">\n          Lemonade Embedding Model\n        </label>\n        <select\n          name=\"EmbeddingModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {!!basePath\n              ? \"--loading available models--\"\n              : \"Enter Lemonade URL first\"}\n          </option>\n        </select>\n        <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n          Select the Lemonade model for embeddings. Models will load after\n          entering a valid Lemonade URL.\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Lemonade Embedding Model\n      </label>\n      <select\n        name=\"EmbeddingModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Your loaded models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings.EmbeddingModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/LiteLLMOptions/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport System from \"@/models/system\";\nimport { Warning, Info } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\n\nexport default function LiteLLMOptions({ settings }) {\n  const [basePathValue, setBasePathValue] = useState(settings?.LiteLLMBasePath);\n  const [basePath, setBasePath] = useState(settings?.LiteLLMBasePath);\n  const [apiKeyValue, setApiKeyValue] = useState(settings?.LiteLLMAPIKey);\n  const [apiKey, setApiKey] = useState(settings?.LiteLLMAPIKey);\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Base URL\n          </label>\n          <input\n            type=\"url\"\n            name=\"LiteLLMBasePath\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"http://127.0.0.1:4000\"\n            defaultValue={settings?.LiteLLMBasePath}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={(e) => setBasePathValue(e.target.value)}\n            onBlur={() => setBasePath(basePathValue)}\n          />\n        </div>\n        <LiteLLMModelSelection\n          settings={settings}\n          basePath={basePath}\n          apiKey={apiKey}\n        />\n        <div className=\"flex flex-col w-60\">\n          <div\n            data-tooltip-place=\"top\"\n            data-tooltip-id=\"max-embedding-chunk-length-tooltip\"\n            className=\"flex gap-x-1 items-center mb-3\"\n          >\n            <Info\n              size={16}\n              className=\"text-theme-text-secondary cursor-pointer\"\n            />\n            <label className=\"text-white text-sm font-semibold block\">\n              Max embedding chunk length\n            </label>\n            <Tooltip id=\"max-embedding-chunk-length-tooltip\">\n              Maximum length of text chunks, in characters, for embedding.\n            </Tooltip>\n          </div>\n          <input\n            type=\"number\"\n            name=\"EmbeddingModelMaxChunkLength\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"8192\"\n            min={1}\n            onScroll={(e) => e.target.blur()}\n            defaultValue={settings?.EmbeddingModelMaxChunkLength}\n            required={false}\n            autoComplete=\"off\"\n          />\n        </div>\n      </div>\n      <div className=\"w-full flex items-center gap-[36px]\">\n        <div className=\"flex flex-col w-60\">\n          <div className=\"flex flex-col gap-y-1 mb-4\">\n            <label className=\"text-white text-sm font-semibold flex items-center gap-x-2\">\n              API Key <p className=\"!text-xs !italic !font-thin\">optional</p>\n            </label>\n          </div>\n          <input\n            type=\"password\"\n            name=\"LiteLLMAPIKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"sk-mysecretkey\"\n            defaultValue={settings?.LiteLLMAPIKey ? \"*\".repeat(20) : \"\"}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={(e) => setApiKeyValue(e.target.value)}\n            onBlur={() => setApiKey(apiKeyValue)}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction LiteLLMModelSelection({ settings, basePath = null, apiKey = null }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!basePath) {\n        setCustomModels([]);\n        setLoading(false);\n        return;\n      }\n      setLoading(true);\n      const { models } = await System.customModels(\n        \"litellm\",\n        typeof apiKey === \"boolean\" ? null : apiKey,\n        basePath\n      );\n      setCustomModels(models || []);\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [basePath, apiKey]);\n\n  if (loading || customModels.length == 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Embedding Model Selection\n        </label>\n        <select\n          name=\"EmbeddingModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {basePath?.includes(\"/v1\")\n              ? \"-- loading available models --\"\n              : \"-- waiting for URL --\"}\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <div className=\"flex items-center\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Embedding Model Selection\n        </label>\n        <EmbeddingModelTooltip />\n      </div>\n      <select\n        name=\"EmbeddingModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Your loaded models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings.EmbeddingModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n    </div>\n  );\n}\n\nfunction EmbeddingModelTooltip() {\n  return (\n    <div className=\"flex items-center justify-center -mt-3 ml-1\">\n      <Warning\n        size={14}\n        className=\"ml-1 text-orange-500 cursor-pointer\"\n        data-tooltip-id=\"model-tooltip\"\n        data-tooltip-place=\"right\"\n      />\n      <Tooltip\n        delayHide={300}\n        id=\"model-tooltip\"\n        className=\"max-w-xs\"\n        clickable={true}\n      >\n        <p className=\"text-sm\">\n          Be sure to select a valid embedding model. Chat models are not\n          embedding models. See{\" \"}\n          <a\n            href=\"https://litellm.vercel.app/docs/embedding/supported_embedding\"\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"underline\"\n          >\n            this page\n          </a>{\" \"}\n          for more information.\n        </p>\n      </Tooltip>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/LocalAiOptions/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { CaretDown, CaretUp, Info } from \"@phosphor-icons/react\";\nimport System from \"@/models/system\";\nimport PreLoader from \"@/components/Preloader\";\nimport { LOCALAI_COMMON_URLS } from \"@/utils/constants\";\nimport useProviderEndpointAutoDiscovery from \"@/hooks/useProviderEndpointAutoDiscovery\";\nimport { Tooltip } from \"react-tooltip\";\n\nexport default function LocalAiOptions({ settings }) {\n  const {\n    autoDetecting: loading,\n    basePath,\n    basePathValue,\n    showAdvancedControls,\n    setShowAdvancedControls,\n    handleAutoDetectClick,\n  } = useProviderEndpointAutoDiscovery({\n    provider: \"localai\",\n    initialBasePath: settings?.EmbeddingBasePath,\n    ENDPOINTS: LOCALAI_COMMON_URLS,\n  });\n  const [apiKeyValue, setApiKeyValue] = useState(settings?.LocalAiApiKey);\n  const [apiKey, setApiKey] = useState(settings?.LocalAiApiKey);\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <LocalAIModelSelection\n          settings={settings}\n          apiKey={apiKey}\n          basePath={basePath.value}\n        />\n        <div className=\"flex flex-col w-60\">\n          <div className=\"flex flex-col gap-y-1 mb-2\">\n            <div className=\"flex gap-x-1 items-center\">\n              <label className=\"text-white text-sm font-semibold block\">\n                Local AI API Key\n              </label>\n              <Info\n                size={16}\n                data-tooltip-id=\"localai-api-key-tooltip\"\n                className=\"text-theme-text-secondary cursor-pointer\"\n              />\n              <Tooltip\n                id=\"localai-api-key-tooltip\"\n                place=\"top\"\n                delayShow={300}\n                className=\"tooltip !text-xs !opacity-100\"\n                style={{\n                  maxWidth: \"250px\",\n                  whiteSpace: \"normal\",\n                  wordWrap: \"break-word\",\n                }}\n              >\n                The API key for the LocalAI server (if applicable).\n              </Tooltip>\n            </div>\n          </div>\n          <input\n            type=\"password\"\n            name=\"LocalAiApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"sk-mysecretkey\"\n            defaultValue={settings?.LocalAiApiKey ? \"*\".repeat(20) : \"\"}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={(e) => setApiKeyValue(e.target.value)}\n            onBlur={() => setApiKey(apiKeyValue)}\n          />\n        </div>\n      </div>\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <div\n            data-tooltip-place=\"top\"\n            data-tooltip-id=\"max-embedding-chunk-length-tooltip\"\n            className=\"flex gap-x-1 items-center mb-3\"\n          >\n            <label className=\"text-white text-sm font-semibold block\">\n              Max embedding chunk length\n            </label>\n            <Info\n              size={16}\n              className=\"text-theme-text-secondary cursor-pointer\"\n            />\n            <Tooltip\n              id=\"max-embedding-chunk-length-tooltip\"\n              place=\"top\"\n              delayShow={300}\n              className=\"tooltip !text-xs !opacity-100\"\n              style={{\n                maxWidth: \"250px\",\n                whiteSpace: \"normal\",\n                wordWrap: \"break-word\",\n              }}\n            >\n              Maximum length of text chunks, in characters, for embedding.\n            </Tooltip>\n          </div>\n          <input\n            type=\"number\"\n            name=\"EmbeddingModelMaxChunkLength\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"1000\"\n            min={1}\n            onScroll={(e) => e.target.blur()}\n            defaultValue={settings?.EmbeddingModelMaxChunkLength}\n            required={false}\n            autoComplete=\"off\"\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <div\n            data-tooltip-id=\"embedding-output-dimensions-tooltip\"\n            className=\"flex gap-x-1 items-center mb-3\"\n          >\n            <label className=\"text-white text-sm font-semibold block\">\n              Output dimensions\n            </label>\n            <Info\n              size={16}\n              className=\"text-theme-text-secondary cursor-pointer\"\n            />\n            <Tooltip\n              id=\"embedding-output-dimensions-tooltip\"\n              place=\"top\"\n              delayShow={300}\n              className=\"tooltip !text-xs !opacity-100\"\n              style={{\n                maxWidth: \"250px\",\n                whiteSpace: \"normal\",\n                wordWrap: \"break-word\",\n              }}\n            >\n              The number of dimensions the resulting output embeddings should\n              have if it supports multiple dimensions output.\n              <br />\n              <br /> Leave blank to use the default dimensions for the selected\n              model.\n            </Tooltip>\n          </div>\n          <input\n            type=\"number\"\n            name=\"EmbeddingOutputDimensions\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Assume default dimensions\"\n            min={1}\n            onScroll={(e) => e.target.blur()}\n            defaultValue={settings?.EmbeddingOutputDimensions}\n            required={false}\n            autoComplete=\"off\"\n          />\n        </div>\n      </div>\n      <div className=\"flex justify-start mt-4\">\n        <button\n          onClick={(e) => {\n            e.preventDefault();\n            setShowAdvancedControls(!showAdvancedControls);\n          }}\n          className=\"border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm\"\n        >\n          {showAdvancedControls ? \"Hide\" : \"Show\"} advanced settings\n          {showAdvancedControls ? (\n            <CaretUp size={14} className=\"ml-1\" />\n          ) : (\n            <CaretDown size={14} className=\"ml-1\" />\n          )}\n        </button>\n      </div>\n      <div hidden={!showAdvancedControls}>\n        <div className=\"w-full flex items-center gap-4\">\n          <div className=\"flex flex-col w-60\">\n            <div className=\"flex justify-between items-center mb-2\">\n              <label className=\"text-white text-sm font-semibold\">\n                LocalAI Base URL\n              </label>\n              {loading ? (\n                <PreLoader size=\"6\" />\n              ) : (\n                <>\n                  {!basePathValue.value && (\n                    <button\n                      onClick={handleAutoDetectClick}\n                      className=\"bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]\"\n                    >\n                      Auto-Detect\n                    </button>\n                  )}\n                </>\n              )}\n            </div>\n            <input\n              type=\"url\"\n              name=\"EmbeddingBasePath\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"http://localhost:8080/v1\"\n              value={basePathValue.value}\n              required={true}\n              autoComplete=\"off\"\n              spellCheck={false}\n              onChange={basePath.onChange}\n              onBlur={basePath.onBlur}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction LocalAIModelSelection({ settings, apiKey = null, basePath = null }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!basePath || !basePath.includes(\"/v1\")) {\n        setCustomModels([]);\n        setLoading(false);\n        return;\n      }\n      setLoading(true);\n      const { models } = await System.customModels(\n        \"localai\",\n        typeof apiKey === \"boolean\" ? null : apiKey,\n        basePath\n      );\n      setCustomModels(models || []);\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [basePath, apiKey]);\n\n  if (loading || customModels.length == 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-2\">\n          Embedding Model Name\n        </label>\n        <select\n          name=\"EmbeddingModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {basePath?.includes(\"/v1\")\n              ? \"-- loading available models --\"\n              : \"-- waiting for URL --\"}\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-2\">\n        Embedding Model Name\n      </label>\n      <select\n        name=\"EmbeddingModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Your loaded models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.EmbeddingModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/MistralAiOptions/index.jsx",
    "content": "export default function MistralAiOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-4\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"MistralApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Mistral AI API Key\"\n            defaultValue={settings?.MistralApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Model Preference\n          </label>\n          <select\n            name=\"EmbeddingModelPref\"\n            required={true}\n            defaultValue={settings?.EmbeddingModelPref}\n            className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n          >\n            <optgroup label=\"Available embedding models\">\n              {[\"mistral-embed\"].map((model) => {\n                return (\n                  <option key={model} value={model}>\n                    {model}\n                  </option>\n                );\n              })}\n            </optgroup>\n          </select>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/NativeEmbeddingOptions/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport System from \"@/models/system\";\n\nexport default function NativeEmbeddingOptions({ settings }) {\n  const [loading, setLoading] = useState(true);\n  const [availableModels, setAvailableModels] = useState([]);\n  const [selectedModel, setSelectedModel] = useState(\n    settings?.EmbeddingModelPref\n  );\n  const [selectedModelInfo, setSelectedModelInfo] = useState();\n\n  useEffect(() => {\n    System.customModels(\"native-embedder\")\n      .then(({ models }) => {\n        if (models?.length > 0) {\n          setAvailableModels(models);\n          const _selectedModel =\n            models.find((model) => model.id === settings?.EmbeddingModelPref) ??\n            models[0];\n          setSelectedModel(_selectedModel.id);\n          setSelectedModelInfo(_selectedModel);\n        }\n      })\n      .finally(() => {\n        setLoading(false);\n      });\n  }, []);\n\n  useEffect(() => {\n    if (!availableModels?.length || !selectedModel) return;\n    setSelectedModelInfo(\n      availableModels.find((model) => model.id === selectedModel)\n    );\n  }, [selectedModel, availableModels]);\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-4\">\n      <div className=\"w-full flex flex-col mt-1.5\">\n        <div className=\"flex flex-col w-96\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Model Preference\n          </label>\n          <select\n            name=\"EmbeddingModelPref\"\n            required={true}\n            defaultValue={selectedModel}\n            className=\"border-none bg-theme-settings-input-bg border-gray-500 text-theme-text-primary text-sm rounded-lg block w-60 p-2.5\"\n            onChange={(e) => setSelectedModel(e.target.value)}\n          >\n            {loading ? (\n              <option\n                value=\"--loading-available-models--\"\n                disabled={true}\n                selected={true}\n              >\n                --loading available models--\n              </option>\n            ) : (\n              <optgroup label=\"Available embedding models\">\n                {availableModels.map((model) => {\n                  return (\n                    <option\n                      key={model.id}\n                      value={model.id}\n                      selected={selectedModel === model.id}\n                    >\n                      {model.name}\n                    </option>\n                  );\n                })}\n              </optgroup>\n            )}\n          </select>\n        </div>\n        {selectedModelInfo && (\n          <div className=\"flex flex-col gap-y-2 mt-2\">\n            <p className=\"text-theme-text-secondary text-xs font-normal block\">\n              {selectedModelInfo?.description}\n            </p>\n            <p className=\"text-theme-text-secondary text-xs font-normal block\">\n              Trained on: {selectedModelInfo?.lang}\n            </p>\n            <p className=\"text-theme-text-secondary text-xs font-normal block\">\n              Download Size: {selectedModelInfo?.size}\n            </p>\n            <Link\n              to={selectedModelInfo?.modelCard}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-theme-text-secondary text-xs font-normal block underline hover:text-theme-text-primary\"\n            >\n              View model card on Hugging Face &rarr;\n            </Link>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/OllamaOptions/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport System from \"@/models/system\";\nimport PreLoader from \"@/components/Preloader\";\nimport { OLLAMA_COMMON_URLS } from \"@/utils/constants\";\nimport { CaretDown, CaretUp, Info } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\nimport useProviderEndpointAutoDiscovery from \"@/hooks/useProviderEndpointAutoDiscovery\";\n\nexport default function OllamaEmbeddingOptions({ settings }) {\n  const {\n    autoDetecting: loading,\n    basePath,\n    basePathValue,\n    showAdvancedControls,\n    setShowAdvancedControls,\n    handleAutoDetectClick,\n    authToken,\n    authTokenValue,\n  } = useProviderEndpointAutoDiscovery({\n    provider: \"ollama\",\n    initialBasePath: settings?.EmbeddingBasePath,\n    ENDPOINTS: OLLAMA_COMMON_URLS,\n  });\n\n  const [maxChunkLength, setMaxChunkLength] = useState(\n    settings?.EmbeddingModelMaxChunkLength || 8192\n  );\n  const [batchSize, setBatchSize] = useState(\n    settings?.OllamaEmbeddingBatchSize || 1\n  );\n\n  const handleMaxChunkLengthChange = (e) => {\n    setMaxChunkLength(Number(e.target.value));\n  };\n\n  const handleBatchSizeChange = (e) => {\n    setBatchSize(Number(e.target.value));\n  };\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-start gap-[36px] mt-1.5\">\n        <OllamaEmbeddingModelSelection\n          settings={settings}\n          basePath={basePath.value}\n        />\n        <div className=\"flex flex-col w-60\">\n          <div\n            data-tooltip-place=\"top\"\n            data-tooltip-id=\"max-embedding-chunk-length-tooltip\"\n            className=\"flex gap-x-1 items-center mb-3\"\n          >\n            <label className=\"text-white text-sm font-semibold block\">\n              Max embedding chunk length\n            </label>\n            <Info\n              size={16}\n              className=\"text-theme-text-secondary cursor-pointer\"\n            />\n            <Tooltip id=\"max-embedding-chunk-length-tooltip\">\n              Maximum length of text chunks, in characters, for embedding.\n            </Tooltip>\n          </div>\n          <input\n            type=\"number\"\n            name=\"EmbeddingModelMaxChunkLength\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"8192\"\n            min={1}\n            value={maxChunkLength}\n            onChange={handleMaxChunkLengthChange}\n            onScroll={(e) => e.target.blur()}\n            required={true}\n            autoComplete=\"off\"\n          />\n        </div>\n      </div>\n      <div className=\"flex justify-start mt-4\">\n        <button\n          onClick={(e) => {\n            e.preventDefault();\n            setShowAdvancedControls(!showAdvancedControls);\n          }}\n          className=\"border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm\"\n        >\n          {showAdvancedControls ? \"Hide\" : \"Show\"} Advanced Settings\n          {showAdvancedControls ? (\n            <CaretUp size={14} className=\"ml-1\" />\n          ) : (\n            <CaretDown size={14} className=\"ml-1\" />\n          )}\n        </button>\n      </div>\n\n      <div hidden={!showAdvancedControls}>\n        <div className=\"w-full flex items-start gap-4\">\n          <div className=\"flex flex-col w-60\">\n            <div className=\"flex justify-between items-center mb-2\">\n              <label className=\"text-white text-sm font-semibold\">\n                Ollama Base URL\n              </label>\n              {loading ? (\n                <PreLoader size=\"6\" />\n              ) : (\n                <>\n                  {!basePathValue.value && (\n                    <button\n                      onClick={handleAutoDetectClick}\n                      className=\"bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]\"\n                    >\n                      Auto-Detect\n                    </button>\n                  )}\n                </>\n              )}\n            </div>\n            <input\n              type=\"url\"\n              name=\"EmbeddingBasePath\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"http://127.0.0.1:11434\"\n              value={basePathValue.value}\n              required={true}\n              autoComplete=\"off\"\n              spellCheck={false}\n              onChange={basePath.onChange}\n              onBlur={basePath.onBlur}\n            />\n            <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n              Enter the URL where Ollama is running.\n            </p>\n          </div>\n          <div className=\"flex flex-col w-60\">\n            <div\n              data-tooltip-place=\"top\"\n              data-tooltip-id=\"ollama-batch-size-tooltip\"\n              className=\"flex gap-x-1 items-center mb-3\"\n            >\n              <label className=\"text-white text-sm font-semibold block\">\n                Embedding batch size\n              </label>\n              <Info\n                size={16}\n                className=\"text-theme-text-secondary cursor-pointer\"\n              />\n              <Tooltip id=\"ollama-batch-size-tooltip\">\n                Number of text chunks to embed in parallel. Higher values\n                improve speed but use more memory. Default is 1.\n              </Tooltip>\n            </div>\n            <input\n              type=\"number\"\n              name=\"OllamaEmbeddingBatchSize\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"1\"\n              min={1}\n              value={batchSize}\n              onChange={handleBatchSizeChange}\n              onScroll={(e) => e.target.blur()}\n              required={true}\n              autoComplete=\"off\"\n            />\n            <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n              Increase this value to process multiple chunks simultaneously for\n              faster embedding.\n            </p>\n          </div>\n          <div>\n            <label className=\"text-white font-semibold block mb-3 text-sm\">\n              Auth Token (optional)\n            </label>\n            <input\n              type=\"password\"\n              name=\"OllamaLLMAuthToken\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"Enter your Auth Token\"\n              defaultValue={settings?.OllamaLLMAuthToken ? \"*\".repeat(20) : \"\"}\n              value={authTokenValue.value}\n              onChange={authToken.onChange}\n              onBlur={authToken.onBlur}\n              required={false}\n              autoComplete=\"off\"\n              spellCheck={false}\n            />\n            <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n              Enter a <code>Bearer</code> Auth Token for interacting with your\n              Ollama server.\n              <br />\n              Used <b>only</b> if running Ollama behind an authentication\n              server.\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction OllamaEmbeddingModelSelection({ settings, basePath = null }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!basePath) {\n        setCustomModels([]);\n        setLoading(false);\n        return;\n      }\n      setLoading(true);\n      try {\n        const { models } = await System.customModels(\"ollama\", null, basePath);\n        setCustomModels(models || []);\n      } catch (error) {\n        console.error(\"Failed to fetch custom models:\", error);\n        setCustomModels([]);\n      }\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [basePath]);\n\n  if (loading || customModels.length == 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-2\">\n          Ollama Embedding Model\n        </label>\n        <select\n          name=\"EmbeddingModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {!!basePath\n              ? \"--loading available models--\"\n              : \"Enter Ollama URL first\"}\n          </option>\n        </select>\n        <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n          Select the Ollama model for embeddings. Models will load after\n          entering a valid Ollama URL.\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-2\">\n        Ollama Embedding Model\n      </label>\n      <select\n        name=\"EmbeddingModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Your loaded models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings.EmbeddingModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n      <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n        Choose the Ollama model you want to use for generating embeddings.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/OpenAiOptions/index.jsx",
    "content": "export default function OpenAiOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-4\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"OpenAiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"OpenAI API Key\"\n            defaultValue={settings?.OpenAiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Model Preference\n          </label>\n          <select\n            name=\"EmbeddingModelPref\"\n            required={true}\n            className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n          >\n            <optgroup label=\"Available embedding models\">\n              {[\n                \"text-embedding-ada-002\",\n                \"text-embedding-3-small\",\n                \"text-embedding-3-large\",\n              ].map((model) => {\n                return (\n                  <option\n                    key={model}\n                    value={model}\n                    selected={settings?.EmbeddingModelPref === model}\n                  >\n                    {model}\n                  </option>\n                );\n              })}\n            </optgroup>\n          </select>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/OpenRouterOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function OpenRouterOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-4\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"OpenRouterApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"OpenRouter API Key\"\n            defaultValue={settings?.OpenRouterApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <OpenRouterEmbeddingModelSelection settings={settings} />\n      </div>\n    </div>\n  );\n}\n\nfunction OpenRouterEmbeddingModelSelection({ settings }) {\n  const [models, setModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n  const [selectedModel, setSelectedModel] = useState(\n    settings?.EmbeddingModelPref || \"\"\n  );\n\n  useEffect(() => {\n    async function fetchModels() {\n      setLoading(true);\n      const response = await System.customModels(\"openrouter-embedder\");\n      const fetchedModels = response?.models || [];\n      setModels(fetchedModels);\n\n      if (\n        settings?.EmbeddingModelPref &&\n        fetchedModels.some((m) => m.id === settings.EmbeddingModelPref)\n      ) {\n        setSelectedModel(settings.EmbeddingModelPref);\n      } else if (fetchedModels.length > 0) {\n        setSelectedModel(fetchedModels[0].id);\n      }\n\n      setLoading(false);\n    }\n    fetchModels();\n  }, [settings?.EmbeddingModelPref]);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Model Preference\n        </label>\n        <select\n          name=\"EmbeddingModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Model Preference\n      </label>\n      <select\n        name=\"EmbeddingModelPref\"\n        required={true}\n        value={selectedModel}\n        onChange={(e) => setSelectedModel(e.target.value)}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {models.map((model) => (\n          <option key={model.id} value={model.id}>\n            {model.name}\n          </option>\n        ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/EmbeddingSelection/VoyageAiOptions/index.jsx",
    "content": "export default function VoyageAiOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-4\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"VoyageAiApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Voyage AI API Key\"\n            defaultValue={settings?.VoyageAiApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Model Preference\n          </label>\n          <select\n            name=\"EmbeddingModelPref\"\n            required={true}\n            defaultValue={settings?.EmbeddingModelPref}\n            className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n          >\n            <optgroup label=\"Available embedding models\">\n              {[\n                \"voyage-large-2-instruct\",\n                \"voyage-finance-2\",\n                \"voyage-multilingual-2\",\n                \"voyage-law-2\",\n                \"voyage-code-2\",\n                \"voyage-large-2\",\n                \"voyage-2\",\n                \"voyage-3\",\n                \"voyage-3-lite\",\n                \"voyage-3-large\",\n                \"voyage-code-3\",\n              ].map((model) => {\n                return (\n                  <option key={model} value={model}>\n                    {model}\n                  </option>\n                );\n              })}\n            </optgroup>\n          </select>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/ErrorBoundaryFallback/index.jsx",
    "content": "import { NavLink } from \"react-router-dom\";\nimport { House, ArrowClockwise, Copy, Check } from \"@phosphor-icons/react\";\nimport { useState } from \"react\";\n\nexport default function ErrorBoundaryFallback({ error, resetErrorBoundary }) {\n  const [copied, setCopied] = useState(false);\n\n  const copyErrorDetails = async () => {\n    const details = {\n      url: window.location.href,\n      error: error?.name || \"Unknown Error\",\n      message: error?.message || \"No message available\",\n      stack: error?.stack || \"No stack trace available\",\n      userAgent: navigator.userAgent,\n      timestamp: new Date().toISOString(),\n    };\n\n    const formattedDetails = `\nError Report\n============\nTimestamp: ${details.timestamp}\nURL: ${details.url}\nUser Agent: ${details.userAgent}\n\nError: ${details.error}\nMessage: ${details.message}\n\nStack Trace:\n${details.stack}\n    `.trim();\n\n    try {\n      await navigator.clipboard.writeText(formattedDetails);\n      setCopied(true);\n      setTimeout(() => setCopied(false), 2000);\n    } catch (err) {\n      console.error(\"Failed to copy error details:\", err);\n    }\n  };\n\n  return (\n    <div className=\"flex flex-col items-center justify-center min-h-screen bg-theme-bg-primary text-theme-text-primary gap-4 p-4 md:p-8 w-full\">\n      <h1 className=\"text-xl md:text-2xl font-bold text-center\">\n        An error occurred.\n      </h1>\n      <p className=\"text-theme-text-secondary text-center px-4\">\n        {error?.message}\n      </p>\n      {import.meta.env.DEV && (\n        <div className=\"w-full max-w-4xl\">\n          <div className=\"flex justify-end mb-2\">\n            <button\n              onClick={copyErrorDetails}\n              className=\"flex items-center gap-2 px-3 py-1.5 bg-theme-bg-secondary text-theme-text-primary rounded hover:bg-theme-sidebar-item-hover transition-all duration-200 text-xs font-medium\"\n              title=\"Copy error details\"\n            >\n              {copied ? (\n                <>\n                  <Check className=\"w-3.5 h-3.5\" weight=\"bold\" />\n                  Copied!\n                </>\n              ) : (\n                <>\n                  <Copy className=\"w-3.5 h-3.5\" />\n                  Copy Details\n                </>\n              )}\n            </button>\n          </div>\n          <pre className=\"w-full text-xs md:text-sm text-theme-text-secondary bg-theme-bg-secondary p-4 md:p-6 rounded-lg overflow-x-auto overflow-y-auto max-h-[60vh] md:max-h-[70vh] whitespace-pre-wrap break-words font-mono border border-theme-border shadow-sm\">\n            {error?.stack}\n          </pre>\n        </div>\n      )}\n      <div className=\"flex flex-col md:flex-row gap-3 md:gap-4 mt-4 w-full md:w-auto\">\n        <button\n          onClick={resetErrorBoundary}\n          className=\"flex items-center justify-center gap-2 px-4 py-2 bg-theme-bg-secondary text-theme-text-primary rounded-lg hover:bg-theme-sidebar-item-hover transition-all duration-300 w-full md:w-auto\"\n        >\n          <ArrowClockwise className=\"w-4 h-4\" />\n          Reset\n        </button>\n        <NavLink\n          to=\"/\"\n          className=\"flex items-center justify-center gap-2 px-4 py-2 bg-theme-bg-secondary text-theme-text-primary rounded-lg hover:bg-theme-sidebar-item-hover transition-all duration-300 w-full md:w-auto\"\n        >\n          <House className=\"w-4 h-4\" />\n          Home\n        </NavLink>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Footer/index.jsx",
    "content": "import System from \"@/models/system\";\nimport paths from \"@/utils/paths\";\nimport {\n  BookOpen,\n  DiscordLogo,\n  GithubLogo,\n  Briefcase,\n  Envelope,\n  Globe,\n  HouseLine,\n  Info,\n  LinkSimple,\n} from \"@phosphor-icons/react\";\nimport React, { useEffect, useState } from \"react\";\nimport SettingsButton from \"../SettingsButton\";\nimport { isMobile } from \"react-device-detect\";\nimport { Tooltip } from \"react-tooltip\";\nimport { Link } from \"react-router-dom\";\n\nexport const MAX_ICONS = 3;\nexport const ICON_COMPONENTS = {\n  BookOpen: BookOpen,\n  DiscordLogo: DiscordLogo,\n  GithubLogo: GithubLogo,\n  Envelope: Envelope,\n  LinkSimple: LinkSimple,\n  HouseLine: HouseLine,\n  Globe: Globe,\n  Briefcase: Briefcase,\n  Info: Info,\n};\n\nexport default function Footer() {\n  const [footerData, setFooterData] = useState(false);\n\n  useEffect(() => {\n    async function fetchFooterData() {\n      const { footerData } = await System.fetchCustomFooterIcons();\n      setFooterData(footerData);\n    }\n    fetchFooterData();\n  }, []);\n\n  // wait for some kind of non-false response from footer data first\n  // to prevent pop-in.\n  if (footerData === false) return null;\n\n  if (!Array.isArray(footerData) || footerData.length === 0) {\n    return (\n      <div className=\"flex justify-center mb-2\">\n        <div className=\"flex space-x-4\">\n          <div className=\"flex w-fit\">\n            <Link\n              to={paths.github()}\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              className=\"transition-all duration-300 p-2 rounded-full bg-theme-sidebar-footer-icon hover:bg-theme-sidebar-footer-icon-hover\"\n              aria-label=\"Find us on GitHub\"\n              data-tooltip-id=\"footer-item\"\n              data-tooltip-content=\"View Source Code\"\n            >\n              <GithubLogo\n                weight=\"fill\"\n                className=\"h-5 w-5 text-white light:text-slate-800\"\n              />\n            </Link>\n          </div>\n          <div className=\"flex w-fit\">\n            <Link\n              to={paths.docs()}\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              className=\"transition-all duration-300 p-2 rounded-full bg-theme-sidebar-footer-icon hover:bg-theme-sidebar-footer-icon-hover\"\n              aria-label=\"Docs\"\n              data-tooltip-id=\"footer-item\"\n              data-tooltip-content=\"Open AnythingLLM help docs\"\n            >\n              <BookOpen\n                weight=\"fill\"\n                className=\"h-5 w-5 text-white light:text-slate-800\"\n              />\n            </Link>\n          </div>\n          <div className=\"flex w-fit\">\n            <Link\n              to={paths.discord()}\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              className=\"transition-all duration-300 p-2 rounded-full bg-theme-sidebar-footer-icon hover:bg-theme-sidebar-footer-icon-hover\"\n              aria-label=\"Join our Discord server\"\n              data-tooltip-id=\"footer-item\"\n              data-tooltip-content=\"Join the AnythingLLM Discord\"\n            >\n              <DiscordLogo\n                weight=\"fill\"\n                className=\"h-5 w-5 text-white light:text-slate-800\"\n              />\n            </Link>\n          </div>\n          {!isMobile && <SettingsButton />}\n        </div>\n        <Tooltip\n          id=\"footer-item\"\n          place=\"top\"\n          delayShow={300}\n          className=\"tooltip !text-xs z-99\"\n        />\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex justify-center mb-2\">\n      <div className=\"flex space-x-4\">\n        {footerData.map((item, index) => (\n          <a\n            key={index}\n            href={item.url}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"transition-all duration-300 flex w-fit h-fit p-2 p-2 rounded-full bg-theme-sidebar-footer-icon hover:bg-theme-sidebar-footer-icon-hover hover:border-slate-100\"\n          >\n            {React.createElement(\n              ICON_COMPONENTS?.[item.icon] ?? ICON_COMPONENTS.Info,\n              {\n                weight: \"fill\",\n                className: \"h-5 w-5\",\n                color: \"var(--theme-sidebar-footer-icon-fill)\",\n              }\n            )}\n          </a>\n        ))}\n        {!isMobile && <SettingsButton />}\n      </div>\n      <Tooltip\n        id=\"footer-item\"\n        place=\"top\"\n        delayShow={300}\n        className=\"tooltip !text-xs z-99\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/KeyboardShortcutsHelp/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\nimport {\n  SHORTCUTS,\n  isMac,\n  KEYBOARD_SHORTCUTS_HELP_EVENT,\n} from \"@/utils/keyboardShortcuts\";\n\nexport default function KeyboardShortcutsHelp() {\n  const [isOpen, setIsOpen] = useState(false);\n  const { t } = useTranslation();\n\n  useEffect(() => {\n    window.addEventListener(KEYBOARD_SHORTCUTS_HELP_EVENT, () =>\n      setIsOpen((prev) => !prev)\n    );\n    return () => {\n      window.removeEventListener(KEYBOARD_SHORTCUTS_HELP_EVENT, () =>\n        setIsOpen(false)\n      );\n    };\n  }, []);\n\n  if (!isOpen) return null;\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n      <div className=\"relative bg-theme-bg-secondary rounded-lg p-6 max-w-2xl w-full mx-4\">\n        <div className=\"flex justify-between items-center mb-6\">\n          <h2 className=\"text-xl font-semibold text-white\">\n            {t(\"keyboard-shortcuts.title\")}\n          </h2>\n          <button\n            onClick={() => setIsOpen(false)}\n            className=\"text-white hover:text-gray-300\"\n            aria-label=\"Close\"\n          >\n            <X size={24} />\n          </button>\n        </div>\n\n        <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n          {Object.entries(SHORTCUTS).map(([key, shortcut]) => (\n            <div\n              key={key}\n              className=\"flex items-center justify-between p-3 bg-theme-bg-hover rounded-lg\"\n            >\n              <span className=\"text-white\">\n                {t(`keyboard-shortcuts.shortcuts.${shortcut.translationKey}`)}\n              </span>\n              <kbd className=\"px-2 py-1 bg-theme-bg-secondary text-white rounded border border-gray-600\">\n                {isMac ? key : key.replace(\"⌘\", \"Ctrl\")}\n              </kbd>\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/AnthropicAiOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\nimport { CaretDown, CaretUp } from \"@phosphor-icons/react\";\n\nexport default function AnthropicAiOptions({ settings }) {\n  const [showAdvancedControls, setShowAdvancedControls] = useState(false);\n  const [inputValue, setInputValue] = useState(settings?.AnthropicApiKey);\n  const [anthropicApiKey, setAnthropicApiKey] = useState(\n    settings?.AnthropicApiKey\n  );\n\n  return (\n    <div className=\"w-full flex flex-col\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Anthropic API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"AnthropicApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Anthropic Claude-2 API Key\"\n            defaultValue={settings?.AnthropicApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={(e) => setInputValue(e.target.value)}\n            onBlur={() => setAnthropicApiKey(inputValue)}\n          />\n        </div>\n        {!settings?.credentialsOnly && (\n          <AnthropicModelSelection\n            apiKey={anthropicApiKey}\n            settings={settings}\n          />\n        )}\n      </div>\n      <div className=\"flex justify-start mt-4\">\n        <button\n          onClick={(e) => {\n            e.preventDefault();\n            setShowAdvancedControls(!showAdvancedControls);\n          }}\n          className=\"border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm\"\n        >\n          {showAdvancedControls ? \"Hide\" : \"Show\"} advanced settings\n          {showAdvancedControls ? (\n            <CaretUp size={14} className=\"ml-1\" />\n          ) : (\n            <CaretDown size={14} className=\"ml-1\" />\n          )}\n        </button>\n      </div>\n      <div hidden={!showAdvancedControls}>\n        <div className=\"w-full flex items-start gap-4 mt-1.5\">\n          <div className=\"flex flex-col w-60\">\n            <div className=\"flex justify-between items-center mb-2\">\n              <label className=\"text-white text-sm font-semibold\">\n                Prompt Caching\n              </label>\n            </div>\n            <select\n              name=\"AnthropicCacheControl\"\n              className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n            >\n              <option\n                value=\"none\"\n                selected={settings?.AnthropicCacheControl === \"none\"}\n              >\n                No caching\n              </option>\n              <option\n                value=\"5m\"\n                selected={settings?.AnthropicCacheControl === \"5m\"}\n              >\n                5 minutes\n              </option>\n              <option\n                value=\"1h\"\n                selected={settings?.AnthropicCacheControl === \"1h\"}\n              >\n                1 hour\n              </option>\n            </select>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nconst DEFAULT_MODELS = [\n  {\n    id: \"claude-3-7-sonnet-20250219\",\n    name: \"Claude 3.7 Sonnet\",\n  },\n  {\n    id: \"claude-3-5-sonnet-20241022\",\n    name: \"Claude 3.5 Sonnet (New)\",\n  },\n  {\n    id: \"claude-3-5-haiku-20241022\",\n    name: \"Claude 3.5 Haiku\",\n  },\n  {\n    id: \"claude-3-5-sonnet-20240620\",\n    name: \"Claude 3.5 Sonnet (Old)\",\n  },\n  {\n    id: \"claude-3-haiku-20240307\",\n    name: \"Claude 3 Haiku\",\n  },\n  {\n    id: \"claude-3-opus-20240229\",\n    name: \"Claude 3 Opus\",\n  },\n  {\n    id: \"claude-3-sonnet-20240229\",\n    name: \"Claude 3 Sonnet\",\n  },\n  {\n    id: \"claude-2.1\",\n    name: \"Claude 2.1\",\n  },\n  {\n    id: \"claude-2.0\",\n    name: \"Claude 2.0\",\n  },\n];\n\nfunction AnthropicModelSelection({ apiKey, settings }) {\n  const [models, setModels] = useState(DEFAULT_MODELS);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models } = await System.customModels(\n        \"anthropic\",\n        typeof apiKey === \"boolean\" ? null : apiKey\n      );\n      if (models.length > 0) setModels(models);\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"AnthropicModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"AnthropicModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {models.map((model) => (\n          <option\n            key={model.id}\n            value={model.id}\n            selected={settings?.AnthropicModelPref === model.id}\n          >\n            {model.name}\n          </option>\n        ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/ApiPieOptions/index.jsx",
    "content": "import System from \"@/models/system\";\nimport { useState, useEffect } from \"react\";\n\nexport default function ApiPieLLMOptions({ settings }) {\n  return (\n    <div className=\"flex flex-col gap-y-4 mt-1.5\">\n      <div className=\"flex gap-[36px]\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            APIpie API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"ApipieLLMApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"APIpie API Key\"\n            defaultValue={settings?.ApipieLLMApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        {!settings?.credentialsOnly && (\n          <APIPieModelSelection settings={settings} />\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction APIPieModelSelection({ settings }) {\n  const [groupedModels, setGroupedModels] = useState({});\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models } = await System.customModels(\"apipie\");\n      if (models?.length > 0) {\n        const modelsByOrganization = models.reduce((acc, model) => {\n          acc[model.organization] = acc[model.organization] || [];\n          acc[model.organization].push(model);\n          return acc;\n        }, {});\n\n        setGroupedModels(modelsByOrganization);\n      }\n\n      setLoading(false);\n    }\n    findCustomModels();\n  }, []);\n\n  if (loading || Object.keys(groupedModels).length === 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"ApipieLLMModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"ApipieLLMModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n      >\n        {Object.keys(groupedModels)\n          .sort()\n          .map((organization) => (\n            <optgroup key={organization} label={organization}>\n              {groupedModels[organization].map((model) => (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.ApipieLLMModelPref === model.id}\n                >\n                  {model.name}\n                </option>\n              ))}\n            </optgroup>\n          ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/AwsBedrockLLMOptions/index.jsx",
    "content": "import { ArrowSquareOut, Info } from \"@phosphor-icons/react\";\nimport { AWS_REGIONS } from \"./regions\";\nimport { useState } from \"react\";\n\nexport default function AwsBedrockLLMOptions({ settings }) {\n  const [connectionMethod, setConnectionMethod] = useState(\n    settings?.AwsBedrockLLMConnectionMethod ?? \"iam\"\n  );\n\n  return (\n    <div className=\"w-full flex flex-col\">\n      {!settings?.credentialsOnly && connectionMethod !== \"apiKey\" && (\n        <div className=\"flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2\">\n          <div className=\"gap-x-2 flex items-center\">\n            <Info size={40} />\n            <p className=\"text-base\">\n              You should use a properly defined IAM user for inferencing.\n              <br />\n              <a\n                href=\"https://docs.anythingllm.com/setup/llm-configuration/cloud/aws-bedrock\"\n                target=\"_blank\"\n                className=\"underline flex gap-x-1 items-center\"\n                rel=\"noreferrer\"\n              >\n                Read more on how to use AWS Bedrock in AnythingLLM\n                <ArrowSquareOut size={14} />\n              </a>\n            </p>\n          </div>\n        </div>\n      )}\n\n      <div className=\"flex flex-col gap-y-2 mb-2\">\n        <input\n          type=\"hidden\"\n          name=\"AwsBedrockLLMConnectionMethod\"\n          value={connectionMethod}\n        />\n        <div className=\"flex flex-col w-full\">\n          <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n            Authentication Method\n          </label>\n          <p className=\"text-theme-text-secondary text-sm\">\n            Select the method to authenticate with AWS Bedrock.\n          </p>\n        </div>\n        <select\n          name=\"AwsBedrockLLMConnectionMethod\"\n          value={connectionMethod}\n          required={true}\n          onChange={(e) => setConnectionMethod(e.target.value)}\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-fit p-2.5\"\n        >\n          <option value=\"iam\">IAM (Explicit Credentials)</option>\n          <option value=\"sessionToken\">\n            Session Token (Temporary Credentials)\n          </option>\n          <option value=\"iam_role\">IAM Role (Implied Credentials)</option>\n          <option value=\"apiKey\">Bedrock API Key</option>\n        </select>\n      </div>\n\n      <div className=\"w-full flex items-center gap-[36px] my-1.5\">\n        {[\"iam\", \"sessionToken\"].includes(connectionMethod) && (\n          <>\n            <div className=\"flex flex-col w-60\">\n              <label className=\"text-white text-sm font-semibold block mb-3\">\n                AWS Bedrock IAM Access ID\n              </label>\n              <input\n                type=\"password\"\n                name=\"AwsBedrockLLMAccessKeyId\"\n                className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                placeholder=\"AWS Bedrock IAM User Access ID\"\n                defaultValue={\n                  settings?.AwsBedrockLLMAccessKeyId ? \"*\".repeat(20) : \"\"\n                }\n                required={true}\n                autoComplete=\"off\"\n                spellCheck={false}\n              />\n            </div>\n            <div className=\"flex flex-col w-60\">\n              <label className=\"text-white text-sm font-semibold block mb-3\">\n                AWS Bedrock IAM Access Key\n              </label>\n              <input\n                type=\"password\"\n                name=\"AwsBedrockLLMAccessKey\"\n                className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                placeholder=\"AWS Bedrock IAM User Access Key\"\n                defaultValue={\n                  settings?.AwsBedrockLLMAccessKey ? \"*\".repeat(20) : \"\"\n                }\n                required={true}\n                autoComplete=\"off\"\n                spellCheck={false}\n              />\n            </div>\n          </>\n        )}\n        {connectionMethod === \"sessionToken\" && (\n          <div className=\"flex flex-col w-60\">\n            <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n              AWS Bedrock Session Token\n            </label>\n            <input\n              type=\"password\"\n              name=\"AwsBedrockLLMSessionToken\"\n              className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"AWS Bedrock Session Token\"\n              defaultValue={\n                settings?.AwsBedrockLLMSessionToken ? \"*\".repeat(20) : \"\"\n              }\n              required={true}\n              autoComplete=\"off\"\n              spellCheck={false}\n            />\n          </div>\n        )}\n        {connectionMethod === \"apiKey\" && (\n          <div className=\"flex flex-col w-60\">\n            <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n              AWS Bedrock API Key\n            </label>\n            <input\n              type=\"password\"\n              name=\"AwsBedrockLLMAPIKey\"\n              className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"AWS Bedrock API Key\"\n              defaultValue={settings?.AwsBedrockLLMAPIKey ? \"*\".repeat(20) : \"\"}\n              required={true}\n              autoComplete=\"off\"\n              spellCheck={false}\n            />\n          </div>\n        )}\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            AWS region\n          </label>\n          <select\n            name=\"AwsBedrockLLMRegion\"\n            defaultValue={settings?.AwsBedrockLLMRegion || \"us-west-2\"}\n            required={true}\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          >\n            {AWS_REGIONS.map((region) => {\n              return (\n                <option key={region.code} value={region.code}>\n                  {region.name} ({region.code})\n                </option>\n              );\n            })}\n          </select>\n        </div>\n      </div>\n\n      <div className=\"w-full flex items-center gap-[36px] my-1.5\">\n        {!settings?.credentialsOnly && (\n          <>\n            <div className=\"flex flex-col w-60\">\n              <label className=\"text-white text-sm font-semibold block mb-3\">\n                Model ID\n              </label>\n              <input\n                type=\"text\"\n                name=\"AwsBedrockLLMModel\"\n                className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                placeholder=\"Model id from AWS eg: meta.llama3.1-v0.1\"\n                defaultValue={settings?.AwsBedrockLLMModel}\n                required={true}\n                autoComplete=\"off\"\n                spellCheck={false}\n              />\n            </div>\n            <div className=\"flex flex-col w-60\">\n              <label className=\"text-white text-sm font-semibold block mb-3\">\n                Model context window\n              </label>\n              <input\n                type=\"number\"\n                name=\"AwsBedrockLLMTokenLimit\"\n                className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                placeholder=\"Content window limit (eg: 8192)\"\n                min={1}\n                onScroll={(e) => e.target.blur()}\n                defaultValue={settings?.AwsBedrockLLMTokenLimit}\n                required={true}\n                autoComplete=\"off\"\n              />\n            </div>\n            <div className=\"flex flex-col w-60\">\n              <label className=\"text-white text-sm font-semibold block mb-3\">\n                Model max output tokens\n              </label>\n              <input\n                type=\"number\"\n                name=\"AwsBedrockLLMMaxOutputTokens\"\n                className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                placeholder=\"Max output tokens (eg: 4096)\"\n                min={1}\n                onScroll={(e) => e.target.blur()}\n                defaultValue={settings?.AwsBedrockLLMMaxOutputTokens}\n                required={true}\n                autoComplete=\"off\"\n              />\n            </div>\n          </>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/AwsBedrockLLMOptions/regions.js",
    "content": "export const AWS_REGIONS = [\n  {\n    name: \"N. Virginia\",\n    full_name: \"US East (N. Virginia)\",\n    code: \"us-east-1\",\n    public: true,\n    zones: [\n      \"us-east-1a\",\n      \"us-east-1b\",\n      \"us-east-1c\",\n      \"us-east-1d\",\n      \"us-east-1e\",\n      \"us-east-1f\",\n    ],\n  },\n  {\n    name: \"Ohio\",\n    full_name: \"US East (Ohio)\",\n    code: \"us-east-2\",\n    public: true,\n    zones: [\"us-east-2a\", \"us-east-2b\", \"us-east-2c\"],\n  },\n  {\n    name: \"N. California\",\n    full_name: \"US West (N. California)\",\n    code: \"us-west-1\",\n    public: true,\n    zone_limit: 2,\n    zones: [\"us-west-1a\", \"us-west-1b\", \"us-west-1c\"],\n  },\n  {\n    name: \"Oregon\",\n    full_name: \"US West (Oregon)\",\n    code: \"us-west-2\",\n    public: true,\n    zones: [\"us-west-2a\", \"us-west-2b\", \"us-west-2c\", \"us-west-2d\"],\n  },\n  {\n    name: \"GovCloud West\",\n    full_name: \"AWS GovCloud (US)\",\n    code: \"us-gov-west-1\",\n    public: false,\n    zones: [\"us-gov-west-1a\", \"us-gov-west-1b\", \"us-gov-west-1c\"],\n  },\n  {\n    name: \"GovCloud East\",\n    full_name: \"AWS GovCloud (US-East)\",\n    code: \"us-gov-east-1\",\n    public: false,\n    zones: [\"us-gov-east-1a\", \"us-gov-east-1b\", \"us-gov-east-1c\"],\n  },\n  {\n    name: \"Canada\",\n    full_name: \"Canada (Central)\",\n    code: \"ca-central-1\",\n    public: true,\n    zones: [\"ca-central-1a\", \"ca-central-1b\", \"ca-central-1c\", \"ca-central-1d\"],\n  },\n  {\n    name: \"Stockholm\",\n    full_name: \"EU (Stockholm)\",\n    code: \"eu-north-1\",\n    public: true,\n    zones: [\"eu-north-1a\", \"eu-north-1b\", \"eu-north-1c\"],\n  },\n  {\n    name: \"Ireland\",\n    full_name: \"EU (Ireland)\",\n    code: \"eu-west-1\",\n    public: true,\n    zones: [\"eu-west-1a\", \"eu-west-1b\", \"eu-west-1c\"],\n  },\n  {\n    name: \"London\",\n    full_name: \"EU (London)\",\n    code: \"eu-west-2\",\n    public: true,\n    zones: [\"eu-west-2a\", \"eu-west-2b\", \"eu-west-2c\"],\n  },\n  {\n    name: \"Paris\",\n    full_name: \"EU (Paris)\",\n    code: \"eu-west-3\",\n    public: true,\n    zones: [\"eu-west-3a\", \"eu-west-3b\", \"eu-west-3c\"],\n  },\n  {\n    name: \"Frankfurt\",\n    full_name: \"EU (Frankfurt)\",\n    code: \"eu-central-1\",\n    public: true,\n    zones: [\"eu-central-1a\", \"eu-central-1b\", \"eu-central-1c\"],\n  },\n  {\n    name: \"Milan\",\n    full_name: \"EU (Milan)\",\n    code: \"eu-south-1\",\n    public: true,\n    zones: [\"eu-south-1a\", \"eu-south-1b\", \"eu-south-1c\"],\n  },\n  {\n    name: \"Cape Town\",\n    full_name: \"Africa (Cape Town)\",\n    code: \"af-south-1\",\n    public: true,\n    zones: [\"af-south-1a\", \"af-south-1b\", \"af-south-1c\"],\n  },\n  {\n    name: \"Tokyo\",\n    full_name: \"Asia Pacific (Tokyo)\",\n    code: \"ap-northeast-1\",\n    public: true,\n    zone_limit: 3,\n    zones: [\n      \"ap-northeast-1a\",\n      \"ap-northeast-1b\",\n      \"ap-northeast-1c\",\n      \"ap-northeast-1d\",\n    ],\n  },\n  {\n    name: \"Seoul\",\n    full_name: \"Asia Pacific (Seoul)\",\n    code: \"ap-northeast-2\",\n    public: true,\n    zones: [\n      \"ap-northeast-2a\",\n      \"ap-northeast-2b\",\n      \"ap-northeast-2c\",\n      \"ap-northeast-2d\",\n    ],\n  },\n  {\n    name: \"Osaka\",\n    full_name: \"Asia Pacific (Osaka-Local)\",\n    code: \"ap-northeast-3\",\n    public: true,\n    zones: [\"ap-northeast-3a\", \"ap-northeast-3b\", \"ap-northeast-3c\"],\n  },\n  {\n    name: \"Singapore\",\n    full_name: \"Asia Pacific (Singapore)\",\n    code: \"ap-southeast-1\",\n    public: true,\n    zones: [\"ap-southeast-1a\", \"ap-southeast-1b\", \"ap-southeast-1c\"],\n  },\n  {\n    name: \"Sydney\",\n    full_name: \"Asia Pacific (Sydney)\",\n    code: \"ap-southeast-2\",\n    public: true,\n    zones: [\"ap-southeast-2a\", \"ap-southeast-2b\", \"ap-southeast-2c\"],\n  },\n  {\n    name: \"Jakarta\",\n    full_name: \"Asia Pacific (Jakarta)\",\n    code: \"ap-southeast-3\",\n    public: true,\n    zones: [\"ap-southeast-3a\", \"ap-southeast-3b\", \"ap-southeast-3c\"],\n  },\n  {\n    name: \"Hong Kong\",\n    full_name: \"Asia Pacific (Hong Kong)\",\n    code: \"ap-east-1\",\n    public: true,\n    zones: [\"ap-east-1a\", \"ap-east-1b\", \"ap-east-1c\"],\n  },\n  {\n    name: \"Mumbai\",\n    full_name: \"Asia Pacific (Mumbai)\",\n    code: \"ap-south-1\",\n    public: true,\n    zones: [\"ap-south-1a\", \"ap-south-1b\", \"ap-south-1c\"],\n  },\n  {\n    name: \"São Paulo\",\n    full_name: \"South America (São Paulo)\",\n    code: \"sa-east-1\",\n    public: true,\n    zone_limit: 2,\n    zones: [\"sa-east-1a\", \"sa-east-1b\", \"sa-east-1c\"],\n  },\n  {\n    name: \"Bahrain\",\n    full_name: \"Middle East (Bahrain)\",\n    code: \"me-south-1\",\n    public: true,\n    zones: [\"me-south-1a\", \"me-south-1b\", \"me-south-1c\"],\n  },\n  {\n    name: \"UAE\",\n    full_name: \"Middle East (UAE)\",\n    code: \"me-central-1\",\n    public: true,\n    zones: [\"me-central-1a\", \"me-central-1b\", \"me-central-1c\"],\n  },\n  {\n    name: \"Beijing\",\n    full_name: \"China (Beijing)\",\n    code: \"cn-north-1\",\n    public: false,\n    zones: [\"cn-north-1a\", \"cn-north-1b\", \"cn-north-1c\"],\n  },\n  {\n    name: \"Ningxia\",\n    full_name: \"China (Ningxia)\",\n    code: \"cn-northwest-1\",\n    public: false,\n    zones: [\"cn-northwest-1a\", \"cn-northwest-1b\", \"cn-northwest-1c\"],\n  },\n];\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/AzureAiOptions/index.jsx",
    "content": "import { Info } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Tooltip } from \"react-tooltip\";\n\nexport default function AzureAiOptions({ settings }) {\n  const { t } = useTranslation();\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7 mt-1.5\">\n      <div className=\"w-full flex items-center gap-[36px]\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            {t(\"llm.providers.azure_openai.azure_service_endpoint\")}\n          </label>\n          <input\n            type=\"url\"\n            name=\"AzureOpenAiEndpoint\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"https://my-azure.openai.azure.com\"\n            defaultValue={settings?.AzureOpenAiEndpoint}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            {t(\"llm.providers.azure_openai.api_key\")}\n          </label>\n          <input\n            type=\"password\"\n            name=\"AzureOpenAiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Azure OpenAI API Key\"\n            defaultValue={settings?.AzureOpenAiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            {t(\"llm.providers.azure_openai.chat_deployment_name\")}\n          </label>\n          <input\n            type=\"text\"\n            name=\"AzureOpenAiModelPref\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Azure OpenAI chat model deployment name\"\n            defaultValue={settings?.AzureOpenAiModelPref}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n\n      <div className=\"w-full flex items-center gap-[36px]\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            {t(\"llm.providers.azure_openai.chat_model_token_limit\")}\n          </label>\n          <select\n            name=\"AzureOpenAiTokenLimit\"\n            defaultValue={settings?.AzureOpenAiTokenLimit || 4096}\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            required={true}\n          >\n            <option value={4096}>4,096 (gpt-3.5-turbo)</option>\n            <option value={16384}>16,384 (gpt-3.5-16k)</option>\n            <option value={8192}>8,192 (gpt-4)</option>\n            <option value={32768}>32,768 (gpt-4-32k)</option>\n            <option value={128000}>\n              128,000 (gpt-4-turbo,gpt-4o,gpt-4o-mini,o1-mini)\n            </option>\n            <option value={200000}>200,000 (o1,o1-pro,o3-mini)</option>\n            <option value={1047576}>1,047,576 (gpt-4.1)</option>\n          </select>\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <div className=\"flex items-center gap-1 mb-3\">\n            <label className=\"text-white text-sm font-semibold block\">\n              {t(\"llm.providers.azure_openai.model_type\")}\n            </label>\n            <Tooltip\n              id=\"azure-openai-model-type\"\n              place=\"top\"\n              delayShow={300}\n              className=\"tooltip !text-xs !opacity-100\"\n              style={{\n                maxWidth: \"250px\",\n                whiteSpace: \"normal\",\n                wordWrap: \"break-word\",\n              }}\n            />\n            <div\n              type=\"button\"\n              className=\"text-theme-text-secondary cursor-pointer hover:bg-theme-bg-primary flex items-center justify-center rounded-full\"\n              data-tooltip-id=\"azure-openai-model-type\"\n              data-tooltip-place=\"top\"\n              data-tooltip-content={t(\n                \"llm.providers.azure_openai.model_type_tooltip\"\n              )}\n            >\n              <Info size={18} className=\"text-theme-text-secondary\" />\n            </div>\n          </div>\n          <select\n            name=\"AzureOpenAiModelType\"\n            defaultValue={settings?.AzureOpenAiModelType || \"default\"}\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            required={true}\n          >\n            <option value=\"default\">\n              {t(\"llm.providers.azure_openai.default\")}\n            </option>\n            <option value=\"reasoning\">\n              {t(\"llm.providers.azure_openai.reasoning\")}\n            </option>\n          </select>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/CohereAiOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function CohereAiOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.CohereApiKey);\n  const [cohereApiKey, setCohereApiKey] = useState(settings?.CohereApiKey);\n\n  return (\n    <div className=\"w-full flex flex-col\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Cohere API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"CohereApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Cohere API Key\"\n            defaultValue={settings?.CohereApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={(e) => setInputValue(e.target.value)}\n            onBlur={() => setCohereApiKey(inputValue)}\n          />\n        </div>\n        {!settings?.credentialsOnly && (\n          <CohereModelSelection settings={settings} apiKey={cohereApiKey} />\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction CohereModelSelection({ apiKey, settings }) {\n  const [models, setModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!apiKey) {\n        setModels([]);\n        setLoading(true);\n        return;\n      }\n\n      setLoading(true);\n      const { models } = await System.customModels(\n        \"cohere\",\n        typeof apiKey === \"boolean\" ? null : apiKey\n      );\n      setModels(models || []);\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"CohereModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"CohereModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {models.map((model) => (\n          <option\n            key={model.id}\n            value={model.id}\n            selected={settings?.CohereModelPref === model.id}\n          >\n            {model.name}\n          </option>\n        ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/CometApiLLMOptions/index.jsx",
    "content": "import System from \"@/models/system\";\nimport { CaretDown, CaretUp } from \"@phosphor-icons/react\";\nimport { useState, useEffect } from \"react\";\n\nexport default function CometApiLLMOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-start gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n            CometAPI API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"CometApiLLMApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"CometAPI API Key\"\n            defaultValue={settings?.CometApiLLMApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        {!settings?.credentialsOnly && (\n          <CometApiModelSelection settings={settings} />\n        )}\n      </div>\n      <AdvancedControls settings={settings} />\n    </div>\n  );\n}\n\nfunction AdvancedControls({ settings }) {\n  const [showAdvancedControls, setShowAdvancedControls] = useState(false);\n\n  return (\n    <div className=\"flex flex-col gap-y-4\">\n      <div className=\"flex justify-start\">\n        <button\n          type=\"button\"\n          onClick={() => setShowAdvancedControls(!showAdvancedControls)}\n          className=\"border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm\"\n        >\n          {showAdvancedControls ? \"Hide\" : \"Show\"} advanced settings\n          {showAdvancedControls ? (\n            <CaretUp size={14} className=\"ml-1\" />\n          ) : (\n            <CaretDown size={14} className=\"ml-1\" />\n          )}\n        </button>\n      </div>\n      <div hidden={!showAdvancedControls}>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n            Stream Timeout (ms)\n          </label>\n          <input\n            type=\"number\"\n            name=\"CometApiLLMTimeout\"\n            className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Timeout value between token responses to auto-timeout the stream\"\n            defaultValue={settings?.CometApiLLMTimeout ?? 3_000}\n            autoComplete=\"off\"\n            onScroll={(e) => e.target.blur()}\n            min={500}\n            step={1}\n          />\n          <p className=\"text-xs leading-[18px] font-base text-theme-text-primary text-opacity-60 mt-2\">\n            Timeout value between token responses to auto-timeout the stream.\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction CometApiModelSelection({ settings }) {\n  // TODO: For now, CometAPI models list is noisy; show a flat, deduped list without grouping.\n  // Revisit after CometAPI model list API provides better categorization/metadata.\n  const [models, setModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models: fetched = [] } = await System.customModels(\"cometapi\");\n      if (fetched?.length > 0) {\n        // De-duplicate by id (case-insensitive) and sort by name for readability\n        const seen = new Set();\n        const unique = [];\n        for (const m of fetched) {\n          const key = String(m.id || m.name || \"\").toLowerCase();\n          if (!seen.has(key)) {\n            seen.add(key);\n            unique.push(m);\n          }\n        }\n        unique.sort((a, b) =>\n          String(a.name || a.id).localeCompare(String(b.name || b.id))\n        );\n        setModels(unique);\n      } else {\n        setModels([]);\n      }\n      setLoading(false);\n    }\n    findCustomModels();\n  }, []);\n\n  if (loading || models.length === 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <input\n          type=\"text\"\n          name=\"CometApiLLMModelPref\"\n          className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg block w-full p-2.5\"\n          placeholder=\"-- loading available models --\"\n          disabled\n        />\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <input\n        type=\"text\"\n        name=\"CometApiLLMModelPref\"\n        list=\"cometapi-models-list\"\n        required\n        className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg block w-full p-2.5\"\n        placeholder=\"Type or select a model\"\n        defaultValue={settings?.CometApiLLMModelPref || \"\"}\n        autoComplete=\"off\"\n        spellCheck={false}\n      />\n      <datalist id=\"cometapi-models-list\">\n        {models.map((model) => (\n          <option key={model.id} value={model.id}>\n            {model.name}\n          </option>\n        ))}\n      </datalist>\n      <p className=\"text-xs leading-[18px] font-base text-theme-text-primary text-opacity-60 mt-2\">\n        You can type the model id directly or pick from suggestions.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/DPAISOptions/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { CaretDown, CaretUp } from \"@phosphor-icons/react\";\nimport System from \"@/models/system\";\nimport PreLoader from \"@/components/Preloader\";\nimport { DPAIS_COMMON_URLS } from \"@/utils/constants\";\nimport useProviderEndpointAutoDiscovery from \"@/hooks/useProviderEndpointAutoDiscovery\";\n\nexport default function DellProAIStudioOptions({ settings }) {\n  const {\n    autoDetecting: loading,\n    basePath,\n    basePathValue,\n    showAdvancedControls,\n    setShowAdvancedControls,\n    handleAutoDetectClick,\n  } = useProviderEndpointAutoDiscovery({\n    provider: \"dpais\",\n    initialBasePath: settings?.DellProAiStudioBasePath,\n    ENDPOINTS: DPAIS_COMMON_URLS,\n  });\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        {!settings?.credentialsOnly && (\n          <>\n            <DellProAiStudioModelSelection\n              settings={settings}\n              basePath={basePath.value}\n            />\n            <div className=\"flex flex-col w-60\">\n              <label className=\"text-white text-sm font-semibold block mb-2\">\n                Model context window\n              </label>\n              <input\n                type=\"number\"\n                name=\"DellProAiStudioTokenLimit\"\n                className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                placeholder=\"4096\"\n                min={1}\n                onScroll={(e) => e.target.blur()}\n                defaultValue={settings?.DellProAiStudioTokenLimit}\n                required={true}\n                autoComplete=\"off\"\n              />\n            </div>\n          </>\n        )}\n      </div>\n      <div className=\"flex justify-start mt-4\">\n        <button\n          onClick={(e) => {\n            e.preventDefault();\n            setShowAdvancedControls(!showAdvancedControls);\n          }}\n          className=\"border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm\"\n        >\n          {showAdvancedControls ? \"Hide\" : \"Show\"} advanced settings\n          {showAdvancedControls ? (\n            <CaretUp size={14} className=\"ml-1\" />\n          ) : (\n            <CaretDown size={14} className=\"ml-1\" />\n          )}\n        </button>\n      </div>\n      <div hidden={!showAdvancedControls}>\n        <div className=\"w-full flex items-center gap-4\">\n          <div className=\"flex flex-col w-fit\">\n            <div className=\"flex justify-between items-center mb-2 gap-x-2\">\n              <label className=\"text-white text-sm font-semibold\">\n                Dell Pro AI Studio Base URL\n              </label>\n              {loading ? (\n                <PreLoader size=\"6\" />\n              ) : (\n                <>\n                  {!basePathValue.value && (\n                    <button\n                      onClick={handleAutoDetectClick}\n                      className=\"bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]\"\n                    >\n                      Auto-Detect\n                    </button>\n                  )}\n                </>\n              )}\n            </div>\n            <input\n              type=\"url\"\n              name=\"DellProAiStudioBasePath\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"http://localhost:8553/v1/openai\"\n              value={basePathValue.value}\n              required={true}\n              autoComplete=\"off\"\n              spellCheck={false}\n              onChange={basePath.onChange}\n              onBlur={basePath.onBlur}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction DellProAiStudioModelSelection({ settings, basePath = null }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!basePath) {\n        setCustomModels([]);\n        setLoading(false);\n        return;\n      }\n      setLoading(true);\n      const { models } = await System.customModels(\n        \"dpais\",\n        null,\n        basePath,\n        2_000\n      );\n      setCustomModels(models || []);\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [basePath]);\n\n  if (loading || customModels.length == 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-2\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"DellProAiStudioModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-2\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"DellProAiStudioModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Your loaded models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings.DellProAiStudioModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/DeepSeekOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function DeepSeekOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.DeepSeekApiKey);\n  const [deepSeekApiKey, setDeepSeekApiKey] = useState(\n    settings?.DeepSeekApiKey\n  );\n\n  return (\n    <div className=\"flex gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"DeepSeekApiKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"DeepSeek API Key\"\n          defaultValue={settings?.DeepSeekApiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n          onChange={(e) => setInputValue(e.target.value)}\n          onBlur={() => setDeepSeekApiKey(inputValue)}\n        />\n      </div>\n      {!settings?.credentialsOnly && (\n        <DeepSeekModelSelection settings={settings} apiKey={deepSeekApiKey} />\n      )}\n    </div>\n  );\n}\n\nfunction DeepSeekModelSelection({ apiKey, settings }) {\n  const [models, setModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!apiKey) {\n        setModels([]);\n        setLoading(true);\n        return;\n      }\n\n      setLoading(true);\n      const { models } = await System.customModels(\n        \"deepseek\",\n        typeof apiKey === \"boolean\" ? null : apiKey\n      );\n      setModels(models || []);\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"DeepSeekModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"DeepSeekModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {models.map((model) => (\n          <option\n            key={model.id}\n            value={model.id}\n            selected={settings?.DeepSeekModelPref === model.id}\n          >\n            {model.name}\n          </option>\n        ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/DockerModelRunnerOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\nimport useProviderEndpointAutoDiscovery from \"@/hooks/useProviderEndpointAutoDiscovery\";\nimport { CircleNotch, Info } from \"@phosphor-icons/react\";\nimport strDistance from \"js-levenshtein\";\nimport { LLM_PREFERENCE_CHANGED_EVENT } from \"@/pages/GeneralSettings/LLMPreference\";\nimport { DOCKER_MODEL_RUNNER_COMMON_URLS } from \"@/utils/constants\";\nimport { Tooltip } from \"react-tooltip\";\nimport { Link } from \"react-router-dom\";\nimport ModelTable from \"@/components/lib/ModelTable\";\nimport ModelTableLayout from \"@/components/lib/ModelTable/layout\";\nimport ModelTableLoadingSkeleton from \"@/components/lib/ModelTable/loading\";\nimport DMRUtils from \"@/models/utils/dmrUtils\";\nimport showToast from \"@/utils/toast\";\n\nexport default function DockerModelRunnerOptions({ settings }) {\n  const {\n    autoDetecting: loading,\n    basePath,\n    basePathValue,\n    handleAutoDetectClick,\n  } = useProviderEndpointAutoDiscovery({\n    provider: \"docker-model-runner\",\n    initialBasePath: settings?.DockerModelRunnerBasePath,\n    ENDPOINTS: DOCKER_MODEL_RUNNER_COMMON_URLS,\n  });\n  const [selectedModelId, setSelectedModelId] = useState(\n    settings?.DockerModelRunnerModelPref\n  );\n  const [maxTokens, setMaxTokens] = useState(\n    settings?.DockerModelRunnerModelTokenLimit || 4096\n  );\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"flex gap-[36px] mt-1.5 flex-wrap\">\n        <div className=\"flex flex-col w-60\">\n          <div className=\"flex items-center gap-1 mb-3\">\n            <div className=\"flex justify-between items-center gap-x-2\">\n              <label className=\"text-white text-sm font-semibold\">\n                Base URL\n              </label>\n              {loading ? (\n                <CircleNotch className=\"w-4 h-4 text-theme-text-secondary animate-spin\" />\n              ) : (\n                <>\n                  {!basePathValue.value && (\n                    <button\n                      onClick={handleAutoDetectClick}\n                      className=\"bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]\"\n                    >\n                      Auto-Detect\n                    </button>\n                  )}\n                </>\n              )}\n            </div>\n            <Tooltip\n              id=\"docker-model-runner-base-url\"\n              place=\"top\"\n              delayShow={300}\n              delayHide={800}\n              clickable={true}\n              className=\"tooltip !text-xs !opacity-100 z-99\"\n              style={{\n                maxWidth: \"250px\",\n                whiteSpace: \"normal\",\n                wordWrap: \"break-word\",\n              }}\n            >\n              Enter the URL where the Docker Model Runner is running.\n              <br />\n              <br />\n              You <b>must</b> have enabled the Docker Model Runner TCP support\n              for this to work.\n              <br />\n              <br />\n              <Link\n                to=\"https://docs.docker.com/ai/model-runner/get-started/#docker-desktop\"\n                target=\"_blank\"\n                className=\"text-blue-500 hover:underline\"\n              >\n                Learn more &rarr;\n              </Link>\n            </Tooltip>\n            <div\n              className=\"text-theme-text-secondary cursor-pointer hover:bg-theme-bg-primary flex items-center justify-center rounded-full\"\n              data-tooltip-id=\"docker-model-runner-base-url\"\n              data-tooltip-place=\"top\"\n              data-tooltip-delay-hide={800}\n            >\n              <Info size={18} className=\"text-theme-text-secondary\" />\n            </div>\n          </div>\n\n          <input\n            type=\"url\"\n            name=\"DockerModelRunnerBasePath\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"http://localhost:12434/engines/llama.cpp/v1\"\n            value={basePathValue.value}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={basePath.onChange}\n            onBlur={basePath.onBlur}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <div className=\"flex items-center gap-1 mb-3\">\n            <label className=\"text-white text-sm font-semibold block\">\n              Model context window\n            </label>\n            <Tooltip\n              id=\"docker-model-runner-model-context-window\"\n              place=\"top\"\n              delayShow={300}\n              delayHide={800}\n              clickable={true}\n              className=\"tooltip !text-xs !opacity-100 z-99\"\n              style={{\n                maxWidth: \"350px\",\n                whiteSpace: \"normal\",\n                wordWrap: \"break-word\",\n              }}\n            >\n              The maximum number of tokens that can be used for a model context\n              window.\n              <br />\n              <br />\n              To set the context window limit for a model, you can use the{\" \"}\n              <code>docker run</code> command with the{\" \"}\n              <code>--context-window</code> parameter.\n              <br />\n              <br />\n              <code>\n                docker model configure --context-size {maxTokens || 8192}{\" \"}\n                {selectedModelId ?? \"ai/qwen3:latest\"}\n              </code>\n              <br />\n              <br />\n              <Link\n                to=\"https://docs.docker.com/ai/model-runner/#context-size\"\n                target=\"_blank\"\n                className=\"text-blue-500 hover:underline\"\n              >\n                Learn more &rarr;\n              </Link>\n            </Tooltip>\n            <div\n              className=\"text-theme-text-secondary cursor-pointer hover:bg-theme-bg-primary flex items-center justify-center rounded-full\"\n              data-tooltip-id=\"docker-model-runner-model-context-window\"\n              data-tooltip-place=\"top\"\n              data-tooltip-delay-hide={800}\n            >\n              <Info size={18} className=\"text-theme-text-secondary\" />\n            </div>\n          </div>\n          <input\n            type=\"number\"\n            name=\"DockerModelRunnerModelTokenLimit\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"4096\"\n            min={1}\n            value={maxTokens}\n            onChange={(e) => setMaxTokens(Number(e.target.value))}\n            onScroll={(e) => e.target.blur()}\n            required={true}\n            autoComplete=\"off\"\n          />\n        </div>\n        <DockerModelRunnerModelSelection\n          selectedModelId={selectedModelId}\n          setSelectedModelId={setSelectedModelId}\n          basePath={basePathValue.value}\n        />\n      </div>\n    </div>\n  );\n}\n\nfunction DockerModelRunnerModelSelection({\n  selectedModelId,\n  setSelectedModelId,\n  basePath = null,\n}) {\n  const [customModels, setCustomModels] = useState([]);\n  const [filteredModels, setFilteredModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n\n  async function fetchModels() {\n    if (!basePath) {\n      setCustomModels([]);\n      setFilteredModels([]);\n      setLoading(false);\n      setSearchQuery(\"\");\n      return;\n    }\n    setLoading(true);\n    const { models } = await System.customModels(\n      \"docker-model-runner\",\n      null,\n      basePath\n    );\n    setCustomModels(models || []);\n    setFilteredModels(models || []);\n    setSearchQuery(\"\");\n    setLoading(false);\n  }\n\n  useEffect(() => {\n    fetchModels();\n  }, [basePath]);\n\n  useEffect(() => {\n    if (!searchQuery || !customModels.length) {\n      setFilteredModels(customModels || []);\n      return;\n    }\n\n    const normalizedSearchQuery = searchQuery.toLowerCase().trim();\n    const filteredModels = new Map();\n\n    customModels.forEach((model) => {\n      const modelNameNormalized = model.name.toLowerCase();\n      const modelOrganizationNormalized = model.organization.toLowerCase();\n\n      if (modelNameNormalized.startsWith(normalizedSearchQuery))\n        filteredModels.set(model.id, model);\n      if (modelOrganizationNormalized.startsWith(normalizedSearchQuery))\n        filteredModels.set(model.id, model);\n      if (strDistance(modelNameNormalized, normalizedSearchQuery) <= 2)\n        filteredModels.set(model.id, model);\n      if (strDistance(modelOrganizationNormalized, normalizedSearchQuery) <= 2)\n        filteredModels.set(model.id, model);\n    });\n\n    setFilteredModels(Array.from(filteredModels.values()));\n  }, [searchQuery]);\n\n  async function downloadModel(modelId, fileSize, progressCallback) {\n    try {\n      if (\n        !window.confirm(\n          `Are you sure you want to download this model? It is ${fileSize} in size and may take a while to download.`\n        )\n      )\n        return;\n      const { success, error } = await DMRUtils.downloadModel(\n        modelId,\n        basePath,\n        progressCallback\n      );\n      if (!success)\n        throw new Error(\n          error || \"An error occurred while downloading the model\"\n        );\n      progressCallback(100);\n      handleSetActiveModel(modelId);\n\n      const existingModels = [...customModels];\n      const newModel = existingModels.find((model) => model.id === modelId);\n      if (newModel) {\n        newModel.downloaded = true;\n        setCustomModels(existingModels);\n        setFilteredModels(existingModels);\n        setSearchQuery(\"\");\n      }\n    } catch (e) {\n      console.error(\"Error downloading model:\", e);\n      showToast(\n        e.message || \"An error occurred while downloading the model\",\n        \"error\",\n        { clear: true }\n      );\n    } finally {\n      setLoading(false);\n    }\n  }\n\n  function groupModelsByAlias(models) {\n    const mapping = new Map();\n    mapping.set(\"installed\", new Map());\n    mapping.set(\"not installed\", new Map());\n\n    const groupedModels = models.reduce((acc, model) => {\n      acc[model.organization] = acc[model.organization] || [];\n      acc[model.organization].push(model);\n      return acc;\n    }, {});\n\n    Object.entries(groupedModels).forEach(([organization, models]) => {\n      const hasInstalled = models.some((model) => model.downloaded);\n      if (hasInstalled) {\n        const installedModels = models.filter((model) => model.downloaded);\n        mapping\n          .get(\"installed\")\n          .set(\"Downloaded Models\", [\n            ...(mapping.get(\"installed\").get(\"Downloaded Models\") || []),\n            ...installedModels,\n          ]);\n      }\n      const tags = models.map((model) => ({\n        ...model,\n        name: model.name.split(\":\")[1],\n      }));\n      mapping.get(\"not installed\").set(organization, tags);\n    });\n\n    const orderedMap = new Map();\n    const installedMap = new Map();\n    mapping\n      .get(\"installed\")\n      .entries()\n      .forEach(([organization, models]) =>\n        installedMap.set(organization, models)\n      );\n    mapping\n      .get(\"not installed\")\n      .entries()\n      .forEach(([organization, models]) =>\n        orderedMap.set(organization, models)\n      );\n\n    // Sort the models by organization/creator name alphabetically but keep the installed models at the top\n    return Object.fromEntries(\n      Array.from(installedMap.entries())\n        .sort((a, b) => a[0].localeCompare(b[0]))\n        .concat(\n          Array.from(orderedMap.entries()).sort((a, b) =>\n            a[0].localeCompare(b[0])\n          )\n        )\n    );\n  }\n\n  function handleSetActiveModel(modelId) {\n    if (modelId === selectedModelId) return;\n    setSelectedModelId(modelId);\n    window.dispatchEvent(new Event(LLM_PREFERENCE_CHANGED_EVENT));\n  }\n\n  const groupedModels = groupModelsByAlias(filteredModels);\n  return (\n    <ModelTableLayout\n      fetchModels={fetchModels}\n      searchQuery={searchQuery}\n      setSearchQuery={setSearchQuery}\n      loading={loading}\n    >\n      <Tooltip\n        id=\"install-model-tooltip\"\n        place=\"top\"\n        className=\"tooltip !text-xs !opacity-100 z-99\"\n      />\n      <input\n        type=\"hidden\"\n        name=\"DockerModelRunnerModelPref\"\n        id=\"DockerModelRunnerModelPref\"\n        value={selectedModelId}\n      />\n      {loading ? (\n        <ModelTableLoadingSkeleton />\n      ) : filteredModels.length === 0 ? (\n        <div className=\"flex flex-col w-full gap-y-2 mt-4\">\n          <p className=\"text-theme-text-secondary text-sm\">No models found!</p>\n        </div>\n      ) : (\n        Object.entries(groupedModels).map(([alias, models]) => (\n          <ModelTable\n            key={alias}\n            alias={alias}\n            models={models}\n            setActiveModel={handleSetActiveModel}\n            downloadModel={downloadModel}\n            selectedModelId={selectedModelId}\n            ui={{\n              showRuntime: false,\n            }}\n          />\n        ))\n      )}\n    </ModelTableLayout>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/FireworksAiOptions/index.jsx",
    "content": "import System from \"@/models/system\";\nimport { useState, useEffect } from \"react\";\n\nexport default function FireworksAiOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.FireworksAiLLMApiKey);\n  const [fireworksAiApiKey, setFireworksAiApiKey] = useState(\n    settings?.FireworksAiLLMApiKey\n  );\n\n  return (\n    <div className=\"flex gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Fireworks AI API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"FireworksAiLLMApiKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"Fireworks AI API Key\"\n          defaultValue={settings?.FireworksAiLLMApiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n          onChange={(e) => setInputValue(e.target.value)}\n          onBlur={() => setFireworksAiApiKey(inputValue)}\n        />\n      </div>\n      {!settings?.credentialsOnly && (\n        <FireworksAiModelSelection\n          apiKey={fireworksAiApiKey}\n          settings={settings}\n        />\n      )}\n    </div>\n  );\n}\nfunction FireworksAiModelSelection({ apiKey, settings }) {\n  const [groupedModels, setGroupedModels] = useState({});\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models } = await System.customModels(\"fireworksai\", apiKey);\n\n      if (models?.length > 0) {\n        const modelsByOrganization = models.reduce((acc, model) => {\n          acc[model.organization] = acc[model.organization] || [];\n          acc[model.organization].push(model);\n          return acc;\n        }, {});\n\n        setGroupedModels(modelsByOrganization);\n      }\n\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading || Object.keys(groupedModels).length === 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"FireworksAiLLMModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"FireworksAiLLMModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {Object.keys(groupedModels)\n          .sort()\n          .map((organization) => (\n            <optgroup key={organization} label={organization}>\n              {groupedModels[organization].map((model) => (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.FireworksAiLLMModelPref === model.id}\n                >\n                  {model.name}\n                </option>\n              ))}\n            </optgroup>\n          ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/FoundryOptions/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function FoundryOptions({ settings }) {\n  const [models, setModels] = useState([]);\n  const [loading, setLoading] = useState(!!settings?.FoundryBasePath);\n  const [basePath, setBasePath] = useState(settings?.FoundryBasePath);\n  const [model, setModel] = useState(settings?.FoundryModelPref || \"\");\n\n  useEffect(() => {\n    setModel(settings?.FoundryModelPref || \"\");\n  }, [settings?.FoundryModelPref]);\n\n  useEffect(() => {\n    async function fetchModels() {\n      try {\n        setLoading(true);\n        if (!basePath) throw new Error(\"Base path is required\");\n        const { models, error } = await System.customModels(\n          \"foundry\",\n          null,\n          basePath\n        );\n        if (error) throw new Error(error);\n        setModels(models);\n      } catch (error) {\n        console.error(\"Error fetching Foundry models:\", error);\n        setModels([]);\n      } finally {\n        setLoading(false);\n      }\n    }\n    fetchModels();\n  }, [basePath]);\n\n  return (\n    <div className=\"flex flex-col gap-y-7\">\n      <div className=\"flex gap-[36px] mt-1.5 flex-wrap\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Base URL\n          </label>\n          <input\n            type=\"url\"\n            name=\"FoundryBasePath\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"eg: http://127.0.0.1:8080\"\n            defaultValue={settings?.FoundryBasePath}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={(e) => setBasePath(e.target.value)}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Chat Model\n          </label>\n          {loading ? (\n            <select\n              name=\"FoundryModelPref\"\n              required={true}\n              disabled={true}\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            >\n              <option>---- Loading ----</option>\n            </select>\n          ) : (\n            <select\n              name=\"FoundryModelPref\"\n              value={model}\n              onChange={(e) => setModel(e.target.value)}\n              required={true}\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            >\n              {models.length > 0 ? (\n                <>\n                  <option value=\"\">-- Select a model --</option>\n                  {models.map((model) => (\n                    <option key={model.id} value={model.id}>\n                      {model.id}\n                    </option>\n                  ))}\n                </>\n              ) : (\n                <option disabled value=\"\">\n                  No models found\n                </option>\n              )}\n            </select>\n          )}\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Model context window\n          </label>\n          <input\n            type=\"number\"\n            name=\"FoundryModelTokenLimit\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"4096\"\n            defaultValue={settings?.FoundryModelTokenLimit}\n            autoComplete=\"off\"\n            min={0}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/GeminiLLMOptions/index.jsx",
    "content": "import System from \"@/models/system\";\nimport { useEffect, useState } from \"react\";\n\nexport default function GeminiLLMOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.GeminiLLMApiKey);\n  const [geminiApiKey, setGeminiApiKey] = useState(settings?.GeminiLLMApiKey);\n\n  return (\n    <div className=\"w-full flex flex-col\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Google AI API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"GeminiLLMApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Google Gemini API Key\"\n            defaultValue={settings?.GeminiLLMApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={(e) => setInputValue(e.target.value)}\n            onBlur={() => setGeminiApiKey(inputValue)}\n          />\n        </div>\n\n        {!settings?.credentialsOnly && (\n          <>\n            <GeminiModelSelection apiKey={geminiApiKey} settings={settings} />\n            {/* \n            \n            Safety setting is not supported for Gemini yet due to the openai compatible Gemini API.\n            We are not using the generativeAPI endpoint and therefore cannot set the safety threshold.\n\n            <div className=\"flex flex-col w-60\">\n              <label className=\"text-white text-sm font-semibold block mb-3\">\n                Safety Setting\n              </label>\n              <select\n                name=\"GeminiSafetySetting\"\n                defaultValue={\n                  settings?.GeminiSafetySetting || \"BLOCK_MEDIUM_AND_ABOVE\"\n                }\n                required={true}\n                className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n              >\n                <option value=\"BLOCK_NONE\">None</option>\n                <option value=\"BLOCK_ONLY_HIGH\">Block few</option>\n                <option value=\"BLOCK_MEDIUM_AND_ABOVE\">\n                  Block some (default)\n                </option>\n                <option value=\"BLOCK_LOW_AND_ABOVE\">Block most</option>\n              </select>\n            </div> */}\n          </>\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction GeminiModelSelection({ apiKey, settings }) {\n  const [groupedModels, setGroupedModels] = useState({});\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models } = await System.customModels(\"gemini\", apiKey);\n\n      if (models?.length > 0) {\n        const modelsByOrganization = models.reduce((acc, model) => {\n          acc[model.experimental ? \"Experimental\" : \"Stable\"] =\n            acc[model.experimental ? \"Experimental\" : \"Stable\"] || [];\n          acc[model.experimental ? \"Experimental\" : \"Stable\"].push(model);\n          return acc;\n        }, {});\n        setGroupedModels(modelsByOrganization);\n      }\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"GeminiLLMModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"GeminiLLMModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {Object.keys(groupedModels)\n          .sort((a, b) => {\n            if (a === \"Stable\") return -1;\n            if (b === \"Stable\") return 1;\n            return a.localeCompare(b);\n          })\n          .map((organization) => (\n            <optgroup key={organization} label={organization}>\n              {groupedModels[organization].map((model) => (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.GeminiLLMModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              ))}\n            </optgroup>\n          ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/GenericOpenAiOptions/index.jsx",
    "content": "export default function GenericOpenAiOptions({ settings }) {\n  return (\n    <div className=\"flex flex-col gap-y-7\">\n      <div className=\"flex gap-[36px] mt-1.5 flex-wrap\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Base URL\n          </label>\n          <input\n            type=\"url\"\n            name=\"GenericOpenAiBasePath\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"eg: https://proxy.openai.com\"\n            defaultValue={settings?.GenericOpenAiBasePath}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"GenericOpenAiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Generic service API Key\"\n            defaultValue={settings?.GenericOpenAiKey ? \"*\".repeat(20) : \"\"}\n            required={false}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Chat Model Name\n          </label>\n          <input\n            type=\"text\"\n            name=\"GenericOpenAiModelPref\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Model id used for chat requests\"\n            defaultValue={settings?.GenericOpenAiModelPref}\n            required={true}\n            autoComplete=\"off\"\n          />\n        </div>\n      </div>\n      <div className=\"flex gap-[36px] flex-wrap\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Model context window\n          </label>\n          <input\n            type=\"number\"\n            name=\"GenericOpenAiTokenLimit\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Content window limit (eg: 4096)\"\n            min={1}\n            onScroll={(e) => e.target.blur()}\n            defaultValue={settings?.GenericOpenAiTokenLimit}\n            required={true}\n            autoComplete=\"off\"\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Max Tokens\n          </label>\n          <input\n            type=\"number\"\n            name=\"GenericOpenAiMaxTokens\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Max tokens per request (eg: 1024)\"\n            min={1}\n            defaultValue={settings?.GenericOpenAiMaxTokens || 1024}\n            required={true}\n            autoComplete=\"off\"\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/GiteeAIOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function GiteeAIOptions({ settings }) {\n  return (\n    <div className=\"flex gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"GiteeAIApiKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"GiteeAI API Key\"\n          defaultValue={settings?.GiteeAIApiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n        />\n      </div>\n      {!settings?.credentialsOnly && (\n        <>\n          <GiteeAIModelSelection settings={settings} />\n          <div className=\"flex flex-col w-60\">\n            <label className=\"text-white text-sm font-semibold block mb-2\">\n              Model context window\n            </label>\n            <input\n              type=\"number\"\n              name=\"GiteeAITokenLimit\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"Content window limit (eg: 8192)\"\n              min={1}\n              onScroll={(e) => e.target.blur()}\n              defaultValue={settings?.GiteeAITokenLimit}\n              required={true}\n              autoComplete=\"off\"\n            />\n          </div>\n        </>\n      )}\n    </div>\n  );\n}\n\nfunction GiteeAIModelSelection({ settings }) {\n  const [groupedModels, setGroupedModels] = useState({});\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models = [] } = await System.customModels(\"giteeai\");\n      if (models?.length > 0) {\n        const modelsByOrganization = models.reduce((acc, model) => {\n          acc[model.organization] = acc[model.organization] || [];\n          acc[model.organization].push(model);\n          return acc;\n        }, {});\n        setGroupedModels(modelsByOrganization);\n      }\n\n      setLoading(false);\n    }\n    findCustomModels();\n  }, []);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"GiteeAIModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"GiteeAIModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {Object.keys(groupedModels)\n          .sort()\n          .map((organization) => (\n            <optgroup key={organization} label={organization}>\n              {groupedModels[organization].map((model) => (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.GiteeAIModelPref === model.id}\n                >\n                  {model.name}\n                </option>\n              ))}\n            </optgroup>\n          ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/GroqAiOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function GroqAiOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.GroqApiKey);\n  const [apiKey, setApiKey] = useState(settings?.GroqApiKey);\n\n  return (\n    <div className=\"flex gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Groq API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"GroqApiKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"Groq API Key\"\n          defaultValue={settings?.GroqApiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n          onChange={(e) => setInputValue(e.target.value)}\n          onBlur={() => setApiKey(inputValue)}\n        />\n      </div>\n\n      {!settings?.credentialsOnly && (\n        <GroqAIModelSelection settings={settings} apiKey={apiKey} />\n      )}\n    </div>\n  );\n}\n\nfunction GroqAIModelSelection({ apiKey, settings }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!apiKey) {\n        setCustomModels([]);\n        setLoading(true);\n        return;\n      }\n\n      try {\n        setLoading(true);\n        const { models } = await System.customModels(\"groq\", apiKey);\n        setCustomModels(models || []);\n      } catch (error) {\n        console.error(\"Failed to fetch custom models:\", error);\n        setCustomModels([]);\n      } finally {\n        setLoading(false);\n      }\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"GroqModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            --loading available models--\n          </option>\n        </select>\n        <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n          Enter a valid API key to view all available models for your account.\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"GroqModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Available models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.GroqModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n      <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n        Select the GroqAI model you want to use for your conversations.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/HuggingFaceOptions/index.jsx",
    "content": "export default function HuggingFaceOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            HuggingFace Inference Endpoint\n          </label>\n          <input\n            type=\"url\"\n            name=\"HuggingFaceLLMEndpoint\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"https://example.endpoints.huggingface.cloud\"\n            defaultValue={settings?.HuggingFaceLLMEndpoint}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            HuggingFace Access Token\n          </label>\n          <input\n            type=\"password\"\n            name=\"HuggingFaceLLMAccessToken\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"HuggingFace Access Token\"\n            defaultValue={\n              settings?.HuggingFaceLLMAccessToken ? \"*\".repeat(20) : \"\"\n            }\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Model Token Limit\n          </label>\n          <input\n            type=\"number\"\n            name=\"HuggingFaceLLMTokenLimit\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"4096\"\n            min={1}\n            onScroll={(e) => e.target.blur()}\n            defaultValue={settings?.HuggingFaceLLMTokenLimit}\n            required={true}\n            autoComplete=\"off\"\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/KoboldCPPOptions/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport System from \"@/models/system\";\nimport PreLoader from \"@/components/Preloader\";\nimport { KOBOLDCPP_COMMON_URLS } from \"@/utils/constants\";\nimport { CaretDown, CaretUp } from \"@phosphor-icons/react\";\nimport useProviderEndpointAutoDiscovery from \"@/hooks/useProviderEndpointAutoDiscovery\";\n\nexport default function KoboldCPPOptions({ settings }) {\n  const {\n    autoDetecting: loading,\n    basePath,\n    basePathValue,\n    showAdvancedControls,\n    setShowAdvancedControls,\n    handleAutoDetectClick,\n  } = useProviderEndpointAutoDiscovery({\n    provider: \"koboldcpp\",\n    initialBasePath: settings?.KoboldCPPBasePath,\n    ENDPOINTS: KOBOLDCPP_COMMON_URLS,\n  });\n\n  const [tokenLimit, setTokenLimit] = useState(\n    settings?.KoboldCPPTokenLimit || 4096\n  );\n  const [maxTokens, setMaxTokens] = useState(\n    settings?.KoboldCPPMaxTokens || 2048\n  );\n\n  const handleTokenLimitChange = (e) => {\n    setTokenLimit(Number(e.target.value));\n  };\n\n  const handleMaxTokensChange = (e) => {\n    setMaxTokens(Number(e.target.value));\n  };\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-start gap-[36px] mt-1.5\">\n        <KoboldCPPModelSelection\n          settings={settings}\n          basePath={basePath.value}\n        />\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-2\">\n            Model context window\n          </label>\n          <input\n            type=\"number\"\n            name=\"KoboldCPPTokenLimit\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"4096\"\n            min={1}\n            value={tokenLimit}\n            onChange={handleTokenLimitChange}\n            onScroll={(e) => e.target.blur()}\n            required={true}\n            autoComplete=\"off\"\n          />\n          <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n            Maximum number of tokens for context and response.\n          </p>\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-2\">\n            Max response tokens\n          </label>\n          <input\n            type=\"number\"\n            name=\"KoboldCPPMaxTokens\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"2048\"\n            min={1}\n            value={maxTokens}\n            onChange={handleMaxTokensChange}\n            onScroll={(e) => e.target.blur()}\n            required={true}\n            autoComplete=\"off\"\n          />\n          <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n            Maximum number of tokens for the response.\n          </p>\n        </div>\n      </div>\n      <div className=\"flex justify-start mt-4\">\n        <button\n          onClick={(e) => {\n            e.preventDefault();\n            setShowAdvancedControls(!showAdvancedControls);\n          }}\n          className=\"border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm\"\n        >\n          {showAdvancedControls ? \"Hide\" : \"Show\"} Manual Endpoint Input\n          {showAdvancedControls ? (\n            <CaretUp size={14} className=\"ml-1\" />\n          ) : (\n            <CaretDown size={14} className=\"ml-1\" />\n          )}\n        </button>\n      </div>\n\n      <div hidden={!showAdvancedControls}>\n        <div className=\"w-full flex items-start gap-4\">\n          <div className=\"flex flex-col w-60\">\n            <div className=\"flex justify-between items-center mb-2\">\n              <label className=\"text-white text-sm font-semibold\">\n                KoboldCPP Base URL\n              </label>\n              {loading ? (\n                <PreLoader size=\"6\" />\n              ) : (\n                <>\n                  {!basePathValue.value && (\n                    <button\n                      onClick={handleAutoDetectClick}\n                      className=\"border-none bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]\"\n                    >\n                      Auto-Detect\n                    </button>\n                  )}\n                </>\n              )}\n            </div>\n            <input\n              type=\"url\"\n              name=\"KoboldCPPBasePath\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"http://127.0.0.1:5000/v1\"\n              value={basePathValue.value}\n              required={true}\n              autoComplete=\"off\"\n              spellCheck={false}\n              onChange={basePath.onChange}\n              onBlur={basePath.onBlur}\n            />\n            <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n              Enter the URL where KoboldCPP is running.\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction KoboldCPPModelSelection({ settings, basePath = null }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!basePath || !basePath.includes(\"/v1\")) {\n        setCustomModels([]);\n        setLoading(false);\n        return;\n      }\n      setLoading(true);\n      try {\n        const { models } = await System.customModels(\n          \"koboldcpp\",\n          null,\n          basePath\n        );\n        setCustomModels(models || []);\n      } catch (error) {\n        console.error(\"Failed to fetch custom models:\", error);\n        setCustomModels([]);\n      }\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [basePath]);\n\n  if (loading || customModels.length === 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-2\">\n          KoboldCPP Model\n        </label>\n        <select\n          name=\"KoboldCPPModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {basePath?.includes(\"/v1\")\n              ? \"--loading available models--\"\n              : \"Enter KoboldCPP URL first\"}\n          </option>\n        </select>\n        <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n          Select the KoboldCPP model you want to use. Models will load after\n          entering a valid KoboldCPP URL.\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-2\">\n        KoboldCPP Model\n      </label>\n      <select\n        name=\"KoboldCPPModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.map((model) => (\n          <option\n            key={model.id}\n            value={model.id}\n            selected={settings.KoboldCPPModelPref === model.id}\n          >\n            {model.id}\n          </option>\n        ))}\n      </select>\n      <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n        Choose the KoboldCPP model you want to use for your conversations.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/LLMItem/index.jsx",
    "content": "export default function LLMItem({\n  name,\n  value,\n  image,\n  description,\n  checked,\n  onClick,\n}) {\n  return (\n    <div\n      onClick={() => onClick(value)}\n      className={`w-full p-2 rounded-md hover:cursor-pointer hover:bg-theme-bg-secondary ${\n        checked ? \"bg-theme-bg-secondary\" : \"\"\n      }`}\n    >\n      <input\n        type=\"checkbox\"\n        value={value}\n        className=\"peer hidden\"\n        checked={checked}\n        readOnly={true}\n        formNoValidate={true}\n      />\n      <div className=\"flex gap-x-4 items-center\">\n        <img\n          src={image}\n          alt={`${name} logo`}\n          className=\"w-10 h-10 rounded-md\"\n        />\n        <div className=\"flex flex-col\">\n          <div className=\"text-sm font-semibold text-white\">{name}</div>\n          <div className=\"mt-1 text-xs text-description\">{description}</div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/LLMProviderOption/index.jsx",
    "content": "export default function LLMProviderOption({\n  name,\n  link,\n  description,\n  value,\n  image,\n  checked = false,\n  onClick,\n}) {\n  return (\n    <div onClick={() => onClick(value)}>\n      <input\n        type=\"checkbox\"\n        value={value}\n        className=\"peer hidden\"\n        checked={checked}\n        readOnly={true}\n        formNoValidate={true}\n      />\n      <label className=\"transition-all duration-300 inline-flex flex-col h-full w-60 cursor-pointer items-start justify-between rounded-2xl bg-preference-gradient border-2 border-transparent shadow-md px-5 py-4 text-white hover:bg-selected-preference-gradient hover:border-white/60 peer-checked:border-white peer-checked:border-opacity-90 peer-checked:bg-selected-preference-gradient\">\n        <div className=\"flex items-center\">\n          <img src={image} alt={name} className=\"h-10 w-10 rounded\" />\n          <div className=\"ml-4 text-sm font-semibold\">{name}</div>\n        </div>\n        <div className=\"mt-2 text-xs font-base text-white tracking-wide\">\n          {description}\n        </div>\n        <a\n          href={`https://${link}`}\n          className=\"mt-2 text-xs text-white font-medium underline\"\n        >\n          {link}\n        </a>\n      </label>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/LMStudioOptions/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport {\n  Info,\n  CaretDown,\n  CaretUp,\n  CircleNotch,\n  Warning,\n} from \"@phosphor-icons/react\";\nimport paths from \"@/utils/paths\";\nimport System from \"@/models/system\";\nimport { LMSTUDIO_COMMON_URLS } from \"@/utils/constants\";\nimport useProviderEndpointAutoDiscovery from \"@/hooks/useProviderEndpointAutoDiscovery\";\nimport { Tooltip } from \"react-tooltip\";\n\nexport default function LMStudioOptions({ settings, showAlert = false }) {\n  const {\n    autoDetecting: loading,\n    basePath,\n    basePathValue,\n    authToken,\n    authTokenValue,\n    showAdvancedControls,\n    setShowAdvancedControls,\n    handleAutoDetectClick,\n  } = useProviderEndpointAutoDiscovery({\n    provider: \"lmstudio\",\n    initialBasePath: settings?.LMStudioBasePath,\n    ENDPOINTS: LMSTUDIO_COMMON_URLS,\n  });\n\n  const [maxTokens, setMaxTokens] = useState(\n    settings?.LMStudioTokenLimit || \"\"\n  );\n\n  const handleMaxTokensChange = (e) => {\n    setMaxTokens(e.target.value ? Number(e.target.value) : \"\");\n  };\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      {showAlert && (\n        <div className=\"flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-6 bg-blue-800/30 w-fit rounded-lg px-4 py-2\">\n          <div className=\"gap-x-2 flex items-center\">\n            <Info size={12} className=\"hidden md:visible\" />\n            <p className=\"text-sm md:text-base\">\n              LMStudio as your LLM requires you to set an embedding service to\n              use.\n            </p>\n          </div>\n          <a\n            href={paths.settings.embedder.modelPreference()}\n            className=\"text-sm md:text-base my-2 underline\"\n          >\n            Manage embedding &rarr;\n          </a>\n        </div>\n      )}\n      <div className=\"w-full flex items-start gap-[36px] mt-1.5\">\n        <LMStudioModelSelection\n          settings={settings}\n          basePath={basePath.value}\n          apiKey={authTokenValue.value}\n        />\n      </div>\n      <div className=\"flex justify-start mt-4\">\n        <button\n          onClick={(e) => {\n            e.preventDefault();\n            setShowAdvancedControls(!showAdvancedControls);\n          }}\n          className=\"border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm\"\n        >\n          {showAdvancedControls ? \"Hide\" : \"Show\"} advanced settings\n          {showAdvancedControls ? (\n            <CaretUp size={14} className=\"ml-1\" />\n          ) : (\n            <CaretDown size={14} className=\"ml-1\" />\n          )}\n        </button>\n      </div>\n\n      <div hidden={!showAdvancedControls}>\n        <div className=\"w-full flex items-start gap-4\">\n          <div className=\"flex flex-col w-[300px]\">\n            <div className=\"flex justify-between items-center mb-2\">\n              <div className=\"flex items-center gap-1\">\n                <label className=\"text-white text-sm font-semibold\">\n                  LM Studio Base URL\n                </label>\n                <Info\n                  size={18}\n                  className=\"text-theme-text-secondary cursor-pointer\"\n                  data-tooltip-id=\"lmstudio-base-url\"\n                  data-tooltip-content=\"Enter the URL where LM Studio is running.\"\n                />\n                <Tooltip\n                  id=\"lmstudio-base-url\"\n                  place=\"top\"\n                  delayShow={300}\n                  className=\"tooltip !text-xs !opacity-100\"\n                  style={{\n                    maxWidth: \"250px\",\n                    whiteSpace: \"normal\",\n                    wordWrap: \"break-word\",\n                  }}\n                />\n              </div>\n              {loading ? (\n                <CircleNotch\n                  size={16}\n                  className=\"text-theme-text-secondary animate-spin\"\n                />\n              ) : (\n                <>\n                  {!basePathValue.value && (\n                    <button\n                      onClick={handleAutoDetectClick}\n                      className=\"border-none bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]\"\n                    >\n                      Auto-Detect\n                    </button>\n                  )}\n                </>\n              )}\n            </div>\n            <input\n              type=\"url\"\n              name=\"LMStudioBasePath\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"http://localhost:1234/v1\"\n              value={basePathValue.value}\n              required={true}\n              autoComplete=\"off\"\n              spellCheck={false}\n              onChange={basePath.onChange}\n              onBlur={basePath.onBlur}\n            />\n          </div>\n          <div className=\"flex flex-col w-60\">\n            <div className=\"flex items-center mb-2 gap-x-1\">\n              <label className=\"text-white text-sm font-semibold\">\n                Model Context Window\n              </label>\n              <Info\n                size={18}\n                className=\"text-theme-text-secondary cursor-pointer\"\n                data-tooltip-id=\"lmstudio-max-tokens\"\n                data-tooltip-content=\"Override the context window limit. Leave empty to auto-detect from the model (defaults to 4096 if detection fails).\"\n              />\n              <Tooltip\n                id=\"lmstudio-max-tokens\"\n                className=\"tooltip !text-xs !opacity-100\"\n                style={{\n                  maxWidth: \"250px\",\n                  whiteSpace: \"normal\",\n                  wordWrap: \"break-word\",\n                }}\n              />\n            </div>\n            <input\n              type=\"number\"\n              name=\"LMStudioTokenLimit\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"Auto-detected from model\"\n              min={1}\n              value={maxTokens}\n              onChange={handleMaxTokensChange}\n              onScroll={(e) => e.target.blur()}\n              required={false}\n              autoComplete=\"off\"\n            />\n          </div>\n        </div>\n\n        <div className=\"flex items-start gap-4 mt-4\">\n          <div className=\"flex flex-col w-60\">\n            <div className=\"flex items-center mb-2 gap-x-1\">\n              <label className=\"text-white text-sm font-semibold\">\n                Authentication Token\n              </label>\n              <Info\n                size={18}\n                className=\"text-theme-text-secondary cursor-pointer\"\n                data-tooltip-id=\"lmstudio-authentication-token\"\n              />\n              <Tooltip\n                id=\"lmstudio-authentication-token\"\n                place=\"top\"\n                delayShow={300}\n                delayHide={400}\n                clickable={true}\n                className=\"tooltip !text-xs !opacity-100\"\n                style={{\n                  maxWidth: \"250px\",\n                  whiteSpace: \"normal\",\n                  wordWrap: \"break-word\",\n                }}\n              >\n                <p className=\"text-xs leading-[18px] font-base\">\n                  Enter a <code>Bearer</code> Auth Token for interacting with\n                  your LM Studio server.\n                  <br /> <br />\n                  Useful if running LM Studio behind an authentication or proxy.\n                </p>\n              </Tooltip>\n            </div>\n            <input\n              type=\"password\"\n              name=\"LMStudioAuthToken\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg outline-none block w-full p-2.5 focus:outline-primary-button active:outline-primary-button\"\n              placeholder=\"LM Studio Auth Token\"\n              defaultValue={settings?.LMStudioAuthToken ? \"*\".repeat(20) : \"\"}\n              value={authTokenValue.value}\n              onChange={authToken.onChange}\n              onBlur={authToken.onBlur}\n              required={false}\n              autoComplete=\"off\"\n              spellCheck={false}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction LMStudioModelSelection({ settings, basePath = null, apiKey = null }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!basePath) {\n        setCustomModels([]);\n        setLoading(false);\n        return;\n      }\n      setLoading(true);\n      try {\n        const { models } = await System.customModels(\n          \"lmstudio\",\n          apiKey,\n          basePath\n        );\n        setCustomModels(models || []);\n      } catch (error) {\n        console.error(\"Failed to fetch custom models:\", error);\n        setCustomModels([]);\n      }\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [basePath, apiKey]);\n\n  if (loading || customModels.length === 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <div className=\"flex items-center mb-2 gap-x-1\">\n          <label className=\"text-white text-sm font-semibold\">\n            Selected Model\n          </label>\n          {!loading && !!basePath && (\n            <>\n              <Warning\n                size={18}\n                className=\"text-red-400 cursor-pointer\"\n                data-tooltip-id=\"lmstudio-selected-model\"\n              />\n              <Tooltip\n                id=\"lmstudio-selected-model\"\n                place=\"top\"\n                delayShow={300}\n                delayHide={400}\n                clickable={true}\n                className=\"tooltip !text-xs !opacity-100 z-99\"\n                style={{\n                  maxWidth: \"250px\",\n                  whiteSpace: \"normal\",\n                  wordWrap: \"break-word\",\n                }}\n              >\n                <p className=\"text-xs leading-[18px] font-base\">\n                  Could not reach LM Studio. Verify the URL is correct and the\n                  LMStudio server is running and accessible.\n                </p>\n              </Tooltip>\n            </>\n          )}\n        </div>\n        <select\n          name=\"LMStudioModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {loading\n              ? \"--loading available models--\"\n              : !!basePath\n                ? \"No models found\"\n                : \"Enter LM Studio URL first\"}\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-2\">\n        Selected Model\n      </label>\n      <select\n        name=\"LMStudioModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Your loaded models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings.LMStudioModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/LemonadeOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\nimport useProviderEndpointAutoDiscovery from \"@/hooks/useProviderEndpointAutoDiscovery\";\nimport { CircleNotch, Info } from \"@phosphor-icons/react\";\nimport strDistance from \"js-levenshtein\";\nimport { LLM_PREFERENCE_CHANGED_EVENT } from \"@/pages/GeneralSettings/LLMPreference\";\nimport { LEMONADE_COMMON_URLS } from \"@/utils/constants\";\nimport { Tooltip } from \"react-tooltip\";\nimport { Link } from \"react-router-dom\";\nimport ModelTable from \"@/components/lib/ModelTable\";\nimport ModelTableLayout from \"@/components/lib/ModelTable/layout\";\nimport ModelTableLoadingSkeleton from \"@/components/lib/ModelTable/loading\";\nimport showToast from \"@/utils/toast\";\nimport LemonadeUtils from \"@/models/utils/lemonadeUtils\";\n\nexport function cleanBasePath(basePath = \"\") {\n  try {\n    const url = new URL(basePath);\n    return url.origin;\n  } catch {\n    return basePath;\n  }\n}\n\nexport default function LemonadeOptions({ settings }) {\n  const {\n    autoDetecting: loading,\n    basePath,\n    basePathValue,\n    handleAutoDetectClick,\n  } = useProviderEndpointAutoDiscovery({\n    provider: \"lemonade\",\n    initialBasePath: settings?.LemonadeLLMBasePath,\n    ENDPOINTS: LEMONADE_COMMON_URLS,\n  });\n  const [selectedModelId, setSelectedModelId] = useState(\n    settings?.LemonadeLLMModelPref\n  );\n  const [maxTokens, setMaxTokens] = useState(\n    settings?.LemonadeLLMModelTokenLimit || 4096\n  );\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"flex gap-[36px] mt-1.5 flex-wrap\">\n        <div className=\"flex flex-col w-60\">\n          <div className=\"flex items-center gap-1 mb-3\">\n            <div className=\"flex justify-between items-center gap-x-2\">\n              <label className=\"text-white text-sm font-semibold\">\n                Base URL\n              </label>\n              {loading ? (\n                <CircleNotch className=\"w-4 h-4 text-theme-text-secondary animate-spin\" />\n              ) : (\n                <>\n                  {!basePathValue.value && (\n                    <button\n                      onClick={handleAutoDetectClick}\n                      className=\"border-none bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]\"\n                    >\n                      Auto-Detect\n                    </button>\n                  )}\n                </>\n              )}\n            </div>\n            <Tooltip\n              id=\"lemonade-base-url\"\n              place=\"top\"\n              delayShow={300}\n              delayHide={800}\n              clickable={true}\n              className=\"tooltip !text-xs !opacity-100 z-99\"\n              style={{\n                maxWidth: \"250px\",\n                whiteSpace: \"normal\",\n                wordWrap: \"break-word\",\n              }}\n            >\n              Enter the URL where the Lemonade is running.\n              <br />\n              <br />\n              You <b>must</b> have enabled the Lemonade TCP support for this to\n              work.\n              <br />\n              <br />\n              <Link\n                to=\"https://lemonade-server.ai/docs\"\n                target=\"_blank\"\n                className=\"text-blue-500 hover:underline\"\n              >\n                Learn more &rarr;\n              </Link>\n            </Tooltip>\n            <div\n              className=\"text-theme-text-secondary cursor-pointer hover:bg-theme-bg-primary flex items-center justify-center rounded-full\"\n              data-tooltip-id=\"lemonade-base-url\"\n              data-tooltip-place=\"top\"\n              data-tooltip-delay-hide={800}\n            >\n              <Info size={18} className=\"text-theme-text-secondary\" />\n            </div>\n          </div>\n\n          <input\n            type=\"url\"\n            name=\"LemonadeLLMBasePath\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"http://localhost:8000\"\n            value={cleanBasePath(basePathValue.value)}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={basePath.onChange}\n            onBlur={basePath.onBlur}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <div className=\"flex items-center gap-1 mb-3\">\n            <label className=\"text-white text-sm font-semibold block\">\n              Model context window\n            </label>\n            <Tooltip\n              id=\"lemonade-model-context-window\"\n              place=\"top\"\n              delayShow={300}\n              delayHide={800}\n              clickable={true}\n              className=\"tooltip !text-xs !opacity-100 z-99\"\n              style={{\n                maxWidth: \"350px\",\n                whiteSpace: \"normal\",\n                wordWrap: \"break-word\",\n              }}\n            >\n              The maximum number of tokens that can be used for a model context\n              window. This must be set to a value that is supported by the\n              model.\n            </Tooltip>\n            <div\n              className=\"text-theme-text-secondary cursor-pointer hover:bg-theme-bg-primary flex items-center justify-center rounded-full\"\n              data-tooltip-id=\"lemonade-model-context-window\"\n              data-tooltip-place=\"top\"\n              data-tooltip-delay-hide={800}\n            >\n              <Info size={18} className=\"text-theme-text-secondary\" />\n            </div>\n          </div>\n          <input\n            type=\"number\"\n            name=\"LemonadeLLMModelTokenLimit\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"4096\"\n            min={1}\n            value={maxTokens}\n            onChange={(e) => setMaxTokens(Number(e.target.value))}\n            onScroll={(e) => e.target.blur()}\n            required={true}\n            autoComplete=\"off\"\n          />\n        </div>\n        <LemonadeModelSelection\n          selectedModelId={selectedModelId}\n          setSelectedModelId={setSelectedModelId}\n          basePath={basePathValue.value}\n        />\n      </div>\n    </div>\n  );\n}\n\nfunction LemonadeModelSelection({\n  selectedModelId,\n  setSelectedModelId,\n  basePath = null,\n}) {\n  const [customModels, setCustomModels] = useState([]);\n  const [filteredModels, setFilteredModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n\n  async function fetchModels() {\n    if (!basePath) {\n      setCustomModels([]);\n      setFilteredModels([]);\n      setLoading(false);\n      setSearchQuery(\"\");\n      return;\n    }\n    setLoading(true);\n    const { models } = await System.customModels(\"lemonade\", null, basePath);\n    setCustomModels(models || []);\n    setFilteredModels(models || []);\n    setSearchQuery(\"\");\n    setLoading(false);\n  }\n\n  useEffect(() => {\n    fetchModels();\n  }, [basePath]);\n\n  useEffect(() => {\n    if (!searchQuery || !customModels.length) {\n      setFilteredModels(customModels || []);\n      return;\n    }\n\n    const normalizedSearchQuery = searchQuery.toLowerCase().trim();\n    const filteredModels = new Map();\n\n    customModels.forEach((model) => {\n      const modelNameNormalized = model.name.toLowerCase();\n      const modelOrganizationNormalized = model.organization.toLowerCase();\n\n      if (modelNameNormalized.startsWith(normalizedSearchQuery))\n        filteredModels.set(model.id, model);\n      if (modelOrganizationNormalized.startsWith(normalizedSearchQuery))\n        filteredModels.set(model.id, model);\n      if (strDistance(modelNameNormalized, normalizedSearchQuery) <= 2)\n        filteredModels.set(model.id, model);\n      if (strDistance(modelOrganizationNormalized, normalizedSearchQuery) <= 2)\n        filteredModels.set(model.id, model);\n    });\n\n    setFilteredModels(Array.from(filteredModels.values()));\n  }, [searchQuery]);\n\n  async function uninstallModel(modelId) {\n    try {\n      if (\n        !window.confirm(\n          `Are you sure you want to uninstall this model? You will need to download it again to use it.`\n        )\n      )\n        return;\n      const { success, error } = await LemonadeUtils.deleteModel(\n        modelId,\n        basePath\n      );\n\n      if (!success)\n        throw new Error(\n          error || \"An error occurred while uninstalling the model\"\n        );\n\n      const updatedModels = customModels.map((model) =>\n        model.id === modelId ? { ...model, downloaded: false } : model\n      );\n      setCustomModels(updatedModels);\n      setFilteredModels(updatedModels);\n      setSearchQuery(\"\");\n    } catch (e) {\n      console.error(\"Error uninstalling model:\", e);\n      showToast(\n        e.message || \"An error occurred while uninstalling the model\",\n        \"error\",\n        { clear: true }\n      );\n    } finally {\n      setLoading(false);\n    }\n  }\n\n  async function downloadModel(modelId, fileSize, progressCallback) {\n    try {\n      if (\n        !window.confirm(\n          `Are you sure you want to download this model? It is ${fileSize} in size and may take a while to download.`\n        )\n      )\n        return;\n      const { success, error } = await LemonadeUtils.downloadModel(\n        modelId,\n        basePath,\n        progressCallback\n      );\n      if (!success)\n        throw new Error(\n          error || \"An error occurred while downloading the model\"\n        );\n      progressCallback(100);\n      handleSetActiveModel(modelId);\n\n      const existingModels = [...customModels];\n      const newModel = existingModels.find((model) => model.id === modelId);\n      if (newModel) {\n        newModel.downloaded = true;\n        setCustomModels(existingModels);\n        setFilteredModels(existingModels);\n        setSearchQuery(\"\");\n      }\n    } catch (e) {\n      console.error(\"Error downloading model:\", e);\n      showToast(\n        e.message || \"An error occurred while downloading the model\",\n        \"error\",\n        { clear: true }\n      );\n    } finally {\n      setLoading(false);\n    }\n  }\n\n  function groupModelsByAlias(models) {\n    const mapping = new Map();\n    mapping.set(\"installed\", new Map());\n    mapping.set(\"not installed\", new Map());\n\n    const groupedModels = models.reduce((acc, model) => {\n      acc[model.organization] = acc[model.organization] || [];\n      acc[model.organization].push(model);\n      return acc;\n    }, {});\n\n    Object.entries(groupedModels).forEach(([organization, models]) => {\n      const hasInstalled = models.some((model) => model.downloaded);\n      if (hasInstalled) {\n        const installedModels = models.filter((model) => model.downloaded);\n        mapping\n          .get(\"installed\")\n          .set(\"Downloaded Models\", [\n            ...(mapping.get(\"installed\").get(\"Downloaded Models\") || []),\n            ...installedModels,\n          ]);\n      }\n      const tags = models.map((model) => ({\n        ...model,\n        name: model.name.split(\":\")[1],\n      }));\n      mapping.get(\"not installed\").set(organization, tags);\n    });\n\n    const orderedMap = new Map();\n    const installedMap = new Map();\n    mapping\n      .get(\"installed\")\n      .entries()\n      .forEach(([organization, models]) =>\n        installedMap.set(organization, models)\n      );\n    mapping\n      .get(\"not installed\")\n      .entries()\n      .forEach(([organization, models]) =>\n        orderedMap.set(organization, models)\n      );\n\n    // Sort the models by organization/creator name alphabetically but keep the installed models at the top\n    return Object.fromEntries(\n      Array.from(installedMap.entries())\n        .sort((a, b) => a[0].localeCompare(b[0]))\n        .concat(\n          Array.from(orderedMap.entries()).sort((a, b) =>\n            a[0].localeCompare(b[0])\n          )\n        )\n    );\n  }\n\n  function handleSetActiveModel(modelId) {\n    if (modelId === selectedModelId) return;\n    setSelectedModelId(modelId);\n    window.dispatchEvent(new Event(LLM_PREFERENCE_CHANGED_EVENT));\n  }\n\n  const groupedModels = groupModelsByAlias(filteredModels);\n  return (\n    <ModelTableLayout\n      fetchModels={fetchModels}\n      searchQuery={searchQuery}\n      setSearchQuery={setSearchQuery}\n      loading={loading}\n    >\n      <Tooltip\n        id=\"install-model-tooltip\"\n        place=\"top\"\n        className=\"tooltip !text-xs !opacity-100 z-99\"\n      />\n      <input\n        type=\"hidden\"\n        name=\"LemonadeLLMModelPref\"\n        id=\"LemonadeLLMModelPref\"\n        value={selectedModelId}\n      />\n      {loading ? (\n        <ModelTableLoadingSkeleton />\n      ) : filteredModels.length === 0 ? (\n        <div className=\"flex flex-col w-full gap-y-2 mt-4\">\n          <p className=\"text-theme-text-secondary text-sm\">No models found!</p>\n        </div>\n      ) : (\n        Object.entries(groupedModels).map(([alias, models]) => (\n          <ModelTable\n            key={alias}\n            alias={alias}\n            models={models}\n            setActiveModel={handleSetActiveModel}\n            downloadModel={downloadModel}\n            selectedModelId={selectedModelId}\n            uninstallModel={uninstallModel}\n            ui={{\n              showRuntime: false,\n            }}\n          />\n        ))\n      )}\n    </ModelTableLayout>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/LiteLLMOptions/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function LiteLLMOptions({ settings }) {\n  const [basePathValue, setBasePathValue] = useState(settings?.LiteLLMBasePath);\n  const [basePath, setBasePath] = useState(settings?.LiteLLMBasePath);\n  const [apiKeyValue, setApiKeyValue] = useState(settings?.LiteLLMAPIKey);\n  const [apiKey, setApiKey] = useState(settings?.LiteLLMAPIKey);\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7 mt-1.5\">\n      <div className=\"w-full flex items-center gap-[36px]\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Base URL\n          </label>\n          <input\n            type=\"url\"\n            name=\"LiteLLMBasePath\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"http://127.0.0.1:4000\"\n            defaultValue={settings?.LiteLLMBasePath}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={(e) => setBasePathValue(e.target.value)}\n            onBlur={() => setBasePath(basePathValue)}\n          />\n        </div>\n        <LiteLLMModelSelection\n          settings={settings}\n          basePath={basePath}\n          apiKey={apiKey}\n        />\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Model context window\n          </label>\n          <input\n            type=\"number\"\n            name=\"LiteLLMTokenLimit\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"8192\"\n            min={1}\n            onScroll={(e) => e.target.blur()}\n            defaultValue={settings?.LiteLLMTokenLimit}\n            required={true}\n            autoComplete=\"off\"\n          />\n        </div>\n      </div>\n      <div className=\"w-full flex items-center gap-[36px]\">\n        <div className=\"flex flex-col w-60\">\n          <div className=\"flex flex-col gap-y-1 mb-4\">\n            <label className=\"text-white text-sm font-semibold flex items-center gap-x-2\">\n              API Key <p className=\"!text-xs !italic !font-thin\">optional</p>\n            </label>\n          </div>\n          <input\n            type=\"password\"\n            name=\"LiteLLMAPIKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"sk-mysecretkey\"\n            defaultValue={settings?.LiteLLMAPIKey ? \"*\".repeat(20) : \"\"}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={(e) => setApiKeyValue(e.target.value)}\n            onBlur={() => setApiKey(apiKeyValue)}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction LiteLLMModelSelection({ settings, basePath = null, apiKey = null }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!basePath) {\n        setCustomModels([]);\n        setLoading(false);\n        return;\n      }\n      setLoading(true);\n      const { models } = await System.customModels(\n        \"litellm\",\n        typeof apiKey === \"boolean\" ? null : apiKey,\n        basePath\n      );\n      setCustomModels(models || []);\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [basePath, apiKey]);\n\n  if (loading || customModels.length == 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"LiteLLMModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {basePath?.includes(\"/v1\")\n              ? \"-- loading available models --\"\n              : \"-- waiting for URL --\"}\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"LiteLLMModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Your loaded models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings.LiteLLMModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/LocalAiOptions/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { Info, CaretDown, CaretUp } from \"@phosphor-icons/react\";\nimport paths from \"@/utils/paths\";\nimport System from \"@/models/system\";\nimport PreLoader from \"@/components/Preloader\";\nimport { LOCALAI_COMMON_URLS } from \"@/utils/constants\";\nimport useProviderEndpointAutoDiscovery from \"@/hooks/useProviderEndpointAutoDiscovery\";\n\nexport default function LocalAiOptions({ settings, showAlert = false }) {\n  const {\n    autoDetecting: loading,\n    basePath,\n    basePathValue,\n    showAdvancedControls,\n    setShowAdvancedControls,\n    handleAutoDetectClick,\n  } = useProviderEndpointAutoDiscovery({\n    provider: \"localai\",\n    initialBasePath: settings?.LocalAiBasePath,\n    ENDPOINTS: LOCALAI_COMMON_URLS,\n  });\n  const [apiKeyValue, setApiKeyValue] = useState(settings?.LocalAiApiKey);\n  const [apiKey, setApiKey] = useState(settings?.LocalAiApiKey);\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      {showAlert && (\n        <div className=\"flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-6 bg-blue-800/30 w-fit rounded-lg px-4 py-2\">\n          <div className=\"gap-x-2 flex items-center\">\n            <Info size={12} className=\"hidden md:visible\" />\n            <p className=\"text-sm md:text-base\">\n              LocalAI as your LLM requires you to set an embedding service to\n              use.\n            </p>\n          </div>\n          <a\n            href={paths.settings.embedder.modelPreference()}\n            className=\"text-sm md:text-base my-2 underline\"\n          >\n            Manage embedding &rarr;\n          </a>\n        </div>\n      )}\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        {!settings?.credentialsOnly && (\n          <>\n            <LocalAIModelSelection\n              settings={settings}\n              basePath={basePath.value}\n              apiKey={apiKey}\n            />\n            <div className=\"flex flex-col w-60\">\n              <label className=\"text-white text-sm font-semibold block mb-2\">\n                Model context window\n              </label>\n              <input\n                type=\"number\"\n                name=\"LocalAiTokenLimit\"\n                className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                placeholder=\"4096\"\n                min={1}\n                onScroll={(e) => e.target.blur()}\n                defaultValue={settings?.LocalAiTokenLimit}\n                required={true}\n                autoComplete=\"off\"\n              />\n            </div>\n          </>\n        )}\n        <div className=\"flex flex-col w-60\">\n          <div className=\"flex flex-col gap-y-1 mb-2\">\n            <label className=\"text-white text-sm font-semibold flex items-center gap-x-2\">\n              Local AI API Key{\" \"}\n              <p className=\"!text-xs !italic !font-thin\">optional</p>\n            </label>\n          </div>\n          <input\n            type=\"password\"\n            name=\"LocalAiApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"sk-mysecretkey\"\n            defaultValue={settings?.LocalAiApiKey ? \"*\".repeat(20) : \"\"}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={(e) => setApiKeyValue(e.target.value)}\n            onBlur={() => setApiKey(apiKeyValue)}\n          />\n        </div>\n      </div>\n      <div className=\"flex justify-start mt-4\">\n        <button\n          onClick={(e) => {\n            e.preventDefault();\n            setShowAdvancedControls(!showAdvancedControls);\n          }}\n          className=\"border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm\"\n        >\n          {showAdvancedControls ? \"Hide\" : \"Show\"} advanced settings\n          {showAdvancedControls ? (\n            <CaretUp size={14} className=\"ml-1\" />\n          ) : (\n            <CaretDown size={14} className=\"ml-1\" />\n          )}\n        </button>\n      </div>\n      <div hidden={!showAdvancedControls}>\n        <div className=\"w-full flex items-center gap-4\">\n          <div className=\"flex flex-col w-60\">\n            <div className=\"flex justify-between items-center mb-2\">\n              <label className=\"text-white text-sm font-semibold\">\n                Local AI Base URL\n              </label>\n              {loading ? (\n                <PreLoader size=\"6\" />\n              ) : (\n                <>\n                  {!basePathValue.value && (\n                    <button\n                      onClick={handleAutoDetectClick}\n                      className=\"bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]\"\n                    >\n                      Auto-Detect\n                    </button>\n                  )}\n                </>\n              )}\n            </div>\n            <input\n              type=\"url\"\n              name=\"LocalAiBasePath\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n              placeholder=\"http://localhost:8080/v1\"\n              value={basePathValue.value}\n              required={true}\n              autoComplete=\"off\"\n              spellCheck={false}\n              onChange={basePath.onChange}\n              onBlur={basePath.onBlur}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction LocalAIModelSelection({ settings, basePath = null, apiKey = null }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!basePath || !basePath.includes(\"/v1\")) {\n        setCustomModels([]);\n        setLoading(false);\n        return;\n      }\n      setLoading(true);\n      const { models } = await System.customModels(\n        \"localai\",\n        typeof apiKey === \"boolean\" ? null : apiKey,\n        basePath\n      );\n      setCustomModels(models || []);\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [basePath, apiKey]);\n\n  if (loading || customModels.length == 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-2\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"LocalAiModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {basePath?.includes(\"/v1\")\n              ? \"-- loading available models --\"\n              : \"-- waiting for URL --\"}\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-2\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"LocalAiModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Your loaded models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings.LocalAiModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/MistralOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function MistralOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.MistralApiKey);\n  const [mistralKey, setMistralKey] = useState(settings?.MistralApiKey);\n\n  return (\n    <div className=\"flex gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Mistral API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"MistralApiKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"Mistral API Key\"\n          defaultValue={settings?.MistralApiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n          onChange={(e) => setInputValue(e.target.value)}\n          onBlur={() => setMistralKey(inputValue)}\n        />\n      </div>\n      {!settings?.credentialsOnly && (\n        <MistralModelSelection settings={settings} apiKey={mistralKey} />\n      )}\n    </div>\n  );\n}\n\nfunction MistralModelSelection({ apiKey, settings }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!apiKey) {\n        setCustomModels([]);\n        setLoading(false);\n        return;\n      }\n      setLoading(true);\n      const { models } = await System.customModels(\n        \"mistral\",\n        typeof apiKey === \"boolean\" ? null : apiKey\n      );\n      setCustomModels(models || []);\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading || customModels.length == 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"MistralModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {!!apiKey\n              ? \"-- loading available models --\"\n              : \"-- waiting for API key --\"}\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"MistralModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Available Mistral Models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.MistralModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/MoonshotAiOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function MoonshotAiOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.MoonshotAiApiKey);\n  const [moonshotAiKey, setMoonshotAiKey] = useState(\n    settings?.MoonshotAiApiKey\n  );\n\n  return (\n    <div className=\"flex gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"MoonshotAiApiKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"Moonshot AI API Key\"\n          defaultValue={settings?.MoonshotAiApiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n          onChange={(e) => setInputValue(e.target.value)}\n          onBlur={() => setMoonshotAiKey(inputValue)}\n        />\n      </div>\n      {!settings?.credentialsOnly && (\n        <MoonshotAiModelSelection settings={settings} apiKey={moonshotAiKey} />\n      )}\n    </div>\n  );\n}\n\nfunction MoonshotAiModelSelection({ apiKey, settings }) {\n  const [models, setModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models: availableModels } = await System.customModels(\n        \"moonshotai\",\n        typeof apiKey === \"boolean\" ? null : apiKey\n      );\n\n      if (availableModels?.length > 0) {\n        setModels(availableModels);\n      }\n\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (!apiKey) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"MoonshotAiModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- Enter API key --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"MoonshotAiModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"MoonshotAiModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {models.map((model) => (\n          <option\n            key={model.id}\n            value={model.id}\n            selected={settings?.MoonshotAiModelPref === model.id}\n          >\n            {model.id}\n          </option>\n        ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/NovitaLLMOptions/index.jsx",
    "content": "import System from \"@/models/system\";\nimport { CaretDown, CaretUp } from \"@phosphor-icons/react\";\nimport { useState, useEffect } from \"react\";\n\nexport default function NovitaLLMOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-start gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n            Novita API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"NovitaLLMApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Novita API Key\"\n            defaultValue={settings?.NovitaLLMApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        {!settings?.credentialsOnly && (\n          <NovitaModelSelection settings={settings} />\n        )}\n      </div>\n      <AdvancedControls settings={settings} />\n    </div>\n  );\n}\n\nfunction AdvancedControls({ settings }) {\n  const [showAdvancedControls, setShowAdvancedControls] = useState(false);\n\n  return (\n    <div className=\"flex flex-col gap-y-4\">\n      <div className=\"flex justify-start\">\n        <button\n          type=\"button\"\n          onClick={() => setShowAdvancedControls(!showAdvancedControls)}\n          className=\"border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm\"\n        >\n          {showAdvancedControls ? \"Hide\" : \"Show\"} advanced settings\n          {showAdvancedControls ? (\n            <CaretUp size={14} className=\"ml-1\" />\n          ) : (\n            <CaretDown size={14} className=\"ml-1\" />\n          )}\n        </button>\n      </div>\n      <div hidden={!showAdvancedControls}>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n            Stream Timeout (ms)\n          </label>\n          <input\n            type=\"number\"\n            name=\"NovitaLLMTimeout\"\n            className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Timeout value between token responses to auto-timeout the stream\"\n            defaultValue={settings?.NovitaLLMTimeout ?? 3_000}\n            autoComplete=\"off\"\n            onScroll={(e) => e.target.blur()}\n            min={500}\n            step={1}\n          />\n          <p className=\"text-xs leading-[18px] font-base text-theme-text-primary text-opacity-60 mt-2\">\n            Timeout value between token responses to auto-timeout the stream.\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction NovitaModelSelection({ settings }) {\n  const [groupedModels, setGroupedModels] = useState({});\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models } = await System.customModels(\"novita\");\n      if (models?.length > 0) {\n        const modelsByOrganization = models.reduce((acc, model) => {\n          acc[model.organization] = acc[model.organization] || [];\n          acc[model.organization].push(model);\n          return acc;\n        }, {});\n        setGroupedModels(modelsByOrganization);\n      }\n      setLoading(false);\n    }\n    findCustomModels();\n  }, []);\n\n  if (loading || Object.keys(groupedModels).length === 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"NovitaLLMModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg text-theme-text-primary border-theme-border text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"NovitaLLMModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg text-theme-text-primary border-theme-border text-sm rounded-lg block w-full p-2.5\"\n      >\n        {Object.keys(groupedModels)\n          .sort()\n          .map((organization) => (\n            <optgroup key={organization} label={organization}>\n              {groupedModels[organization].map((model) => (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.NovitaLLMModelPref === model.id}\n                >\n                  {model.name}\n                </option>\n              ))}\n            </optgroup>\n          ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/NvidiaNimOptions/index.jsx",
    "content": "import RemoteNvidiaNimOptions from \"./remote\";\nimport ManagedNvidiaNimOptions from \"./managed\";\n\nexport default function NvidiaNimOptions({ settings }) {\n  const version = \"remote\"; // static to \"remote\" when in docker version.\n  return version === \"remote\" ? (\n    <RemoteNvidiaNimOptions settings={settings} />\n  ) : (\n    <ManagedNvidiaNimOptions settings={settings} />\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/NvidiaNimOptions/managed.jsx",
    "content": "/**\n * This component is used to select, start, and manage NVIDIA NIM\n * containers and images via docker management tools.\n */\nexport default function ManagedNvidiaNimOptions({ settings: _settings }) {\n  return null;\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/NvidiaNimOptions/remote.jsx",
    "content": "import PreLoader from \"@/components/Preloader\";\nimport useProviderEndpointAutoDiscovery from \"@/hooks/useProviderEndpointAutoDiscovery\";\nimport System from \"@/models/system\";\nimport { NVIDIA_NIM_COMMON_URLS } from \"@/utils/constants\";\nimport { useState, useEffect } from \"react\";\n\n/**\n * This component is used to select a remote NVIDIA NIM model endpoint\n * This is the default component and way to connect to NVIDIA NIM\n * as the \"managed\" provider can only work in the Desktop context.\n */\nexport default function RemoteNvidiaNimOptions({ settings }) {\n  const {\n    autoDetecting: loading,\n    basePath,\n    basePathValue,\n    handleAutoDetectClick,\n  } = useProviderEndpointAutoDiscovery({\n    provider: \"nvidia-nim\",\n    initialBasePath: settings?.NvidiaNimLLMBasePath,\n    ENDPOINTS: NVIDIA_NIM_COMMON_URLS,\n  });\n\n  return (\n    <div className=\"flex gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <div className=\"flex justify-between items-center mb-2\">\n          <label className=\"text-white text-sm font-semibold\">\n            NVIDIA Nim Base URL\n          </label>\n          {loading ? (\n            <PreLoader size=\"6\" />\n          ) : (\n            <>\n              {!basePathValue.value && (\n                <button\n                  onClick={handleAutoDetectClick}\n                  className=\"bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]\"\n                >\n                  Auto-Detect\n                </button>\n              )}\n            </>\n          )}\n        </div>\n        <input\n          type=\"url\"\n          name=\"NvidiaNimLLMBasePath\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"http://localhost:8000/v1\"\n          value={basePathValue.value}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n          onChange={basePath.onChange}\n          onBlur={basePath.onBlur}\n        />\n        <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n          Enter the URL where NVIDIA NIM is running.\n        </p>\n      </div>\n      {!settings?.credentialsOnly && (\n        <NvidiaNimModelSelection\n          settings={settings}\n          basePath={basePath.value}\n        />\n      )}\n    </div>\n  );\n}\nfunction NvidiaNimModelSelection({ settings, basePath }) {\n  const [models, setModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models } = await System.customModels(\n        \"nvidia-nim\",\n        null,\n        basePath\n      );\n      setModels(models);\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [basePath]);\n\n  if (loading || models.length === 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"NvidiaNimLLMModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"NvidiaNimLLMModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {models.map((model) => (\n          <option\n            key={model.id}\n            value={model.id}\n            selected={settings?.NvidiaNimLLMModelPref === model.id}\n          >\n            {model.name}\n          </option>\n        ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/OllamaLLMOptions/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport System from \"@/models/system\";\nimport { OLLAMA_COMMON_URLS } from \"@/utils/constants\";\nimport { CaretDown, CaretUp, Info, CircleNotch } from \"@phosphor-icons/react\";\nimport useProviderEndpointAutoDiscovery from \"@/hooks/useProviderEndpointAutoDiscovery\";\nimport { Tooltip } from \"react-tooltip\";\nimport { Link } from \"react-router-dom\";\n\nexport default function OllamaLLMOptions({ settings }) {\n  const {\n    autoDetecting: loading,\n    basePath,\n    basePathValue,\n    authToken,\n    authTokenValue,\n    showAdvancedControls,\n    setShowAdvancedControls,\n    handleAutoDetectClick,\n  } = useProviderEndpointAutoDiscovery({\n    provider: \"ollama\",\n    initialBasePath: settings?.OllamaLLMBasePath,\n    ENDPOINTS: OLLAMA_COMMON_URLS,\n  });\n  const [maxTokens, setMaxTokens] = useState(\n    settings?.OllamaLLMTokenLimit || \"\"\n  );\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-start gap-[36px] mt-1.5\">\n        <OllamaLLMModelSelection\n          settings={settings}\n          basePath={basePath.value}\n          authToken={authToken.value}\n        />\n      </div>\n      <div className=\"flex justify-start mt-4\">\n        <button\n          onClick={(e) => {\n            e.preventDefault();\n            setShowAdvancedControls(!showAdvancedControls);\n          }}\n          className=\"border-none text-theme-text-primary hover:text-theme-text-secondary flex items-center text-sm\"\n        >\n          {showAdvancedControls ? \"Hide\" : \"Show\"} advanced settings\n          {showAdvancedControls ? (\n            <CaretUp size={14} className=\"ml-1\" />\n          ) : (\n            <CaretDown size={14} className=\"ml-1\" />\n          )}\n        </button>\n      </div>\n\n      <div hidden={!showAdvancedControls}>\n        <div className=\"flex flex-col\">\n          <div className=\"w-full flex items-start gap-4 mb-4\">\n            <div className=\"flex flex-col w-60\">\n              <div className=\"flex justify-between items-center mb-2\">\n                <div className=\"flex items-center gap-1\">\n                  <label className=\"text-white text-sm font-semibold\">\n                    Ollama Base URL\n                  </label>\n                  <Info\n                    size={18}\n                    className=\"text-theme-text-secondary cursor-pointer\"\n                    data-tooltip-id=\"ollama-base-url\"\n                    data-tooltip-content=\"Enter the URL where Ollama is running.\"\n                  />\n                  <Tooltip\n                    id=\"ollama-base-url\"\n                    place=\"top\"\n                    delayShow={300}\n                    className=\"tooltip !text-xs !opacity-100\"\n                    style={{\n                      maxWidth: \"250px\",\n                      whiteSpace: \"normal\",\n                      wordWrap: \"break-word\",\n                    }}\n                  />\n                </div>\n                {loading ? (\n                  <CircleNotch\n                    size={16}\n                    className=\"text-theme-text-secondary animate-spin\"\n                  />\n                ) : (\n                  <>\n                    {!basePathValue.value && (\n                      <button\n                        onClick={handleAutoDetectClick}\n                        className=\"bg-primary-button text-xs font-medium px-2 py-1 rounded-lg hover:bg-secondary hover:text-white shadow-[0_4px_14px_rgba(0,0,0,0.25)]\"\n                      >\n                        Auto-Detect\n                      </button>\n                    )}\n                  </>\n                )}\n              </div>\n              <input\n                type=\"url\"\n                name=\"OllamaLLMBasePath\"\n                className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                placeholder=\"http://127.0.0.1:11434\"\n                value={basePathValue.value}\n                required={true}\n                autoComplete=\"off\"\n                spellCheck={false}\n                onChange={basePath.onChange}\n                onBlur={basePath.onBlur}\n              />\n            </div>\n\n            <div className=\"flex flex-col w-60\">\n              <div className=\"flex items-center mb-2 gap-x-1\">\n                <label className=\"text-white text-sm font-semibold block\">\n                  Ollama Keep Alive\n                </label>\n                <Info\n                  size={18}\n                  className=\"text-theme-text-secondary cursor-pointer\"\n                  data-tooltip-id=\"ollama-keep-alive\"\n                />\n                <Tooltip\n                  id=\"ollama-keep-alive\"\n                  place=\"top\"\n                  delayShow={300}\n                  delayHide={400}\n                  clickable={true}\n                  className=\"tooltip !text-xs !opacity-100\"\n                  style={{\n                    maxWidth: \"250px\",\n                    whiteSpace: \"normal\",\n                    wordWrap: \"break-word\",\n                  }}\n                >\n                  <p className=\"text-xs leading-[18px] font-base\">\n                    Choose how long Ollama should keep your model in memory\n                    before unloading.{\" \"}\n                    <Link\n                      className=\"underline text-blue-300\"\n                      to=\"https://docs.ollama.com/faq#how-do-i-keep-a-model-loaded-in-memory-or-make-it-unload-immediately\"\n                      target=\"_blank\"\n                      rel=\"noreferrer\"\n                    >\n                      Learn more &rarr;\n                    </Link>\n                  </p>\n                </Tooltip>\n              </div>\n              <select\n                name=\"OllamaLLMKeepAliveSeconds\"\n                required={true}\n                className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n                defaultValue={settings?.OllamaLLMKeepAliveSeconds ?? \"300\"}\n              >\n                <option value=\"0\">No cache</option>\n                <option value=\"300\">5 minutes</option>\n                <option value=\"3600\">1 hour</option>\n                <option value=\"-1\">Forever</option>\n              </select>\n            </div>\n          </div>\n          <div className=\"w-full flex items-start gap-4\">\n            <div className=\"flex flex-col w-60\">\n              <div className=\"flex items-center mb-2 gap-x-1\">\n                <label className=\"text-white text-sm font-semibold block\">\n                  Model context window\n                </label>\n                <Info\n                  size={18}\n                  className=\"text-theme-text-secondary cursor-pointer\"\n                  data-tooltip-id=\"ollama-model-context-window\"\n                />\n                <Tooltip\n                  id=\"ollama-model-context-window\"\n                  place=\"top\"\n                  delayShow={300}\n                  delayHide={400}\n                  clickable={true}\n                  className=\"tooltip !text-xs !opacity-100\"\n                  style={{\n                    maxWidth: \"250px\",\n                    whiteSpace: \"normal\",\n                    wordWrap: \"break-word\",\n                  }}\n                >\n                  <p className=\"text-xs leading-[18px] font-base\">\n                    Specify the maximum number of tokens that can be used for\n                    the model context window.\n                    <br /> <br />\n                    If you leave this field blank, the context window limit will\n                    be auto-detected from the model and applied to all chats. If\n                    auto-detection fails, a fallback context window limit of\n                    4096 will be used.\n                    <br /> <br />\n                    <b>Important:</b> Some models have very large context\n                    windows using the full context window limit can dramatically\n                    increase the memory usage of your system. For this reason,\n                    we will automatically cap the context window limit to 16,384\n                    tokens if the model supports more than that and no value is\n                    specified.\n                    <br /> <br />\n                    If an invalid value is entered, AnythingLLM will handle this\n                    for you so that chats do not fail.\n                  </p>\n                </Tooltip>\n              </div>\n              <input\n                type=\"number\"\n                name=\"OllamaLLMTokenLimit\"\n                className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                placeholder=\"Auto-detected from model\"\n                min={1}\n                value={maxTokens}\n                onChange={(e) =>\n                  setMaxTokens(e.target.value ? Number(e.target.value) : \"\")\n                }\n                onScroll={(e) => e.target.blur()}\n                required={false}\n                autoComplete=\"off\"\n              />\n            </div>\n\n            <div className=\"flex flex-col w-60\">\n              <div className=\"flex items-center mb-2 gap-x-1\">\n                <label className=\"text-white text-sm font-semibold\">\n                  Authentication Token\n                </label>\n                <Info\n                  size={18}\n                  className=\"text-theme-text-secondary cursor-pointer\"\n                  data-tooltip-id=\"ollama-authentication-token\"\n                />\n                <Tooltip\n                  id=\"ollama-authentication-token\"\n                  place=\"top\"\n                  delayShow={300}\n                  delayHide={400}\n                  clickable={true}\n                  className=\"tooltip !text-xs !opacity-100\"\n                  style={{\n                    maxWidth: \"250px\",\n                    whiteSpace: \"normal\",\n                    wordWrap: \"break-word\",\n                  }}\n                >\n                  <p className=\"text-xs leading-[18px] font-base\">\n                    Enter a <code>Bearer</code> Auth Token for interacting with\n                    your Ollama server.\n                    <br /> <br />\n                    Used <b>only</b> if running Ollama behind an authentication\n                    server.\n                  </p>\n                </Tooltip>\n              </div>\n              <input\n                type=\"password\"\n                name=\"OllamaLLMAuthToken\"\n                className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg outline-none block w-full p-2.5 focus:outline-primary-button active:outline-primary-button\"\n                placeholder=\"Ollama Auth Token\"\n                defaultValue={\n                  settings?.OllamaLLMAuthToken ? \"*\".repeat(20) : \"\"\n                }\n                value={authTokenValue.value}\n                onChange={authToken.onChange}\n                onBlur={authToken.onBlur}\n                required={false}\n                autoComplete=\"off\"\n                spellCheck={false}\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction OllamaLLMModelSelection({\n  settings,\n  basePath = null,\n  authToken = null,\n}) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!basePath) {\n        setCustomModels([]);\n        setLoading(false);\n        return;\n      }\n      setLoading(true);\n      try {\n        const { models } = await System.customModels(\n          \"ollama\",\n          authToken,\n          basePath\n        );\n        setCustomModels(models || []);\n      } catch (error) {\n        console.error(\"Failed to fetch custom models:\", error);\n        setCustomModels([]);\n      }\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [basePath, authToken]);\n\n  if (loading || customModels.length === 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-2\">\n          Ollama Model\n        </label>\n        <select\n          name=\"OllamaLLMModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {!!basePath\n              ? \"--loading available models--\"\n              : \"Enter Ollama URL first\"}\n          </option>\n        </select>\n        <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n          Select the Ollama model you want to use. Models will load after\n          entering a valid Ollama URL.\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-2\">\n        Ollama Model\n      </label>\n      <select\n        name=\"OllamaLLMModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Your loaded models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings.OllamaLLMModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n      <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n        Choose the Ollama model you want to use for your conversations.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/OpenAiOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function OpenAiOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.OpenAiKey);\n  const [openAIKey, setOpenAIKey] = useState(settings?.OpenAiKey);\n\n  return (\n    <div className=\"flex gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"OpenAiKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"OpenAI API Key\"\n          defaultValue={settings?.OpenAiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n          onChange={(e) => setInputValue(e.target.value)}\n          onBlur={() => setOpenAIKey(inputValue)}\n        />\n      </div>\n      {!settings?.credentialsOnly && (\n        <OpenAIModelSelection settings={settings} apiKey={openAIKey} />\n      )}\n    </div>\n  );\n}\n\nfunction OpenAIModelSelection({ apiKey, settings }) {\n  const [groupedModels, setGroupedModels] = useState({});\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models } = await System.customModels(\n        \"openai\",\n        typeof apiKey === \"boolean\" ? null : apiKey\n      );\n\n      if (models?.length > 0) {\n        const modelsByOrganization = models.reduce((acc, model) => {\n          acc[model.organization] = acc[model.organization] || [];\n          acc[model.organization].push(model);\n          return acc;\n        }, {});\n        setGroupedModels(modelsByOrganization);\n      }\n\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"OpenAiModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"OpenAiModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {Object.keys(groupedModels)\n          .sort()\n          .map((organization) => (\n            <optgroup key={organization} label={organization}>\n              {groupedModels[organization].map((model) => (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.OpenAiModelPref === model.id}\n                >\n                  {model.name}\n                </option>\n              ))}\n            </optgroup>\n          ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/OpenRouterOptions/index.jsx",
    "content": "import System from \"@/models/system\";\nimport { CaretDown, CaretUp } from \"@phosphor-icons/react\";\nimport { useState, useEffect } from \"react\";\n\nexport default function OpenRouterOptions({ settings }) {\n  return (\n    <div className=\"flex flex-col gap-y-4 mt-1.5\">\n      <div className=\"flex gap-[36px]\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            OpenRouter API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"OpenRouterApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"OpenRouter API Key\"\n            defaultValue={settings?.OpenRouterApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        {!settings?.credentialsOnly && (\n          <OpenRouterModelSelection settings={settings} />\n        )}\n      </div>\n      <AdvancedControls settings={settings} />\n    </div>\n  );\n}\n\nfunction AdvancedControls({ settings }) {\n  const [showAdvancedControls, setShowAdvancedControls] = useState(false);\n\n  return (\n    <div className=\"flex flex-col gap-y-4\">\n      <button\n        type=\"button\"\n        onClick={() => setShowAdvancedControls(!showAdvancedControls)}\n        className=\"border-none text-white hover:text-white/70 flex items-center text-sm\"\n      >\n        {showAdvancedControls ? \"Hide\" : \"Show\"} advanced controls\n        {showAdvancedControls ? (\n          <CaretUp size={14} className=\"ml-1\" />\n        ) : (\n          <CaretDown size={14} className=\"ml-1\" />\n        )}\n      </button>\n      <div hidden={!showAdvancedControls}>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Stream Timeout (ms)\n          </label>\n          <input\n            type=\"number\"\n            name=\"OpenRouterTimeout\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Timeout value between token responses to auto-timeout the stream\"\n            defaultValue={settings?.OpenRouterTimeout ?? 3_000}\n            autoComplete=\"off\"\n            onScroll={(e) => e.target.blur()}\n            min={500}\n            step={1}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction OpenRouterModelSelection({ settings }) {\n  const [groupedModels, setGroupedModels] = useState({});\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models } = await System.customModels(\"openrouter\");\n      if (models?.length > 0) {\n        const modelsByOrganization = models.reduce((acc, model) => {\n          acc[model.organization] = acc[model.organization] || [];\n          acc[model.organization].push(model);\n          return acc;\n        }, {});\n\n        setGroupedModels(modelsByOrganization);\n      }\n\n      setLoading(false);\n    }\n    findCustomModels();\n  }, []);\n\n  if (loading || Object.keys(groupedModels).length === 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"OpenRouterModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"OpenRouterModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {Object.keys(groupedModels)\n          .sort()\n          .map((organization) => (\n            <optgroup key={organization} label={organization}>\n              {groupedModels[organization].map((model) => (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.OpenRouterModelPref === model.id}\n                >\n                  {model.name}\n                </option>\n              ))}\n            </optgroup>\n          ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/PPIOLLMOptions/index.jsx",
    "content": "import System from \"@/models/system\";\nimport { useState, useEffect } from \"react\";\n\nexport default function PPIOLLMOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-start gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n            PPIO API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"PPIOApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"PPIO API Key\"\n            defaultValue={settings?.PPIOApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        {!settings?.credentialsOnly && (\n          <PPIOModelSelection settings={settings} />\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction PPIOModelSelection({ settings }) {\n  const [groupedModels, setGroupedModels] = useState({});\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function fetchModels() {\n      setLoading(true);\n      const { models } = await System.customModels(\"ppio\");\n      if (models?.length > 0) {\n        const modelsByOrganization = models.reduce((acc, model) => {\n          acc[model.organization] = acc[model.organization] || [];\n          acc[model.organization].push(model);\n          return acc;\n        }, {});\n        setGroupedModels(modelsByOrganization);\n      }\n      setLoading(false);\n    }\n    fetchModels();\n  }, []);\n\n  if (loading || Object.keys(groupedModels).length === 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"PPIOModelPref\"\n          required={true}\n          disabled={true}\n          className=\"bg-theme-settings-input-bg text-theme-text-primary text-sm rounded-lg focus:ring-primary-button focus:border-primary-button block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col\">\n      <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"PPIOModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg text-theme-text-primary border-theme-border text-sm rounded-lg block w-full p-2.5\"\n      >\n        {Object.keys(groupedModels)\n          .sort()\n          .map((organization) => (\n            <optgroup key={organization} label={organization}>\n              {groupedModels[organization].map((model) => (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.PPIOModelPref === model.id}\n                >\n                  {model.name}\n                </option>\n              ))}\n            </optgroup>\n          ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/PerplexityOptions/index.jsx",
    "content": "import System from \"@/models/system\";\nimport { useState, useEffect } from \"react\";\n\nexport default function PerplexityOptions({ settings }) {\n  return (\n    <div className=\"flex gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Perplexity API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"PerplexityApiKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"Perplexity API Key\"\n          defaultValue={settings?.PerplexityApiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n        />\n      </div>\n      {!settings?.credentialsOnly && (\n        <PerplexityModelSelection settings={settings} />\n      )}\n    </div>\n  );\n}\n\nfunction PerplexityModelSelection({ settings }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models } = await System.customModels(\"perplexity\");\n      setCustomModels(models || []);\n      setLoading(false);\n    }\n    findCustomModels();\n  }, []);\n\n  if (loading || customModels.length == 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"PerplexityModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"PerplexityModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Available Perplexity Models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.PerplexityModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/PrivateModeOptions/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { Info } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\nimport System from \"@/models/system\";\nimport { Link } from \"react-router-dom\";\n\nexport default function PrivateModeOptions({ settings }) {\n  const [models, setModels] = useState([]);\n  const [loading, setLoading] = useState(!!settings?.PrivateModeBasePath);\n  const [basePath, setBasePath] = useState(settings?.PrivateModeBasePath);\n  const [model, setModel] = useState(settings?.PrivateModeModelPref || \"\");\n\n  useEffect(() => {\n    setModel(settings?.PrivateModeModelPref || \"\");\n  }, [settings?.PrivateModeModelPref]);\n\n  useEffect(() => {\n    async function fetchModels() {\n      try {\n        setLoading(true);\n        if (!basePath) throw new Error(\"Base path is required\");\n        const { models, error } = await System.customModels(\n          \"privatemode\",\n          null,\n          basePath\n        );\n        if (error) throw new Error(error);\n        setModels(models);\n      } catch (error) {\n        console.error(\"Error fetching Private Mode models:\", error);\n        setModels([]);\n      } finally {\n        setLoading(false);\n      }\n    }\n    fetchModels();\n  }, [basePath]);\n\n  return (\n    <div className=\"flex flex-col gap-y-7\">\n      <div className=\"flex gap-[36px] mt-1.5 flex-wrap\">\n        <div className=\"flex flex-col w-60\">\n          <div className=\"flex items-center gap-1 mb-2\">\n            <label className=\"text-white text-sm font-semibold\">\n              Privatemode Proxy URL\n            </label>\n            <Info\n              size={18}\n              className=\"text-theme-text-secondary cursor-pointer\"\n              data-tooltip-id=\"private-mode-base-url\"\n            />\n            <Tooltip\n              id=\"private-mode-base-url\"\n              place=\"top\"\n              delayShow={300}\n              clickable={true}\n              className=\"tooltip !text-xs !opacity-100\"\n              style={{\n                maxWidth: \"250px\",\n                whiteSpace: \"normal\",\n                wordWrap: \"break-word\",\n              }}\n            >\n              Enter the URL where Privatemode Proxy is running.\n              <br />\n              <br />\n              <Link\n                to=\"https://docs.privatemode.ai/quickstart#2-run-the-proxy\"\n                target=\"_blank\"\n                className=\"text-blue-500 hover:underline\"\n              >\n                Learn more &rarr;\n              </Link>\n            </Tooltip>\n          </div>\n          <input\n            type=\"url\"\n            name=\"PrivateModeBasePath\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"eg: http://127.0.0.1:8080\"\n            defaultValue={settings?.PrivateModeBasePath}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n            onChange={(e) => setBasePath(e.target.value)}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-2\">\n            Chat Model\n          </label>\n          {loading ? (\n            <select\n              name=\"PrivateModeModelPref\"\n              required={true}\n              disabled={true}\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            >\n              <option>---- Loading ----</option>\n            </select>\n          ) : (\n            <select\n              name=\"PrivateModeModelPref\"\n              value={model}\n              onChange={(e) => setModel(e.target.value)}\n              required={true}\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            >\n              {models.length > 0 ? (\n                <>\n                  <option value=\"\">-- Select a model --</option>\n                  {models.map((model) => (\n                    <option key={model.id} value={model.id}>\n                      {model.name}\n                    </option>\n                  ))}\n                </>\n              ) : (\n                <option disabled value=\"\">\n                  No models found\n                </option>\n              )}\n            </select>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/SambaNovaOptions/index.jsx",
    "content": "import System from \"@/models/system\";\nimport { useState, useEffect } from \"react\";\n\nexport default function SambaNovaOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.SambaNovaLLMApiKey);\n  const [apiKey, setApiKey] = useState(settings?.SambaNovaLLMApiKey);\n\n  return (\n    <div className=\"flex gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          SambaNova API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"SambaNovaLLMApiKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"SambaNova AI API Key\"\n          defaultValue={settings?.SambaNovaLLMApiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n          onChange={(e) => setInputValue(e.target.value)}\n          onBlur={() => setApiKey(inputValue)}\n        />\n      </div>\n      {!settings?.credentialsOnly && (\n        <SambaNovaModelSelection settings={settings} apiKey={apiKey} />\n      )}\n    </div>\n  );\n}\n\nfunction SambaNovaModelSelection({ settings, apiKey }) {\n  const [groupedModels, setGroupedModels] = useState({});\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models } = await System.customModels(\"sambanova\", apiKey);\n      if (models?.length > 0) {\n        const modelsByOrganization = models.reduce((acc, model) => {\n          acc[model.organization] = acc[model.organization] || [];\n          acc[model.organization].push(model);\n          return acc;\n        }, {});\n        setGroupedModels(modelsByOrganization);\n      }\n\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading || Object.keys(groupedModels).length === 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"SambaNovaLLMModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"SambaNovaLLMModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {Object.keys(groupedModels)\n          .sort()\n          .map((organization) => (\n            <optgroup key={organization} label={organization}>\n              {groupedModels[organization].map((model) => (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.SambaNovaLLMModelPref === model.id}\n                >\n                  {model.name}\n                </option>\n              ))}\n            </optgroup>\n          ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/TextGenWebUIOptions/index.jsx",
    "content": "export default function TextGenWebUIOptions({ settings }) {\n  return (\n    <div className=\"flex gap-[36px] mt-1.5 flex-wrap\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Base URL\n        </label>\n        <input\n          type=\"url\"\n          name=\"TextGenWebUIBasePath\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"http://127.0.0.1:5000/v1\"\n          defaultValue={settings?.TextGenWebUIBasePath}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n        />\n      </div>\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Model context window\n        </label>\n        <input\n          type=\"number\"\n          name=\"TextGenWebUITokenLimit\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"Content window limit (eg: 4096)\"\n          min={1}\n          onScroll={(e) => e.target.blur()}\n          defaultValue={settings?.TextGenWebUITokenLimit}\n          required={true}\n          autoComplete=\"off\"\n        />\n      </div>\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          API Key (Optional)\n        </label>\n        <input\n          type=\"password\"\n          name=\"TextGenWebUIAPIKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"TextGen Web UI API Key\"\n          defaultValue={settings?.TextGenWebUIAPIKey ? \"*\".repeat(20) : \"\"}\n          autoComplete=\"off\"\n          spellCheck={false}\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/TogetherAiOptions/index.jsx",
    "content": "import System from \"@/models/system\";\nimport { useState, useEffect } from \"react\";\n\nexport default function TogetherAiOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.TogetherAiApiKey);\n  const [apiKey, setApiKey] = useState(settings?.TogetherAiApiKey);\n\n  return (\n    <div className=\"flex gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Together AI API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"TogetherAiApiKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"Together AI API Key\"\n          defaultValue={settings?.TogetherAiApiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n          onChange={(e) => setInputValue(e.target.value)}\n          onBlur={() => setApiKey(inputValue)}\n        />\n      </div>\n      {!settings?.credentialsOnly && (\n        <TogetherAiModelSelection settings={settings} apiKey={apiKey} />\n      )}\n    </div>\n  );\n}\n\nfunction TogetherAiModelSelection({ settings, apiKey }) {\n  const [groupedModels, setGroupedModels] = useState({});\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      try {\n        const key = apiKey === \"*\".repeat(20) ? null : apiKey;\n        const { models } = await System.customModels(\"togetherai\", key);\n        if (models?.length > 0) {\n          const modelsByOrganization = models.reduce((acc, model) => {\n            if (model.type !== \"chat\") return acc; // Only show chat models in dropdown\n            const org = model.organization || \"Unknown\";\n            acc[org] = acc[org] || [];\n            acc[org].push({\n              id: model.id,\n              name: model.name || model.id,\n              organization: org,\n              maxLength: model.maxLength,\n            });\n            return acc;\n          }, {});\n          setGroupedModels(modelsByOrganization);\n        }\n      } catch (error) {\n        console.error(\"Error fetching Together AI models:\", error);\n      }\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading || Object.keys(groupedModels).length === 0) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"TogetherAiModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"TogetherAiModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {Object.keys(groupedModels)\n          .sort()\n          .map((organization) => (\n            <optgroup key={organization} label={organization}>\n              {groupedModels[organization].map((model) => (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.TogetherAiModelPref === model.id}\n                >\n                  {model.name}\n                </option>\n              ))}\n            </optgroup>\n          ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/XAiLLMOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function XAILLMOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.XAIApiKey);\n  const [apiKey, setApiKey] = useState(settings?.XAIApiKey);\n\n  return (\n    <div className=\"flex gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          xAI API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"XAIApiKey\"\n          className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"xAI API Key\"\n          defaultValue={settings?.XAIApiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n          onChange={(e) => setInputValue(e.target.value)}\n          onBlur={() => setApiKey(inputValue)}\n        />\n      </div>\n\n      {!settings?.credentialsOnly && (\n        <XAIModelSelection settings={settings} apiKey={apiKey} />\n      )}\n    </div>\n  );\n}\n\nfunction XAIModelSelection({ apiKey, settings }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!apiKey) {\n        setCustomModels([]);\n        setLoading(true);\n        return;\n      }\n\n      try {\n        setLoading(true);\n        const { models } = await System.customModels(\"xai\", apiKey);\n        setCustomModels(models || []);\n      } catch (error) {\n        console.error(\"Failed to fetch custom models:\", error);\n        setCustomModels([]);\n      } finally {\n        setLoading(false);\n      }\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"XAIModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg text-theme-text-primary border-theme-border text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            --loading available models--\n          </option>\n        </select>\n        <p className=\"text-xs leading-[18px] font-base text-theme-text-primary opacity-60 mt-2\">\n          Enter a valid API key to view all available models for your account.\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"XAIModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg text-theme-text-primary border-theme-border text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Available models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.XAIModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n      <p className=\"text-xs leading-[18px] font-base text-theme-text-primary opacity-60 mt-2\">\n        Select the xAI model you want to use for your conversations.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/LLMSelection/ZAiLLMOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function ZAiLLMOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.ZAiApiKey);\n  const [apiKey, setApiKey] = useState(settings?.ZAiApiKey);\n\n  return (\n    <div className=\"flex gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Z.AI API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"ZAiApiKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"Z.AI API Key\"\n          defaultValue={settings?.ZAiApiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n          onChange={(e) => setInputValue(e.target.value)}\n          onBlur={() => setApiKey(inputValue)}\n        />\n      </div>\n\n      {!settings?.credentialsOnly && (\n        <ZAiModelSelection settings={settings} apiKey={apiKey} />\n      )}\n    </div>\n  );\n}\n\nfunction ZAiModelSelection({ apiKey, settings }) {\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      if (!apiKey) {\n        setCustomModels([]);\n        setLoading(true);\n        return;\n      }\n\n      try {\n        setLoading(true);\n        const { models } = await System.customModels(\"zai\", apiKey);\n        setCustomModels(models || []);\n      } catch (error) {\n        console.error(\"Failed to fetch custom models:\", error);\n        setCustomModels([]);\n      } finally {\n        setLoading(false);\n      }\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"ZAiModelPref\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            --loading available models--\n          </option>\n        </select>\n        <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n          Enter a valid API key to view all available models for your account.\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"ZAiModelPref\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {customModels.length > 0 && (\n          <optgroup label=\"Available models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={settings?.ZAiModelPref === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n      </select>\n      <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n        Select the Z.AI model you want to use for your conversations.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/ModalWrapper/index.jsx",
    "content": "import { createPortal } from \"react-dom\";\n/**\n * @typedef {Object} ModalWrapperProps\n * @property {import(\"react\").ReactComponentElement} children - The DOM/JSX to render\n * @property {boolean} isOpen - Option that renders the modal\n * @property {boolean} noPortal - (default: false) Used for creating sub-DOM modals that need to be rendered as a child element instead of a modal placed at the root\n * Note: This can impact the bg-overlay presentation due to conflicting DOM positions so if using this property you should\n   double check it renders as desired.\n */\n\n/**\n *\n * @param {ModalWrapperProps} props - ModalWrapperProps to pass\n * @returns {import(\"react\").ReactNode}\n *\n * @todo Add a closeModal prop to the ModalWrapper component so we can escape dismiss anywhere this is used\n */\nexport default function ModalWrapper({ children, isOpen, noPortal = false }) {\n  if (!isOpen) return null;\n\n  if (noPortal) {\n    return (\n      <div className=\"bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center z-99\">\n        {children}\n      </div>\n    );\n  }\n\n  return createPortal(\n    <div className=\"bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center z-99\">\n      {children}\n    </div>,\n    document.getElementById(\"root\")\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/DisplayRecoveryCodeModal/index.jsx",
    "content": "import showToast from \"@/utils/toast\";\nimport { DownloadSimple, Key } from \"@phosphor-icons/react\";\nimport { saveAs } from \"file-saver\";\nimport { useState } from \"react\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\n\nexport default function RecoveryCodeModal({\n  recoveryCodes,\n  onDownloadComplete,\n  onClose,\n}) {\n  const [downloadClicked, setDownloadClicked] = useState(false);\n\n  const downloadRecoveryCodes = () => {\n    const blob = new Blob([recoveryCodes.join(\"\\n\")], { type: \"text/plain\" });\n    saveAs(blob, \"recovery_codes.txt\");\n    setDownloadClicked(true);\n  };\n\n  const handleClose = () => {\n    if (downloadClicked) {\n      onDownloadComplete();\n      onClose();\n    }\n  };\n\n  const handleCopyToClipboard = () => {\n    navigator.clipboard.writeText(recoveryCodes.join(\",\\n\")).then(() => {\n      showToast(\"Recovery codes copied to clipboard\", \"success\", {\n        clear: true,\n      });\n    });\n  };\n\n  return (\n    <ModalWrapper isOpen={true}>\n      <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <Key size={24} className=\"text-white\" weight=\"bold\" />\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Recovery Codes\n            </h3>\n          </div>\n        </div>\n        <div\n          className=\"h-full w-full overflow-y-auto\"\n          style={{ maxHeight: \"calc(100vh - 200px)\" }}\n        >\n          <div className=\"py-7 px-9 space-y-2 flex-col\">\n            <p className=\"text-sm text-white flex flex-col\">\n              In order to reset your password in the future, you will need these\n              recovery codes. Download or copy your recovery codes to save them.{\" \"}\n              <br />\n              <b className=\"mt-4\">These recovery codes are only shown once!</b>\n            </p>\n            <div\n              className=\"border-none bg-theme-settings-input-bg text-white hover:text-primary-button\n                   flex items-center justify-center rounded-md mt-6 cursor-pointer\"\n              onClick={handleCopyToClipboard}\n            >\n              <ul className=\"space-y-2 md:p-6 p-4\">\n                {recoveryCodes.map((code, index) => (\n                  <li key={index} className=\"md:text-sm text-xs\">\n                    {code}\n                  </li>\n                ))}\n              </ul>\n            </div>\n          </div>\n          <div className=\"flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b\">\n            <button\n              type=\"button\"\n              className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm flex items-center gap-x-2\"\n              onClick={downloadClicked ? handleClose : downloadRecoveryCodes}\n            >\n              {downloadClicked ? (\n                \"Close\"\n              ) : (\n                <>\n                  <DownloadSimple weight=\"bold\" size={18} />\n                  <p>Download</p>\n                </>\n              )}\n            </button>\n          </div>\n        </div>\n      </div>\n    </ModalWrapper>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/DataConnectors/ConnectorOption/index.jsx",
    "content": "export default function ConnectorOption({\n  slug,\n  selectedConnector,\n  setSelectedConnector,\n  image,\n  name,\n  description,\n}) {\n  return (\n    <button\n      onClick={() => setSelectedConnector(slug)}\n      className={`border-none flex text-left gap-x-3.5 items-center py-2 px-4 hover:bg-theme-file-picker-hover ${\n        selectedConnector === slug ? \"bg-theme-file-picker-hover\" : \"\"\n      } rounded-lg cursor-pointer w-full`}\n    >\n      <img src={image} alt={name} className=\"w-[40px] h-[40px] rounded-md\" />\n      <div className=\"flex flex-col\">\n        <div className=\"text-white font-bold text-[14px]\">{name}</div>\n        <div>\n          <p className=\"text-[12px] text-white/60\">{description}</p>\n        </div>\n      </div>\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/Confluence/index.jsx",
    "content": "import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { Warning } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nexport default function ConfluenceOptions() {\n  const { t } = useTranslation();\n  const [loading, setLoading] = useState(false);\n  const [accessType, setAccessType] = useState(\"username\");\n  const [isCloud, setIsCloud] = useState(true);\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = new FormData(e.target);\n\n    try {\n      setLoading(true);\n      showToast(\n        \"Fetching all pages for Confluence space - this may take a while.\",\n        \"info\",\n        {\n          clear: true,\n          autoClose: false,\n        }\n      );\n      const { data, error } = await System.dataConnectors.confluence.collect({\n        baseUrl: form.get(\"baseUrl\"),\n        spaceKey: form.get(\"spaceKey\"),\n        username: form.get(\"username\"),\n        accessToken: form.get(\"accessToken\"),\n        cloud: form.get(\"isCloud\") === \"true\",\n        personalAccessToken: form.get(\"personalAccessToken\"),\n        bypassSSL: form.get(\"bypassSSL\") === \"true\",\n      });\n\n      if (!!error) {\n        showToast(error, \"error\", { clear: true });\n        setLoading(false);\n        return;\n      }\n\n      showToast(\n        `Pages collected from Confluence space ${data.spaceKey}. Output folder is ${data.destination}.`,\n        \"success\",\n        { clear: true }\n      );\n      e.target.reset();\n      setLoading(false);\n    } catch (e) {\n      console.error(e);\n      showToast(e.message, \"error\", { clear: true });\n      setLoading(false);\n    }\n  };\n\n  return (\n    <div className=\"flex w-full\">\n      <div className=\"flex flex-col w-full px-1 md:pb-6 pb-16\">\n        <form className=\"w-full\" onSubmit={handleSubmit}>\n          <div className=\"w-full flex flex-col py-2\">\n            <div className=\"w-full flex flex-col gap-4\">\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold flex gap-x-2 items-center\">\n                    <p className=\"font-bold text-theme-text-primary\">\n                      {t(\"connectors.confluence.deployment_type\")}\n                    </p>\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    {t(\"connectors.confluence.deployment_type_explained\")}\n                  </p>\n                </div>\n                <select\n                  name=\"isCloud\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  required={true}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                  defaultValue=\"true\"\n                  onChange={(e) => setIsCloud(e.target.value === \"true\")}\n                >\n                  <option value=\"true\">Atlassian Cloud</option>\n                  <option value=\"false\">Self-hosted</option>\n                </select>\n              </div>\n\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold flex gap-x-2 items-center\">\n                    <p className=\"font-bold text-white\">\n                      {t(\"connectors.confluence.base_url\")}\n                    </p>\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    {t(\"connectors.confluence.base_url_explained\")}\n                  </p>\n                </div>\n                <input\n                  type=\"url\"\n                  name=\"baseUrl\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"eg: https://example.atlassian.net, http://localhost:8211, etc...\"\n                  required={true}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                />\n              </div>\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold\">\n                    {t(\"connectors.confluence.space_key\")}\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    {t(\"connectors.confluence.space_key_explained\")}\n                  </p>\n                </div>\n                <input\n                  type=\"text\"\n                  name=\"spaceKey\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"eg: ~7120208c08555d52224113949698b933a3bb56\"\n                  required={true}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                />\n              </div>\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold\">\n                    {t(\"connectors.confluence.auth_type\")}\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    {t(\"connectors.confluence.auth_type_explained\")}\n                  </p>\n                </div>\n                <select\n                  name=\"accessType\"\n                  className=\"border-none bg-theme-settings-input-bg w-fit mt-2 px-4 border-gray-500 text-white text-sm rounded-lg block py-2\"\n                  defaultValue={accessType}\n                  onChange={(e) => setAccessType(e.target.value)}\n                >\n                  {[\n                    {\n                      name: t(\"connectors.confluence.auth_type_username\"),\n                      value: \"username\",\n                    },\n                    {\n                      name: t(\"connectors.confluence.auth_type_personal\"),\n                      value: \"personalToken\",\n                    },\n                  ].map((type) => {\n                    return (\n                      <option key={type.value} value={type.value}>\n                        {type.name}\n                      </option>\n                    );\n                  })}\n                </select>\n              </div>\n              {accessType === \"username\" && (\n                <>\n                  <div className=\"flex flex-col pr-10\">\n                    <div className=\"flex flex-col gap-y-1 mb-4\">\n                      <label className=\"text-white text-sm font-bold\">\n                        {t(\"connectors.confluence.username\")}\n                      </label>\n                      <p className=\"text-xs font-normal text-theme-text-secondary\">\n                        {t(\"connectors.confluence.username_explained\")}\n                      </p>\n                    </div>\n                    <input\n                      type=\"text\"\n                      name=\"username\"\n                      className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                      placeholder=\"jdoe@example.com\"\n                      required={true}\n                      autoComplete=\"off\"\n                      spellCheck={false}\n                    />\n                  </div>\n                  <div className=\"flex flex-col pr-10\">\n                    <div className=\"flex flex-col gap-y-1 mb-4\">\n                      <label className=\"text-white text-sm font-bold flex gap-x-2 items-center\">\n                        <p className=\"font-bold text-white\">\n                          {t(\"connectors.confluence.token\")}\n                        </p>\n                        <Warning\n                          size={14}\n                          className=\"ml-1 text-orange-500 cursor-pointer\"\n                          data-tooltip-id=\"access-token-tooltip\"\n                          data-tooltip-place=\"right\"\n                        />\n                        <Tooltip\n                          delayHide={300}\n                          id=\"access-token-tooltip\"\n                          className=\"max-w-xs z-99\"\n                          clickable={true}\n                        >\n                          <p className=\"text-sm\">\n                            {t(\"connectors.confluence.token_explained_start\")}\n                            <a\n                              href=\"https://id.atlassian.com/manage-profile/security/api-tokens\"\n                              target=\"_blank\"\n                              rel=\"noopener noreferrer\"\n                              className=\"underline\"\n                              onClick={(e) => e.stopPropagation()}\n                            >\n                              {t(\"connectors.confluence.token_explained_link\")}\n                            </a>\n                            .\n                          </p>\n                        </Tooltip>\n                      </label>\n                      <p className=\"text-xs font-normal text-theme-text-secondary\">\n                        {t(\"connectors.confluence.token_desc\")}\n                      </p>\n                    </div>\n                    <input\n                      type=\"password\"\n                      name=\"accessToken\"\n                      className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                      placeholder=\"abcd1234\"\n                      required={true}\n                      autoComplete=\"off\"\n                      spellCheck={false}\n                    />\n                  </div>\n                </>\n              )}\n              {accessType === \"personalToken\" && (\n                <div className=\"flex flex-col pr-10\">\n                  <div className=\"flex flex-col gap-y-1 mb-4\">\n                    <label className=\"text-white text-sm font-bold\">\n                      {t(\"connectors.confluence.pat_token\")}\n                    </label>\n                    <p className=\"text-xs font-normal text-theme-text-secondary\">\n                      {t(\"connectors.confluence.pat_token_explained\")}\n                    </p>\n                  </div>\n                  <input\n                    type=\"password\"\n                    name=\"personalAccessToken\"\n                    className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                    placeholder=\"abcd1234\"\n                    required={true}\n                    autoComplete=\"off\"\n                    spellCheck={false}\n                  />\n                </div>\n              )}\n            </div>\n          </div>\n\n          {!isCloud && (\n            <div className=\"w-full flex flex-col py-2\">\n              <div className=\"w-full flex flex-col gap-4\">\n                <div className=\"flex flex-col pr-10\">\n                  <div className=\"flex flex-col gap-y-1 mb-4\">\n                    <label className=\"text-white text-sm font-bold flex gap-x-2 items-center\">\n                      <Toggle size=\"md\" name=\"bypassSSL\" value=\"true\" />\n                      <p className=\"font-bold text-theme-text-primary\">\n                        {t(\"connectors.confluence.bypass_ssl\")}\n                      </p>\n                    </label>\n                    <p className=\"text-xs font-normal text-theme-text-secondary\">\n                      {t(\"connectors.confluence.bypass_ssl_explained\")}\n                    </p>\n                  </div>\n                </div>\n              </div>\n            </div>\n          )}\n\n          <div className=\"flex flex-col gap-y-2 w-full pr-10\">\n            <button\n              type=\"submit\"\n              disabled={loading}\n              className=\"mt-2 w-full justify-center border-none px-4 py-2 rounded-lg text-dark-text light:text-white text-sm font-bold items-center flex gap-x-2 bg-theme-home-button-primary hover:bg-theme-home-button-primary-hover disabled:bg-theme-home-button-primary-hover disabled:cursor-not-allowed\"\n            >\n              {loading ? \"Collecting pages...\" : \"Submit\"}\n            </button>\n            {loading && (\n              <p className=\"text-xs text-theme-text-secondary\">\n                {t(\"connectors.confluence.task_explained\")}\n              </p>\n            )}\n          </div>\n        </form>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/DrupalWiki/index.jsx",
    "content": "/**\n * Copyright 2024\n *\n * Authors:\n *  - Eugen Mayer (KontextWork)\n */\n\nimport { useState } from \"react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { Warning } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\n\nexport default function DrupalWikiOptions() {\n  const [loading, setLoading] = useState(false);\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = new FormData(e.target);\n\n    try {\n      setLoading(true);\n      showToast(\n        \"Fetching all pages for the given Drupal Wiki spaces - this may take a while.\",\n        \"info\",\n        {\n          clear: true,\n          autoClose: false,\n        }\n      );\n      const { data, error } = await System.dataConnectors.drupalwiki.collect({\n        baseUrl: form.get(\"baseUrl\"),\n        spaceIds: form.get(\"spaceIds\"),\n        accessToken: form.get(\"accessToken\"),\n      });\n\n      if (!!error) {\n        showToast(error, \"error\", { clear: true });\n        setLoading(false);\n        return;\n      }\n\n      showToast(\n        `Pages collected from Drupal Wiki spaces ${data.spaceIds}. Output folder is ${data.destination}.`,\n        \"success\",\n        { clear: true }\n      );\n      e.target.reset();\n      setLoading(false);\n    } catch (e) {\n      console.error(e);\n      showToast(e.message, \"error\", { clear: true });\n      setLoading(false);\n    }\n  };\n\n  return (\n    <div className=\"flex w-full\">\n      <div className=\"flex flex-col w-full px-1 md:pb-6 pb-16\">\n        <form className=\"w-full\" onSubmit={handleSubmit}>\n          <div className=\"w-full flex flex-col py-2\">\n            <div className=\"w-full flex flex-col gap-4\">\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold flex gap-x-2 items-center\">\n                    <p className=\"font-bold text-white\">Drupal Wiki base URL</p>\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    This is the base URL of your&nbsp;\n                    <a\n                      href=\"https://drupal-wiki.com\"\n                      target=\"_blank\"\n                      rel=\"noopener noreferrer\"\n                      className=\"underline\"\n                    >\n                      Drupal Wiki\n                    </a>\n                    .\n                  </p>\n                </div>\n                <input\n                  type=\"url\"\n                  name=\"baseUrl\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"eg: https://mywiki.drupal-wiki.net, https://drupalwiki.mycompany.tld, etc...\"\n                  required={true}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                />\n              </div>\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold\">\n                    Drupal Wiki Space IDs\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    Comma separated Space IDs you want to extract. See the&nbsp;\n                    <a\n                      href=\"https://help.drupal-wiki.com/node/606\"\n                      target=\"_blank\"\n                      rel=\"noopener noreferrer\"\n                      className=\"underline\"\n                      onClick={(e) => e.stopPropagation()}\n                    >\n                      manual\n                    </a>\n                    &nbsp; on how to retrieve the Space IDs. Be sure that your\n                    'API-Token User' has access to those spaces.\n                  </p>\n                </div>\n                <input\n                  type=\"text\"\n                  name=\"spaceIds\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"eg: 12,34,69\"\n                  required={true}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                />\n              </div>\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold flex gap-x-2 items-center\">\n                    <p className=\"font-bold text-white\">\n                      Drupal Wiki API Token\n                    </p>\n                    <Warning\n                      size={14}\n                      className=\"ml-1 text-orange-500 cursor-pointer\"\n                      data-tooltip-id=\"access-token-tooltip\"\n                      data-tooltip-place=\"right\"\n                    />\n                    <Tooltip\n                      delayHide={300}\n                      id=\"access-token-tooltip\"\n                      className=\"max-w-xs z-99\"\n                      clickable={true}\n                    >\n                      <p className=\"text-sm font-light text-theme-text-primary\">\n                        You need to provide an API token for authentication. See\n                        the Drupal Wiki&nbsp;\n                        <a\n                          href=\"https://help.drupal-wiki.com/node/605#2-Zugriffs-Token-generieren\"\n                          target=\"_blank\"\n                          rel=\"noopener noreferrer\"\n                          className=\"underline\"\n                        >\n                          manual\n                        </a>\n                        &nbsp;on how to generate an API-Token for your user.\n                      </p>\n                    </Tooltip>\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    Access token for authentication.\n                  </p>\n                </div>\n                <input\n                  type=\"password\"\n                  name=\"accessToken\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"pat:123\"\n                  required={true}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                />\n              </div>\n            </div>\n          </div>\n\n          <div className=\"flex flex-col gap-y-2 w-full pr-10\">\n            <button\n              type=\"submit\"\n              disabled={loading}\n              className=\"mt-2 w-full justify-center border-none px-4 py-2 rounded-lg text-dark-text light:text-white text-sm font-bold items-center flex gap-x-2 bg-theme-home-button-primary hover:bg-theme-home-button-primary-hover disabled:bg-theme-home-button-primary-hover disabled:cursor-not-allowed\"\n            >\n              {loading ? \"Collecting pages...\" : \"Submit\"}\n            </button>\n            {loading && (\n              <p className=\"text-xs text-theme-text-secondary\">\n                Once complete, all pages will be available for embedding into\n                workspaces.\n              </p>\n            )}\n          </div>\n        </form>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/Github/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport System from \"@/models/system\";\nimport { useTranslation } from \"react-i18next\";\nimport showToast from \"@/utils/toast\";\nimport pluralize from \"pluralize\";\nimport { TagsInput } from \"react-tag-input-component\";\nimport { Info, Warning } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\n\nconst DEFAULT_BRANCHES = [\"main\", \"master\"];\nexport default function GithubOptions() {\n  const { t } = useTranslation();\n  const [loading, setLoading] = useState(false);\n  const [repo, setRepo] = useState(null);\n  const [accessToken, setAccessToken] = useState(null);\n  const [ignores, setIgnores] = useState([]);\n\n  const [settings, setSettings] = useState({\n    repo: null,\n    accessToken: null,\n  });\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = new FormData(e.target);\n\n    try {\n      setLoading(true);\n      showToast(\n        \"Fetching all files for repo - this may take a while.\",\n        \"info\",\n        { clear: true, autoClose: false }\n      );\n      const { data, error } = await System.dataConnectors.github.collect({\n        repo: form.get(\"repo\"),\n        accessToken: form.get(\"accessToken\"),\n        branch: form.get(\"branch\"),\n        ignorePaths: ignores,\n      });\n\n      if (!!error) {\n        showToast(error, \"error\", { clear: true });\n        setLoading(false);\n        return;\n      }\n\n      showToast(\n        `${data.files} ${pluralize(\"file\", data.files)} collected from ${\n          data.author\n        }/${data.repo}:${data.branch}. Output folder is ${data.destination}.`,\n        \"success\",\n        { clear: true }\n      );\n      e.target.reset();\n      setLoading(false);\n      return;\n    } catch (e) {\n      console.error(e);\n      showToast(e.message, \"error\", { clear: true });\n      setLoading(false);\n    }\n  };\n\n  return (\n    <div className=\"flex w-full\">\n      <div className=\"flex flex-col w-full px-1 md:pb-6 pb-16\">\n        <form className=\"w-full\" onSubmit={handleSubmit}>\n          <div className=\"w-full flex flex-col py-2\">\n            <div className=\"w-full flex flex-col gap-4\">\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold\">\n                    {t(\"connectors.github.URL\")}\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    {t(\"connectors.github.URL_explained\")}\n                  </p>\n                </div>\n                <input\n                  type=\"url\"\n                  name=\"repo\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"https://github.com/Mintplex-Labs/anything-llm\"\n                  required={true}\n                  autoComplete=\"off\"\n                  onChange={(e) => setRepo(e.target.value)}\n                  onBlur={() => setSettings({ ...settings, repo })}\n                  spellCheck={false}\n                />\n              </div>\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white font-bold text-sm flex gap-x-2 items-center\">\n                    <p className=\"font-bold text-white\">\n                      {t(\"connectors.github.token\")}\n                    </p>{\" \"}\n                    <p className=\"text-xs font-light flex items-center\">\n                      <span className=\"text-theme-text-secondary\">\n                        {t(\"connectors.github.optional\")}\n                      </span>\n                      <PATTooltip accessToken={accessToken} />\n                    </p>\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    {t(\"connectors.github.token_explained\")}\n                  </p>\n                </div>\n                <input\n                  type=\"text\"\n                  name=\"accessToken\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"github_pat_1234_abcdefg\"\n                  required={false}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                  onChange={(e) => setAccessToken(e.target.value)}\n                  onBlur={() => setSettings({ ...settings, accessToken })}\n                />\n              </div>\n              <GitHubBranchSelection\n                repo={settings.repo}\n                accessToken={settings.accessToken}\n              />\n            </div>\n\n            <div className=\"flex flex-col w-full py-4 pr-10\">\n              <div className=\"flex flex-col gap-y-1 mb-4\">\n                <label className=\"text-white text-sm flex gap-x-2 items-center\">\n                  <p className=\"text-white text-sm font-bold\">\n                    {t(\"connectors.github.ignores\")}\n                  </p>\n                </label>\n                <p className=\"text-xs font-normal text-theme-text-secondary\">\n                  {t(\"connectors.github.git_ignore\")}\n                </p>\n              </div>\n              <TagsInput\n                value={ignores}\n                onChange={setIgnores}\n                name=\"ignores\"\n                placeholder=\"!*.js, images/*, .DS_Store, bin/*\"\n                classNames={{\n                  tag: \"bg-theme-settings-input-bg light:bg-black/10 bg-blue-300/10 text-zinc-800\",\n                  input:\n                    \"flex p-1 !bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none\",\n                }}\n              />\n            </div>\n          </div>\n\n          <div className=\"flex flex-col gap-y-2 w-full pr-10\">\n            <PATAlert accessToken={accessToken} />\n            <button\n              type=\"submit\"\n              disabled={loading}\n              className=\"mt-2 w-full justify-center border-none px-4 py-2 rounded-lg text-dark-text light:text-white text-sm font-bold items-center flex gap-x-2 bg-theme-home-button-primary hover:bg-theme-home-button-primary-hover disabled:bg-theme-home-button-primary-hover disabled:cursor-not-allowed\"\n            >\n              {loading ? \"Collecting files...\" : \"Submit\"}\n            </button>\n            {loading && (\n              <p className=\"text-xs text-white/50\">\n                {t(\"connectors.github.task_explained\")}\n              </p>\n            )}\n          </div>\n        </form>\n      </div>\n    </div>\n  );\n}\n\nfunction GitHubBranchSelection({ repo, accessToken }) {\n  const { t } = useTranslation();\n  const [allBranches, setAllBranches] = useState(DEFAULT_BRANCHES);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function fetchAllBranches() {\n      if (!repo) {\n        setAllBranches(DEFAULT_BRANCHES);\n        setLoading(false);\n        return;\n      }\n\n      setLoading(true);\n      const { branches } = await System.dataConnectors.github.branches({\n        repo,\n        accessToken,\n      });\n      setAllBranches(branches.length > 0 ? branches : DEFAULT_BRANCHES);\n      setLoading(false);\n    }\n    fetchAllBranches();\n  }, [repo, accessToken]);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <div className=\"flex flex-col gap-y-1 mb-4\">\n          <label className=\"text-white text-sm font-bold\">Branch</label>\n          <p className=\"text-xs font-normal text-theme-text-secondary\">\n            {t(\"connectors.github.branch\")}\n          </p>\n        </div>\n        <select\n          name=\"branch\"\n          required={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white focus:outline-primary-button active:outline-primary-button outline-none text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {t(\"connectors.github.branch_loading\")}\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <div className=\"flex flex-col gap-y-1 mb-4\">\n        <label className=\"text-white text-sm font-bold\">Branch</label>\n        <p className=\"text-xs font-normal text-theme-text-secondary\">\n          {t(\"connectors.github.branch_explained\")}\n        </p>\n      </div>\n      <select\n        name=\"branch\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white focus:outline-primary-button active:outline-primary-button outline-none text-sm rounded-lg block w-full p-2.5\"\n      >\n        {allBranches.map((branch) => {\n          return (\n            <option key={branch} value={branch}>\n              {branch}\n            </option>\n          );\n        })}\n      </select>\n    </div>\n  );\n}\n\nfunction PATAlert({ accessToken }) {\n  const { t } = useTranslation();\n  if (!!accessToken) return null;\n  return (\n    <div className=\"flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2\">\n      <div className=\"gap-x-2 flex items-center\">\n        <Info className=\"shrink-0\" size={25} />\n        <p className=\"text-sm\">\n          <span\n            dangerouslySetInnerHTML={{\n              __html: t(\"connectors.github.token_information\"),\n            }}\n          />\n          <br />\n          <br />\n          <a\n            href=\"https://github.com/settings/personal-access-tokens/new\"\n            rel=\"noreferrer\"\n            target=\"_blank\"\n            className=\"underline\"\n            onClick={(e) => e.stopPropagation()}\n          >\n            {\" \"}\n            {t(\"connectors.github.token_personal\")}\n          </a>\n        </p>\n      </div>\n    </div>\n  );\n}\n\nfunction PATTooltip({ accessToken }) {\n  const { t } = useTranslation();\n  if (!!accessToken) return null;\n  return (\n    <>\n      {!accessToken && (\n        <Warning\n          size={14}\n          className=\"ml-1 text-orange-500 cursor-pointer\"\n          data-tooltip-id=\"access-token-tooltip\"\n          data-tooltip-place=\"right\"\n        />\n      )}\n      <Tooltip\n        delayHide={300}\n        id=\"access-token-tooltip\"\n        className=\"max-w-xs z-99\"\n        clickable={true}\n      >\n        <p className=\"text-sm\">\n          {t(\"connectors.github.token_explained_start\")}\n          <a\n            href=\"https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens\"\n            rel=\"noreferrer\"\n            target=\"_blank\"\n            className=\"underline\"\n            onClick={(e) => e.stopPropagation()}\n          >\n            {t(\"connectors.github.token_explained_link1\")}\n          </a>\n          {t(\"connectors.github.token_explained_middle\")}\n          <a\n            href=\"https://github.com/settings/personal-access-tokens/new\"\n            rel=\"noreferrer\"\n            target=\"_blank\"\n            className=\"underline\"\n            onClick={(e) => e.stopPropagation()}\n          >\n            {t(\"connectors.github.token_explained_link2\")}\n          </a>\n          {t(\"connectors.github.token_explained_end\")}\n        </p>\n      </Tooltip>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/Gitlab/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport pluralize from \"pluralize\";\nimport { TagsInput } from \"react-tag-input-component\";\nimport { Info, Warning } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\nimport { useTranslation } from \"react-i18next\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nconst DEFAULT_BRANCHES = [\"main\", \"master\"];\nexport default function GitlabOptions() {\n  const { t } = useTranslation();\n  const [loading, setLoading] = useState(false);\n  const [repo, setRepo] = useState(null);\n  const [accessToken, setAccessToken] = useState(null);\n  const [ignores, setIgnores] = useState([]);\n  const [settings, setSettings] = useState({\n    repo: null,\n    accessToken: null,\n  });\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = new FormData(e.target);\n\n    try {\n      setLoading(true);\n      showToast(\n        \"Fetching all files for repo - this may take a while.\",\n        \"info\",\n        { clear: true, autoClose: false }\n      );\n      const { data, error } = await System.dataConnectors.gitlab.collect({\n        repo: form.get(\"repo\"),\n        accessToken: form.get(\"accessToken\"),\n        branch: form.get(\"branch\"),\n        ignorePaths: ignores,\n        fetchIssues: form.get(\"fetchIssues\"),\n        fetchWikis: form.get(\"fetchWikis\"),\n      });\n\n      if (!!error) {\n        showToast(error, \"error\", { clear: true });\n        setLoading(false);\n        return;\n      }\n\n      showToast(\n        `${data.files} ${pluralize(\"file\", data.files)} collected from ${\n          data.author\n        }/${data.repo}:${data.branch}. Output folder is ${data.destination}.`,\n        \"success\",\n        { clear: true }\n      );\n      e.target.reset();\n      setLoading(false);\n      return;\n    } catch (e) {\n      console.error(e);\n      showToast(e.message, \"error\", { clear: true });\n      setLoading(false);\n    }\n  };\n\n  return (\n    <div className=\"flex w-full\">\n      <div className=\"flex flex-col w-full px-1 md:pb-6 pb-16\">\n        <form className=\"w-full\" onSubmit={handleSubmit}>\n          <div className=\"w-full flex flex-col py-2\">\n            <div className=\"w-full flex flex-col gap-4\">\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold\">\n                    {t(\"connectors.gitlab.URL\")}\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    {t(\"connectors.gitlab.URL_explained\")}\n                  </p>\n                </div>\n                <input\n                  type=\"url\"\n                  name=\"repo\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"https://gitlab.com/gitlab-org/gitlab\"\n                  required={true}\n                  autoComplete=\"off\"\n                  onChange={(e) => setRepo(e.target.value)}\n                  onBlur={() => setSettings({ ...settings, repo })}\n                  spellCheck={false}\n                />\n              </div>\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white font-bold text-sm flex gap-x-2 items-center\">\n                    <p className=\"font-bold text-white\">\n                      {t(\"connectors.gitlab.token\")}\n                    </p>{\" \"}\n                    <p className=\"text-xs font-light flex items-center\">\n                      <span className=\"text-theme-text-secondary\">\n                        {t(\"connectors.gitlab.optional\")}\n                      </span>\n                      <PATTooltip accessToken={accessToken} />\n                    </p>\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    {t(\"connectors.gitlab.token_description\")}\n                  </p>\n                </div>\n                <input\n                  type=\"text\"\n                  name=\"accessToken\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"glpat-XXXXXXXXXXXXXXXXXXXX\"\n                  required={false}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                  onChange={(e) => setAccessToken(e.target.value)}\n                  onBlur={() => setSettings({ ...settings, accessToken })}\n                />\n              </div>\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white font-bold text-sm flex gap-x-2 items-center\">\n                    <p className=\"font-bold text-white\">Settings</p>\n                  </label>\n                  <p className=\"text-xs font-normal text-white\">\n                    {t(\"connectors.gitlab.token_description\")}\n                  </p>\n                </div>\n                <div className=\"flex items-center gap-x-2 mb-3\">\n                  <Toggle\n                    name=\"fetchIssues\"\n                    size=\"md\"\n                    label={t(\"connectors.gitlab.fetch_issues\")}\n                  />\n                </div>\n                <div className=\"flex items-center gap-x-2\">\n                  <Toggle\n                    name=\"fetchWikis\"\n                    size=\"md\"\n                    label=\"Fetch Wikis as Documents\"\n                  />\n                </div>\n              </div>\n              <GitLabBranchSelection\n                repo={settings.repo}\n                accessToken={settings.accessToken}\n              />\n            </div>\n\n            <div className=\"flex flex-col w-full py-4 pr-10\">\n              <div className=\"flex flex-col gap-y-1 mb-4\">\n                <label className=\"text-white text-sm flex gap-x-2 items-center\">\n                  <p className=\"text-white text-sm font-bold\">\n                    {t(\"connectors.gitlab.ignores\")}\n                  </p>\n                </label>\n                <p className=\"text-xs font-normal text-theme-text-secondary\">\n                  {t(\"connectors.gitlab.git_ignore\")}\n                </p>\n              </div>\n              <TagsInput\n                value={ignores}\n                onChange={setIgnores}\n                name=\"ignores\"\n                placeholder=\"!*.js, images/*, .DS_Store, bin/*\"\n                classNames={{\n                  tag: \"bg-theme-settings-input-bg light:bg-black/10 bg-blue-300/10 text-zinc-800\",\n                  input:\n                    \"flex p-1 !bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none\",\n                }}\n              />\n            </div>\n          </div>\n\n          <div className=\"flex flex-col gap-y-2 w-full pr-10\">\n            <PATAlert accessToken={accessToken} />\n            <button\n              type=\"submit\"\n              disabled={loading}\n              className=\"mt-2 w-full justify-center border-none px-4 py-2 rounded-lg text-dark-text light:text-white text-sm font-bold items-center flex gap-x-2 bg-theme-home-button-primary hover:bg-theme-home-button-primary-hover disabled:bg-theme-home-button-primary-hover disabled:cursor-not-allowed\"\n            >\n              {loading ? \"Collecting files...\" : \"Submit\"}\n            </button>\n            {loading && (\n              <p className=\"text-xs text-white/50\">\n                {t(\"connectors.gitlab.task_explained\")}\n              </p>\n            )}\n          </div>\n        </form>\n      </div>\n    </div>\n  );\n}\n\nfunction GitLabBranchSelection({ repo, accessToken }) {\n  const { t } = useTranslation();\n  const [allBranches, setAllBranches] = useState(DEFAULT_BRANCHES);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function fetchAllBranches() {\n      if (!repo) {\n        setAllBranches(DEFAULT_BRANCHES);\n        setLoading(false);\n        return;\n      }\n\n      setLoading(true);\n      const { branches } = await System.dataConnectors.gitlab.branches({\n        repo,\n        accessToken,\n      });\n      setAllBranches(branches.length > 0 ? branches : DEFAULT_BRANCHES);\n      setLoading(false);\n    }\n    fetchAllBranches();\n  }, [repo, accessToken]);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <div className=\"flex flex-col gap-y-1 mb-4\">\n          <label className=\"text-white text-sm font-bold\">\n            {t(\"connectors.gitlab.branch\")}\n          </label>\n          <p className=\"text-xs font-normal text-theme-text-secondary\">\n            {t(\"connectors.gitlab.branch_explained\")}\n          </p>\n        </div>\n        <select\n          name=\"branch\"\n          required={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white focus:outline-primary-button active:outline-primary-button outline-none text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {t(\"connectors.gitlab.branch_loading\")}\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <div className=\"flex flex-col gap-y-1 mb-4\">\n        <label className=\"text-white text-sm font-bold\">Branch</label>\n        <p className=\"text-xs font-normal text-theme-text-secondary\">\n          {t(\"connectors.gitlab.branch_explained\")}\n        </p>\n      </div>\n      <select\n        name=\"branch\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white focus:outline-primary-button active:outline-primary-button outline-none text-sm rounded-lg block w-full p-2.5\"\n      >\n        {allBranches.map((branch) => {\n          return (\n            <option key={branch} value={branch}>\n              {branch}\n            </option>\n          );\n        })}\n      </select>\n    </div>\n  );\n}\n\nfunction PATAlert({ accessToken }) {\n  const { t } = useTranslation();\n  if (!!accessToken) return null;\n  return (\n    <div className=\"flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2\">\n      <div className=\"gap-x-2 flex items-center\">\n        <Info className=\"shrink-0\" size={25} />\n        <p className=\"text-sm\">\n          <span\n            dangerouslySetInnerHTML={{\n              __html: t(\"connectors.gitlab.token_information\"),\n            }}\n          />\n          <br />\n          <br />\n          <a\n            href=\"https://gitlab.com/-/user_settings/personal_access_tokens\"\n            rel=\"noreferrer\"\n            target=\"_blank\"\n            className=\"underline\"\n            onClick={(e) => e.stopPropagation()}\n          >\n            {t(\"connectors.gitlab.token_personal\")}\n          </a>\n        </p>\n      </div>\n    </div>\n  );\n}\n\nfunction PATTooltip({ accessToken }) {\n  const { t } = useTranslation();\n  if (!!accessToken) return null;\n  return (\n    <>\n      {!accessToken && (\n        <Warning\n          size={14}\n          className=\"ml-1 text-orange-500 cursor-pointer\"\n          data-tooltip-id=\"access-token-tooltip\"\n          data-tooltip-place=\"right\"\n        />\n      )}\n      <Tooltip\n        delayHide={300}\n        id=\"access-token-tooltip\"\n        className=\"max-w-xs z-99\"\n        clickable={true}\n      >\n        <p className=\"text-sm\">\n          {t(\"connectors.gitlab.token_explained_start\")}\n          <a\n            href=\"https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html\"\n            rel=\"noreferrer\"\n            target=\"_blank\"\n            className=\"underline\"\n            onClick={(e) => e.stopPropagation()}\n          >\n            {t(\"connectors.gitlab.token_explained_link1\")}\n          </a>\n          {t(\"connectors.gitlab.token_explained_middle\")}\n          <a\n            href=\"https://gitlab.com/-/profile/personal_access_tokens\"\n            rel=\"noreferrer\"\n            target=\"_blank\"\n            className=\"underline\"\n            onClick={(e) => e.stopPropagation()}\n          >\n            {t(\"connectors.gitlab.token_explained_link2\")}\n          </a>\n          {t(\"connectors.gitlab.token_explained_end\")}\n        </p>\n      </Tooltip>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/Obsidian/index.jsx",
    "content": "import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { FolderOpen, Info } from \"@phosphor-icons/react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\n\nexport default function ObsidianOptions() {\n  const { t } = useTranslation();\n  const [loading, setLoading] = useState(false);\n  const [vaultPath, setVaultPath] = useState(\"\");\n  const [selectedFiles, setSelectedFiles] = useState([]);\n\n  const handleFolderPick = async (e) => {\n    const files = Array.from(e.target.files);\n    if (files.length === 0) return;\n\n    // Filter for .md files only\n    const markdownFiles = files.filter((file) => file.name.endsWith(\".md\"));\n    setSelectedFiles(markdownFiles);\n\n    // Set the folder path from the first file\n    if (markdownFiles.length > 0) {\n      const path = markdownFiles[0].webkitRelativePath.split(\"/\")[0];\n      setVaultPath(path);\n    }\n  };\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    if (selectedFiles.length === 0) return;\n\n    try {\n      setLoading(true);\n      showToast(\"Importing Obsidian vault - this may take a while.\", \"info\", {\n        clear: true,\n        autoClose: false,\n      });\n\n      // Read all files and prepare them for submission\n      const fileContents = await Promise.all(\n        selectedFiles.map(async (file) => {\n          const content = await file.text();\n          return {\n            name: file.name,\n            path: file.webkitRelativePath,\n            content: content,\n          };\n        })\n      );\n\n      const { data, error } = await System.dataConnectors.obsidian.collect({\n        files: fileContents,\n      });\n\n      if (!!error) {\n        showToast(error, \"error\", { clear: true });\n        setLoading(false);\n        setSelectedFiles([]);\n        setVaultPath(\"\");\n        return;\n      }\n\n      // Show results\n      const successCount = data.processed;\n      const failCount = data.failed;\n      const totalCount = data.total;\n\n      if (successCount === totalCount) {\n        showToast(\n          `Successfully imported ${successCount} files from your vault!`,\n          \"success\",\n          { clear: true }\n        );\n      } else {\n        showToast(\n          `Imported ${successCount} files, ${failCount} failed`,\n          \"warning\",\n          { clear: true }\n        );\n      }\n\n      setLoading(false);\n    } catch (e) {\n      console.error(e);\n      showToast(e.message, \"error\", { clear: true });\n      setLoading(false);\n    }\n  };\n\n  return (\n    <div className=\"flex w-full\">\n      <div className=\"flex flex-col w-full px-1 md:pb-6 pb-16\">\n        <form className=\"w-full\" onSubmit={handleSubmit}>\n          <div className=\"w-full flex flex-col py-2\">\n            <div className=\"w-full flex flex-col gap-4\">\n              <div className=\"flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2\">\n                <div className=\"gap-x-2 flex items-center\">\n                  <Info className=\"shrink-0\" size={25} />\n                  <p className=\"text-sm\">\n                    {t(\"connectors.obsidian.vault_warning\")}\n                  </p>\n                </div>\n              </div>\n\n              <div className=\"flex flex-col\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold\">\n                    {t(\"connectors.obsidian.vault_location\")}\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    {t(\"connectors.obsidian.vault_description\")}\n                  </p>\n                </div>\n                <div className=\"flex gap-x-2\">\n                  <input\n                    type=\"text\"\n                    value={vaultPath}\n                    onChange={(e) => setVaultPath(e.target.value)}\n                    placeholder=\"/path/to/your/vault\"\n                    className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                    required={true}\n                    autoComplete=\"off\"\n                    spellCheck={false}\n                    readOnly\n                  />\n                  <label className=\"px-3 py-2 bg-theme-settings-input-bg border border-none rounded-lg text-white hover:bg-theme-settings-input-bg/80 cursor-pointer\">\n                    <FolderOpen size={20} />\n                    <input\n                      type=\"file\"\n                      webkitdirectory=\"\"\n                      onChange={handleFolderPick}\n                      className=\"hidden\"\n                    />\n                  </label>\n                </div>\n                {selectedFiles.length > 0 && (\n                  <>\n                    <p className=\"text-xs text-white mt-2 font-bold\">\n                      {t(\"connectors.obsidian.selected_files\", {\n                        count: selectedFiles.length,\n                      })}\n                    </p>\n\n                    {selectedFiles.map((file, i) => (\n                      <p key={i} className=\"text-xs text-white mt-2\">\n                        {file.webkitRelativePath}\n                      </p>\n                    ))}\n                  </>\n                )}\n              </div>\n            </div>\n          </div>\n\n          <div className=\"flex flex-col gap-y-2 w-full pr-10\">\n            <button\n              type=\"submit\"\n              disabled={loading || selectedFiles.length === 0}\n              className=\"border-none mt-2 w-full justify-center px-4 py-2 rounded-lg text-dark-text light:text-white text-sm font-bold items-center flex gap-x-2 bg-theme-home-button-primary hover:bg-theme-home-button-primary-hover disabled:bg-theme-home-button-primary-hover disabled:cursor-not-allowed\"\n            >\n              {loading\n                ? t(\"connectors.obsidian.importing\")\n                : t(\"connectors.obsidian.import_vault\")}\n            </button>\n            {loading && (\n              <p className=\"text-xs text-white/50\">\n                {t(\"connectors.obsidian.processing_time\")}\n              </p>\n            )}\n          </div>\n        </form>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/PaperlessNgx/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { Info } from \"@phosphor-icons/react\";\n\nexport default function PaperlessNgxOptions() {\n  const [loading, setLoading] = useState(false);\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = new FormData(e.target);\n\n    try {\n      setLoading(true);\n      showToast(\n        \"Fetching documents from Paperless-ngx - this may take a while.\",\n        \"info\",\n        { clear: true, autoClose: false }\n      );\n\n      const { data, error } = await System.dataConnectors.paperlessNgx.collect({\n        baseUrl: form.get(\"baseUrl\"),\n        apiToken: form.get(\"apiToken\"),\n      });\n\n      if (!!error) {\n        showToast(error, \"error\", { clear: true });\n        setLoading(false);\n        return;\n      }\n\n      showToast(\n        `Successfully imported ${data.files} documents from Paperless-ngx. Output folder is ${data.destination}.`,\n        \"success\",\n        { clear: true }\n      );\n      e.target.reset();\n      setLoading(false);\n    } catch (e) {\n      console.error(e);\n      showToast(e.message, \"error\", { clear: true });\n      setLoading(false);\n    }\n  };\n\n  return (\n    <div className=\"flex w-full\">\n      <div className=\"flex flex-col w-full px-1 md:pb-6 pb-16\">\n        <form className=\"w-full\" onSubmit={handleSubmit}>\n          <div className=\"w-full flex flex-col py-2\">\n            <div className=\"w-full flex flex-col gap-4\">\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold\">\n                    Base URL\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    The URL where your Paperless-ngx instance is running (e.g.,\n                    http://localhost:8000)\n                  </p>\n                </div>\n                <input\n                  type=\"url\"\n                  name=\"baseUrl\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"http://localhost:8000\"\n                  required={true}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                />\n              </div>\n\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold flex gap-x-2 items-center\">\n                    <p className=\"font-bold text-white\">API Token</p>\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    Your Paperless-ngx API token. You can find this under\n                    &apos;My Profile&apos; and then &apos;API Auth Token&apos;.\n                  </p>\n                </div>\n                <input\n                  type=\"password\"\n                  name=\"apiToken\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"Enter your API token\"\n                  required={true}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                />\n              </div>\n            </div>\n          </div>\n\n          <div className=\"flex flex-col gap-y-2 w-full pr-10\">\n            <div className=\"flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2\">\n              <div className=\"gap-x-2 flex items-center\">\n                <Info className=\"shrink-0\" size={25} />\n                <p className=\"text-sm\">\n                  Make sure your Paperless-ngx instance is running and\n                  accessible from this machine.\n                </p>\n              </div>\n            </div>\n            <button\n              type=\"submit\"\n              disabled={loading}\n              className=\"mt-2 w-full justify-center border-none px-4 py-2 rounded-lg text-dark-text light:text-white text-sm font-bold items-center flex gap-x-2 bg-theme-home-button-primary hover:bg-theme-home-button-primary-hover disabled:bg-theme-home-button-primary-hover disabled:cursor-not-allowed\"\n            >\n              {loading ? \"Importing documents...\" : \"Submit\"}\n            </button>\n            {loading && (\n              <p className=\"text-xs text-white/50\">\n                Once complete, all documents will be available for embedding\n                into workspaces.\n              </p>\n            )}\n          </div>\n        </form>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/WebsiteDepth/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport pluralize from \"pluralize\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function WebsiteDepthOptions() {\n  const { t } = useTranslation();\n  const [loading, setLoading] = useState(false);\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = new FormData(e.target);\n\n    try {\n      setLoading(true);\n      showToast(\"Scraping website - this may take a while.\", \"info\", {\n        clear: true,\n        autoClose: false,\n      });\n\n      const { data, error } = await System.dataConnectors.websiteDepth.scrape({\n        url: form.get(\"url\"),\n        depth: parseInt(form.get(\"depth\")),\n        maxLinks: parseInt(form.get(\"maxLinks\")),\n      });\n\n      if (!!error) {\n        showToast(error, \"error\", { clear: true });\n        setLoading(false);\n        return;\n      }\n\n      showToast(\n        `Successfully scraped ${data.length} ${pluralize(\n          \"page\",\n          data.length\n        )}!`,\n        \"success\",\n        { clear: true }\n      );\n      e.target.reset();\n      setLoading(false);\n    } catch (e) {\n      console.error(e);\n      showToast(e.message, \"error\", { clear: true });\n      setLoading(false);\n    }\n  };\n\n  return (\n    <div className=\"flex w-full\">\n      <div className=\"flex flex-col w-full px-1 md:pb-6 pb-16\">\n        <form className=\"w-full\" onSubmit={handleSubmit}>\n          <div className=\"w-full flex flex-col py-2\">\n            <div className=\"w-full flex flex-col gap-4\">\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold\">\n                    {t(\"connectors.website-depth.URL\")}\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    {t(\"connectors.website-depth.URL_explained\")}\n                  </p>\n                </div>\n                <input\n                  type=\"url\"\n                  name=\"url\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"https://example.com\"\n                  required={true}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                />\n              </div>\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold\">\n                    {\" \"}\n                    {t(\"connectors.website-depth.depth\")}\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    {t(\"connectors.website-depth.depth_explained\")}\n                  </p>\n                </div>\n                <input\n                  type=\"number\"\n                  name=\"depth\"\n                  min=\"1\"\n                  max=\"5\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  required={true}\n                  defaultValue=\"1\"\n                />\n              </div>\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold\">\n                    {t(\"connectors.website-depth.max_pages\")}\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    {t(\"connectors.website-depth.max_pages_explained\")}\n                  </p>\n                </div>\n                <input\n                  type=\"number\"\n                  name=\"maxLinks\"\n                  min=\"1\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  required={true}\n                  defaultValue=\"20\"\n                />\n              </div>\n            </div>\n          </div>\n\n          <div className=\"flex flex-col gap-y-2 w-full pr-10\">\n            <button\n              type=\"submit\"\n              disabled={loading}\n              className=\"mt-2 w-full justify-center border-none px-4 py-2 rounded-lg text-dark-text light:text-white text-sm font-bold items-center flex gap-x-2 bg-theme-home-button-primary hover:bg-theme-home-button-primary-hover disabled:bg-theme-home-button-primary-hover disabled:cursor-not-allowed\"\n            >\n              {loading ? \"Scraping website...\" : \"Submit\"}\n            </button>\n            {loading && (\n              <p className=\"text-xs text-theme-text-secondary\">\n                {t(\"connectors.website-depth.task_explained\")}\n              </p>\n            )}\n          </div>\n        </form>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/DataConnectors/Connectors/Youtube/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function YoutubeOptions() {\n  const { t } = useTranslation();\n  const [loading, setLoading] = useState(false);\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = new FormData(e.target);\n\n    try {\n      setLoading(true);\n      showToast(\"Fetching transcript for YouTube video.\", \"info\", {\n        clear: true,\n        autoClose: false,\n      });\n\n      const { data, error } = await System.dataConnectors.youtube.transcribe({\n        url: form.get(\"url\"),\n      });\n\n      if (!!error) {\n        showToast(error, \"error\", { clear: true });\n        setLoading(false);\n        return;\n      }\n\n      showToast(\n        `${data.title} by ${data.author} transcription completed. Output folder is ${data.destination}.`,\n        \"success\",\n        { clear: true }\n      );\n      e.target.reset();\n      setLoading(false);\n      return;\n    } catch (e) {\n      console.error(e);\n      showToast(e.message, \"error\", { clear: true });\n      setLoading(false);\n    }\n  };\n\n  return (\n    <div className=\"flex w-full\">\n      <div className=\"flex flex-col w-full px-1 md:pb-6 pb-16\">\n        <form className=\"w-full\" onSubmit={handleSubmit}>\n          <div className=\"w-full flex flex-col py-2\">\n            <div className=\"w-full flex flex-col gap-4\">\n              <div className=\"flex flex-col pr-10\">\n                <div className=\"flex flex-col gap-y-1 mb-4\">\n                  <label className=\"text-white text-sm font-bold\">\n                    {t(\"connectors.youtube.URL\")}\n                  </label>\n                  <p className=\"text-xs font-normal text-theme-text-secondary\">\n                    {t(\"connectors.youtube.URL_explained_start\")}\n                    <a\n                      href=\"https://support.google.com/youtube/answer/6373554\"\n                      rel=\"noreferrer\"\n                      target=\"_blank\"\n                      className=\"underline\"\n                      onClick={(e) => e.stopPropagation()}\n                    >\n                      {t(\"connectors.youtube.URL_explained_link\")}\n                    </a>\n                    {t(\"connectors.youtube.URL_explained_end\")}\n                  </p>\n                </div>\n                <input\n                  type=\"url\"\n                  name=\"url\"\n                  className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"https://youtube.com/watch?v=abc123\"\n                  required={true}\n                  autoComplete=\"off\"\n                  spellCheck={false}\n                />\n              </div>\n            </div>\n          </div>\n\n          <div className=\"flex flex-col gap-y-2 w-full pr-10\">\n            <button\n              type=\"submit\"\n              disabled={loading}\n              className=\"mt-2 w-full justify-center border-none px-4 py-2 rounded-lg text-dark-text light:text-white text-sm font-bold items-center flex gap-x-2 bg-theme-home-button-primary hover:bg-theme-home-button-primary-hover disabled:bg-theme-home-button-primary-hover disabled:cursor-not-allowed\"\n            >\n              {loading ? \"Collecting transcript...\" : \"Collect transcript\"}\n            </button>\n            {loading && (\n              <p className=\"text-xs text-theme-text-secondary max-w-sm\">\n                {t(\"connectors.youtube.task_explained\")}\n              </p>\n            )}\n          </div>\n        </form>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/DataConnectors/index.jsx",
    "content": "import ConnectorImages from \"@/components/DataConnectorOption/media\";\nimport { MagnifyingGlass } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\nimport GithubOptions from \"./Connectors/Github\";\nimport GitlabOptions from \"./Connectors/Gitlab\";\nimport YoutubeOptions from \"./Connectors/Youtube\";\nimport ConfluenceOptions from \"./Connectors/Confluence\";\nimport DrupalWikiOptions from \"./Connectors/DrupalWiki\";\nimport { useState } from \"react\";\nimport ConnectorOption from \"./ConnectorOption\";\nimport WebsiteDepthOptions from \"./Connectors/WebsiteDepth\";\nimport ObsidianOptions from \"./Connectors/Obsidian\";\nimport PaperlessNgxOptions from \"./Connectors/PaperlessNgx\";\n\nexport const getDataConnectors = (t) => ({\n  github: {\n    name: t(\"connectors.github.name\"),\n    image: ConnectorImages.github,\n    description: t(\"connectors.github.description\"),\n    options: <GithubOptions />,\n  },\n  gitlab: {\n    name: t(\"connectors.gitlab.name\"),\n    image: ConnectorImages.gitlab,\n    description: t(\"connectors.gitlab.description\"),\n    options: <GitlabOptions />,\n  },\n  \"youtube-transcript\": {\n    name: t(\"connectors.youtube.name\"),\n    image: ConnectorImages.youtube,\n    description: t(\"connectors.youtube.description\"),\n    options: <YoutubeOptions />,\n  },\n  \"website-depth\": {\n    name: t(\"connectors.website-depth.name\"),\n    image: ConnectorImages.websiteDepth,\n    description: t(\"connectors.website-depth.description\"),\n    options: <WebsiteDepthOptions />,\n  },\n  confluence: {\n    name: t(\"connectors.confluence.name\"),\n    image: ConnectorImages.confluence,\n    description: t(\"connectors.confluence.description\"),\n    options: <ConfluenceOptions />,\n  },\n  drupalwiki: {\n    name: \"Drupal Wiki\",\n    image: ConnectorImages.drupalwiki,\n    description: \"Import Drupal Wiki spaces in a single click.\",\n    options: <DrupalWikiOptions />,\n  },\n  obsidian: {\n    name: \"Obsidian\",\n    image: ConnectorImages.obsidian,\n    description: \"Import Obsidian vault in a single click.\",\n    options: <ObsidianOptions />,\n  },\n  \"paperless-ngx\": {\n    name: \"Paperless-ngx\",\n    image: ConnectorImages.paperlessNgx,\n    description: \"Import documents from your Paperless-ngx instance.\",\n    options: <PaperlessNgxOptions />,\n  },\n});\n\nexport default function DataConnectors() {\n  const { t } = useTranslation();\n  const [selectedConnector, setSelectedConnector] = useState(\"github\");\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const DATA_CONNECTORS = getDataConnectors(t);\n\n  const filteredConnectors = Object.keys(DATA_CONNECTORS).filter((slug) =>\n    DATA_CONNECTORS[slug].name.toLowerCase().includes(searchQuery.toLowerCase())\n  );\n\n  return (\n    <div className=\"flex upload-modal -mt-10 relative min-h-[80vh] w-[70vw]\">\n      <div className=\"w-full p-4 top-0 z-20\">\n        <div className=\"w-full flex items-center sticky top-0 z-50\">\n          <MagnifyingGlass\n            size={16}\n            weight=\"bold\"\n            className=\"absolute left-4 z-30 text-white\"\n          />\n          <input\n            type=\"text\"\n            placeholder={t(\"connectors.search-placeholder\")}\n            className=\"border-none z-20 pl-10 h-[38px] rounded-full w-full px-4 py-1 text-sm border-2 border-slate-300/40 outline-none focus:outline-primary-button active:outline-primary-button outline-none placeholder:text-theme-settings-input-placeholder text-white bg-theme-settings-input-bg\"\n            autoComplete=\"off\"\n            value={searchQuery}\n            onChange={(e) => setSearchQuery(e.target.value)}\n          />\n        </div>\n        <div className=\"mt-2 flex flex-col gap-y-2\">\n          {filteredConnectors.length > 0 ? (\n            filteredConnectors.map((slug, index) => (\n              <ConnectorOption\n                key={index}\n                slug={slug}\n                selectedConnector={selectedConnector}\n                setSelectedConnector={setSelectedConnector}\n                image={DATA_CONNECTORS[slug].image}\n                name={DATA_CONNECTORS[slug].name}\n                description={DATA_CONNECTORS[slug].description}\n              />\n            ))\n          ) : (\n            <div className=\"text-white text-center mt-4\">\n              {t(\"connectors.no-connectors\")}\n            </div>\n          )}\n        </div>\n      </div>\n      <div className=\"xl:block hidden absolute left-1/2 top-0 bottom-0 w-[0.5px] bg-white/20 -translate-x-1/2\"></div>\n      <div className=\"w-full p-4 top-0 text-white min-w-[500px]\">\n        {DATA_CONNECTORS[selectedConnector].options}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/Documents/Directory/ContextMenu/index.jsx",
    "content": "import { useRef, useEffect } from \"react\";\n\nexport default function ContextMenu({\n  contextMenu,\n  closeContextMenu,\n  files,\n  selectedItems,\n  setSelectedItems,\n}) {\n  const contextMenuRef = useRef(null);\n\n  useEffect(() => {\n    const handleClickOutside = (event) => {\n      if (\n        contextMenuRef.current &&\n        !contextMenuRef.current.contains(event.target)\n      ) {\n        closeContextMenu();\n      }\n    };\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, [closeContextMenu]);\n\n  const isAllSelected = () => {\n    const allItems = files.items.flatMap((folder) => [\n      folder.name,\n      ...folder.items.map((file) => file.id),\n    ]);\n    return allItems.every((item) => selectedItems[item]);\n  };\n\n  const toggleSelectAll = () => {\n    if (isAllSelected()) {\n      setSelectedItems({});\n    } else {\n      const newSelectedItems = {};\n      files.items.forEach((folder) => {\n        newSelectedItems[folder.name] = true;\n        folder.items.forEach((file) => {\n          newSelectedItems[file.id] = true;\n        });\n      });\n      setSelectedItems(newSelectedItems);\n    }\n    closeContextMenu();\n  };\n\n  if (!contextMenu.visible) return null;\n\n  return (\n    <div\n      ref={contextMenuRef}\n      style={{\n        position: \"fixed\",\n        top: `${contextMenu.y}px`,\n        left: `${contextMenu.x}px`,\n        zIndex: 1000,\n      }}\n      className=\"bg-theme-bg-secondary border border-theme-modal-border rounded-md shadow-lg\"\n    >\n      <button\n        onClick={toggleSelectAll}\n        className=\"block w-full text-left px-4 py-2 text-sm text-theme-text-primary hover:bg-theme-file-picker-hover\"\n      >\n        {isAllSelected() ? \"Unselect All\" : \"Select All\"}\n      </button>\n      <button\n        onClick={closeContextMenu}\n        className=\"block w-full text-left px-4 py-2 text-sm text-theme-text-primary hover:bg-theme-file-picker-hover\"\n      >\n        Cancel\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/Documents/Directory/FileRow/index.jsx",
    "content": "import React from \"react\";\nimport {\n  formatDateTimeAsMoment,\n  getFileExtension,\n  middleTruncate,\n} from \"@/utils/directories\";\nimport { File } from \"@phosphor-icons/react\";\n\nexport default function FileRow({ item, selected, toggleSelection }) {\n  return (\n    <tr\n      onClick={() => toggleSelection(item)}\n      className={`text-theme-text-primary text-xs grid grid-cols-12 py-2 pl-8 pr-8 hover:bg-theme-file-picker-hover cursor-pointer file-row ${\n        selected ? \"selected light:text-white\" : \"\"\n      }`}\n    >\n      <div\n        data-tooltip-id=\"directory-item\"\n        className=\"col-span-10 w-fit flex gap-x-[4px] items-center relative\"\n        data-tooltip-content={JSON.stringify({\n          title: item.title,\n          date: formatDateTimeAsMoment(item?.published),\n          extension: getFileExtension(item.url),\n        })}\n      >\n        <div\n          className={`shrink-0 w-3 h-3 rounded border-[1px] border-solid border-white ${\n            selected ? \"text-white\" : \"text-theme-text-primary light:invert\"\n          } flex justify-center items-center cursor-pointer`}\n          role=\"checkbox\"\n          aria-checked={selected}\n          tabIndex={0}\n        >\n          {selected && <div className=\"w-2 h-2 bg-white rounded-[2px]\" />}\n        </div>\n        <File\n          className=\"shrink-0 text-base font-bold w-4 h-4 mr-[3px]\"\n          weight=\"fill\"\n        />\n        <p className=\"whitespace-nowrap overflow-hidden text-ellipsis max-w-[400px]\">\n          {middleTruncate(item.title, 55)}\n        </p>\n      </div>\n      <div className=\"col-span-2 flex justify-end items-center\">\n        {item?.cached && (\n          <div className=\"bg-theme-settings-input-active rounded-3xl\">\n            <p className=\"text-xs px-2 py-0.5\">Cached</p>\n          </div>\n        )}\n      </div>\n    </tr>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/Documents/Directory/FolderRow/index.jsx",
    "content": "import { useState } from \"react\";\nimport FileRow from \"../FileRow\";\nimport { CaretDown, FolderNotch } from \"@phosphor-icons/react\";\nimport { middleTruncate } from \"@/utils/directories\";\n\nexport default function FolderRow({\n  item,\n  totalItems = 0,\n  selected,\n  onRowClick,\n  toggleSelection,\n  isSelected,\n  autoExpanded = false,\n}) {\n  const [expanded, setExpanded] = useState(autoExpanded);\n\n  const handleExpandClick = (event) => {\n    event.stopPropagation();\n    setExpanded(!expanded);\n  };\n\n  return (\n    <>\n      <tr\n        onClick={onRowClick}\n        className={`text-theme-text-primary text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 hover:bg-theme-file-picker-hover cursor-pointer file-row ${\n          selected ? \"selected light:text-white !text-white\" : \"\"\n        }`}\n      >\n        <div\n          className={`col-span-6 flex gap-x-[4px] items-center ${\n            selected ? \"!text-white\" : \"text-theme-text-primary\"\n          }`}\n        >\n          <div\n            className={`shrink-0 w-3 h-3 rounded border-[1px] border-solid border-white ${\n              selected ? \"text-white\" : \"text-theme-text-primary light:invert\"\n            } flex justify-center items-center cursor-pointer`}\n            role=\"checkbox\"\n            aria-checked={selected}\n            tabIndex={0}\n            onClick={(event) => {\n              event.stopPropagation();\n              toggleSelection(item);\n            }}\n          >\n            {selected && <div className=\"w-2 h-2 bg-white rounded-[2px]\" />}\n          </div>\n          <div\n            onClick={handleExpandClick}\n            className={`transform transition-transform duration-200 ${\n              expanded ? \"rotate-360\" : \" rotate-270\"\n            }`}\n          >\n            <CaretDown className=\"text-base font-bold w-4 h-4\" />\n          </div>\n          <FolderNotch\n            className=\"shrink-0 text-base font-bold w-4 h-4 mr-[3px]\"\n            weight=\"fill\"\n          />\n          <p className=\"whitespace-nowrap overflow-show max-w-[400px]\">\n            {middleTruncate(item.name, 35)}\n          </p>\n          {totalItems > 0 && (\n            <span className=\"text-theme-text-secondary text-[10px] font-medium ml-1.5 shrink-0\">\n              ({totalItems})\n            </span>\n          )}\n        </div>\n        <p className=\"col-span-2 pl-3.5\" />\n        <p className=\"col-span-2 pl-2\" />\n      </tr>\n      {expanded && (\n        <>\n          {item.items.map((fileItem) => (\n            <FileRow\n              key={fileItem.id}\n              item={fileItem}\n              selected={isSelected(fileItem.id)}\n              toggleSelection={toggleSelection}\n            />\n          ))}\n        </>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/Documents/Directory/FolderSelectionPopup/index.jsx",
    "content": "import { middleTruncate } from \"@/utils/directories\";\n\nexport default function FolderSelectionPopup({ folders, onSelect, onClose }) {\n  const handleFolderSelect = (folder) => {\n    onSelect(folder);\n    onClose();\n  };\n\n  return (\n    <div className=\"absolute bottom-full left-0 mb-2 bg-white rounded-lg shadow-lg max-h-40 overflow-y-auto no-scroll\">\n      <ul>\n        {folders.map((folder) => (\n          <li\n            key={folder.name}\n            onClick={() => handleFolderSelect(folder)}\n            className=\"px-4 py-2 text-xs text-gray-700 hover:bg-gray-200 rounded-lg cursor-pointer whitespace-nowrap\"\n          >\n            {middleTruncate(folder.name, 25)}\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/Documents/Directory/MoveToFolderIcon.jsx",
    "content": "export default function MoveToFolderIcon({\n  className,\n  width = 18,\n  height = 18,\n}) {\n  return (\n    <svg\n      width={width}\n      height={height}\n      viewBox=\"0 0 17 19\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      className={className}\n    >\n      <path\n        d=\"M1.46092 17.9754L3.5703 12.7019C3.61238 12.5979 3.68461 12.5088 3.7777 12.4462C3.8708 12.3836 3.98051 12.3502 4.09272 12.3504H7.47897C7.59001 12.3502 7.69855 12.3174 7.79116 12.2562L9.19741 11.3196C9.29001 11.2583 9.39855 11.2256 9.50959 11.2254H15.5234C15.6126 11.2254 15.7004 11.2465 15.7798 11.2872C15.8591 11.3278 15.9277 11.3867 15.9798 11.459C16.0319 11.5313 16.0661 11.6149 16.0795 11.703C16.093 11.7912 16.0853 11.8812 16.0571 11.9658L14.0532 17.9754H1.46092Z\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M2.25331 6.53891H2.02342C1.67533 6.53891 1.34149 6.67719 1.09534 6.92333C0.849204 7.16947 0.710922 7.50331 0.710922 7.85141V17.9764C0.710922 18.3906 1.04671 18.7264 1.46092 18.7264C1.87514 18.7264 2.21092 18.3906 2.21092 17.9764V8.03891H2.25331V6.53891ZM13.0859 9.98714V11.2264C13.0859 11.6406 13.4217 11.9764 13.8359 11.9764C14.2501 11.9764 14.5859 11.6406 14.5859 11.2264V9.53891C14.5859 9.19081 14.4476 8.85698 14.2015 8.61083C13.9554 8.36469 13.6215 8.22641 13.2734 8.22641H13.0863V9.98714H13.0859Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M7.53416 1.62906L7.53416 7.70406\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M10.6411 5.21854L7.53456 7.70376L4.42803 5.21854\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/Documents/Directory/NewFolderModal/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport Document from \"@/models/document\";\n\nexport default function NewFolderModal({ closeModal, files, setFiles }) {\n  const [error, setError] = useState(null);\n  const [folderName, setFolderName] = useState(\"\");\n\n  const handleCreate = async (e) => {\n    e.preventDefault();\n    setError(null);\n    if (folderName.trim() !== \"\") {\n      const newFolder = {\n        name: folderName,\n        type: \"folder\",\n        items: [],\n      };\n      const { success } = await Document.createFolder(folderName);\n      if (success) {\n        setFiles({\n          ...files,\n          items: [...files.items, newFolder],\n        });\n        closeModal();\n      } else {\n        setError(\"Failed to create folder\");\n      }\n    }\n  };\n\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n      <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Create New Folder\n            </h3>\n          </div>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div className=\"p-6\">\n          <form onSubmit={handleCreate}>\n            <div className=\"space-y-4\">\n              <div>\n                <label\n                  htmlFor=\"folderName\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Folder Name\n                </label>\n                <input\n                  name=\"folderName\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"Enter folder name\"\n                  required={true}\n                  autoComplete=\"off\"\n                  value={folderName}\n                  onChange={(e) => setFolderName(e.target.value)}\n                />\n              </div>\n              {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n            </div>\n            <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border\">\n              <button\n                onClick={closeModal}\n                type=\"button\"\n                className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n              >\n                Cancel\n              </button>\n              <button\n                type=\"submit\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                Create Folder\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/Documents/Directory/index.jsx",
    "content": "import UploadFile from \"../UploadFile\";\nimport PreLoader from \"@/components/Preloader\";\nimport { memo, useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport FolderRow from \"./FolderRow\";\nimport System from \"@/models/system\";\nimport { MagnifyingGlass, Plus, Trash } from \"@phosphor-icons/react\";\nimport Document from \"@/models/document\";\nimport showToast from \"@/utils/toast\";\nimport FolderSelectionPopup from \"./FolderSelectionPopup\";\nimport MoveToFolderIcon from \"./MoveToFolderIcon\";\nimport { useModal } from \"@/hooks/useModal\";\nimport NewFolderModal from \"./NewFolderModal\";\nimport debounce from \"lodash.debounce\";\nimport { filterFileSearchResults } from \"./utils\";\nimport ContextMenu from \"./ContextMenu\";\nimport { Tooltip } from \"react-tooltip\";\nimport { safeJsonParse } from \"@/utils/request\";\n\nfunction Directory({\n  files,\n  setFiles,\n  loading,\n  setLoading,\n  workspace,\n  fetchKeys,\n  selectedItems,\n  setSelectedItems,\n  setHighlightWorkspace,\n  moveToWorkspace,\n  setLoadingMessage,\n  loadingMessage,\n}) {\n  const { t } = useTranslation();\n  const [amountSelected, setAmountSelected] = useState(0);\n  const [showFolderSelection, setShowFolderSelection] = useState(false);\n  const [searchTerm, setSearchTerm] = useState(\"\");\n  const {\n    isOpen: isFolderModalOpen,\n    openModal: openFolderModal,\n    closeModal: closeFolderModal,\n  } = useModal();\n  const [contextMenu, setContextMenu] = useState({\n    visible: false,\n    x: 0,\n    y: 0,\n  });\n\n  useEffect(() => {\n    setAmountSelected(Object.keys(selectedItems).length);\n  }, [selectedItems]);\n\n  const deleteFiles = async (event) => {\n    event.stopPropagation();\n    if (!window.confirm(t(\"connectors.directory.delete-confirmation\"))) {\n      return false;\n    }\n\n    try {\n      const toRemove = [];\n      const foldersToRemove = [];\n\n      for (const itemId of Object.keys(selectedItems)) {\n        for (const folder of files.items) {\n          const foundItem = folder.items.find((file) => file.id === itemId);\n          if (foundItem) {\n            toRemove.push(`${folder.name}/${foundItem.name}`);\n            break;\n          }\n        }\n      }\n      for (const folder of files.items) {\n        if (folder.name === \"custom-documents\") {\n          continue;\n        }\n\n        if (isSelected(folder.id, folder)) {\n          foldersToRemove.push(folder.name);\n        }\n      }\n\n      setLoading(true);\n      setLoadingMessage(\n        t(\"connectors.directory.removing-message\", {\n          count: toRemove.length,\n          folderCount: foldersToRemove.length,\n        })\n      );\n      await System.deleteDocuments(toRemove);\n      for (const folderName of foldersToRemove) {\n        await System.deleteFolder(folderName);\n      }\n\n      await fetchKeys(true);\n      setSelectedItems({});\n    } catch (error) {\n      console.error(\"Failed to delete files and folders:\", error);\n    } finally {\n      setLoading(false);\n      setSelectedItems({});\n    }\n  };\n\n  const toggleSelection = (item) => {\n    setSelectedItems((prevSelectedItems) => {\n      const newSelectedItems = { ...prevSelectedItems };\n      if (item.type === \"folder\") {\n        // select all files in the folder\n        if (newSelectedItems[item.name]) {\n          delete newSelectedItems[item.name];\n          item.items.forEach((file) => delete newSelectedItems[file.id]);\n        } else {\n          newSelectedItems[item.name] = true;\n          item.items.forEach((file) => (newSelectedItems[file.id] = true));\n        }\n      } else {\n        // single file selections\n        if (newSelectedItems[item.id]) {\n          delete newSelectedItems[item.id];\n        } else {\n          newSelectedItems[item.id] = true;\n        }\n      }\n\n      return newSelectedItems;\n    });\n  };\n\n  // check if item is selected based on selectedItems state\n  const isSelected = (id, item) => {\n    if (item && item.type === \"folder\") {\n      if (!selectedItems[item.name]) {\n        return false;\n      }\n      return item.items.every((file) => selectedItems[file.id]);\n    }\n\n    return !!selectedItems[id];\n  };\n\n  const moveToFolder = async (folder) => {\n    const toMove = [];\n    for (const itemId of Object.keys(selectedItems)) {\n      for (const currentFolder of files.items) {\n        const foundItem = currentFolder.items.find(\n          (file) => file.id === itemId\n        );\n        if (foundItem) {\n          toMove.push({ ...foundItem, folderName: currentFolder.name });\n          break;\n        }\n      }\n    }\n    setLoading(true);\n    setLoadingMessage(`Moving ${toMove.length} documents. Please wait.`);\n    const { success, message } = await Document.moveToFolder(\n      toMove,\n      folder.name\n    );\n    if (!success) {\n      showToast(`Error moving files: ${message}`, \"error\");\n      setLoading(false);\n      return;\n    }\n\n    if (success && message) {\n      // show info if some files were not moved due to being embedded\n      showToast(message, \"info\");\n    } else {\n      showToast(\n        t(\"connectors.directory.move-success\", { count: toMove.length }),\n        \"success\"\n      );\n    }\n    await fetchKeys(true);\n    setSelectedItems({});\n    setLoading(false);\n  };\n\n  const handleSearch = debounce((e) => {\n    const searchValue = e.target.value;\n    setSearchTerm(searchValue);\n  }, 500);\n\n  const filteredFiles = filterFileSearchResults(files, searchTerm);\n\n  const handleContextMenu = (event) => {\n    event.preventDefault();\n    setContextMenu({ visible: true, x: event.clientX, y: event.clientY });\n  };\n\n  const closeContextMenu = () => {\n    setContextMenu({ visible: false, x: 0, y: 0 });\n  };\n\n  const totalDocCount = (files?.items ?? []).reduce((acc, folder) => {\n    if (folder.type === \"folder\") return folder.items.length + acc;\n    return acc;\n  }, 0);\n\n  return (\n    <>\n      <div className=\"px-8 pb-8\" onContextMenu={handleContextMenu}>\n        <div className=\"flex flex-col gap-y-6\">\n          <div className=\"flex items-center justify-between w-[560px] px-5 relative\">\n            <h3 className=\"text-white text-base font-bold\">\n              {t(\"connectors.directory.my-documents\")}\n            </h3>\n            <div className=\"relative\">\n              <input\n                type=\"search\"\n                placeholder={t(\"connectors.directory.search-document\")}\n                onChange={handleSearch}\n                className=\"border-none search-input bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder focus:outline-primary-button active:outline-primary-button outline-none text-sm rounded-lg pl-9 pr-2.5 py-2 w-[250px] h-[32px] light:border-theme-modal-border light:border\"\n              />\n              <MagnifyingGlass\n                size={14}\n                className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-white\"\n                weight=\"bold\"\n              />\n            </div>\n            <button\n              className=\"border-none flex items-center gap-x-2 cursor-pointer px-[14px] py-[7px] -mr-[14px] rounded-lg hover:bg-theme-sidebar-subitem-hover z-20 relative\"\n              onClick={openFolderModal}\n            >\n              <Plus\n                size={18}\n                weight=\"bold\"\n                className=\"text-theme-text-primary light:text-[#0ba5ec]\"\n              />\n              <div className=\"text-theme-text-primary light:text-[#0ba5ec] text-xs font-bold leading-[18px]\">\n                {t(\"connectors.directory.new-folder\")}\n              </div>\n            </button>\n          </div>\n\n          <div className=\"relative w-[560px] h-[310px] bg-theme-settings-input-bg rounded-2xl overflow-hidden border border-theme-modal-border\">\n            <div className=\"absolute top-0 left-0 right-0 z-10 rounded-t-2xl text-theme-text-primary text-xs grid grid-cols-12 py-2 px-8 border-b border-white/20 light:border-theme-modal-border bg-theme-settings-input-bg\">\n              <p className=\"col-span-6\">Name</p>\n              {totalDocCount > 0 && (\n                <p className=\"col-span-6 text-right text-theme-text-secondary\">\n                  {t(`connectors.directory.total-documents`, {\n                    count: totalDocCount,\n                  })}\n                </p>\n              )}\n            </div>\n\n            <div className=\"overflow-y-auto h-full pt-8\">\n              {loading ? (\n                <div className=\"w-full h-full flex items-center justify-center flex-col gap-y-5\">\n                  <PreLoader />\n                  <p className=\"text-white text-sm font-semibold animate-pulse text-center w-1/3\">\n                    {loadingMessage}\n                  </p>\n                </div>\n              ) : filteredFiles.length > 0 ? (\n                filteredFiles.map(\n                  (item, index) =>\n                    item.type === \"folder\" && (\n                      <FolderRow\n                        key={index}\n                        item={item}\n                        totalItems={item.items?.length ?? 0}\n                        selected={isSelected(\n                          item.id,\n                          item.type === \"folder\" ? item : null\n                        )}\n                        onRowClick={() => toggleSelection(item)}\n                        toggleSelection={toggleSelection}\n                        isSelected={isSelected}\n                        autoExpanded={index === 0}\n                      />\n                    )\n                )\n              ) : (\n                <div className=\"w-full h-full flex items-center justify-center\">\n                  <p className=\"text-white text-opacity-40 text-sm font-medium\">\n                    {t(\"connectors.directory.no-documents\")}\n                  </p>\n                </div>\n              )}\n            </div>\n            {amountSelected !== 0 && (\n              <div className=\"absolute bottom-[12px] left-0 right-0 flex justify-center pointer-events-none\">\n                <div className=\"mx-auto bg-white/40 light:bg-white rounded-lg py-1 px-2 pointer-events-auto light:shadow-lg\">\n                  <div className=\"flex flex-row items-center gap-x-2\">\n                    <button\n                      onClick={moveToWorkspace}\n                      onMouseEnter={() => setHighlightWorkspace(true)}\n                      onMouseLeave={() => setHighlightWorkspace(false)}\n                      className=\"border-none text-sm font-semibold bg-white light:bg-[#E0F2FE] h-[30px] px-2.5 rounded-lg hover:bg-neutral-800/80 hover:text-white light:text-[#026AA2] light:hover:bg-[#026AA2] light:hover:text-white\"\n                    >\n                      {t(\"connectors.directory.move-workspace\")}\n                    </button>\n                    <div className=\"relative\">\n                      <button\n                        onClick={() =>\n                          setShowFolderSelection(!showFolderSelection)\n                        }\n                        className=\"border-none text-sm font-semibold bg-white light:bg-[#E0F2FE] h-[32px] w-[32px] rounded-lg text-dark-text hover:bg-neutral-800/80 hover:text-white light:text-[#026AA2] light:hover:bg-[#026AA2] light:hover:text-white flex justify-center items-center group\"\n                      >\n                        <MoveToFolderIcon className=\"text-dark-text light:text-[#026AA2] group-hover:text-white\" />\n                      </button>\n                      {showFolderSelection && (\n                        <FolderSelectionPopup\n                          folders={files.items.filter(\n                            (item) => item.type === \"folder\"\n                          )}\n                          onSelect={moveToFolder}\n                          onClose={() => setShowFolderSelection(false)}\n                        />\n                      )}\n                    </div>\n                    <button\n                      onClick={deleteFiles}\n                      className=\"border-none text-sm font-semibold bg-white light:bg-[#E0F2FE] h-[32px] w-[32px] rounded-lg text-dark-text hover:bg-neutral-800/80 hover:text-white light:text-[#026AA2] light:hover:bg-[#026AA2] light:hover:text-white flex justify-center items-center\"\n                    >\n                      <Trash size={18} weight=\"bold\" />\n                    </button>\n                  </div>\n                </div>\n              </div>\n            )}\n          </div>\n          <UploadFile\n            workspace={workspace}\n            fetchKeys={fetchKeys}\n            setLoading={setLoading}\n            setLoadingMessage={setLoadingMessage}\n          />\n        </div>\n        {isFolderModalOpen && (\n          <div className=\"bg-black/60 backdrop-blur-sm fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center z-30\">\n            <NewFolderModal\n              closeModal={closeFolderModal}\n              files={files}\n              setFiles={setFiles}\n            />\n          </div>\n        )}\n        <ContextMenu\n          contextMenu={contextMenu}\n          closeContextMenu={closeContextMenu}\n          files={files}\n          selectedItems={selectedItems}\n          setSelectedItems={setSelectedItems}\n        />\n      </div>\n      <DirectoryTooltips />\n    </>\n  );\n}\n\n/**\n * Tooltips for the directory components. Renders when the directory is shown\n * or updated so that tooltips are attached as the items are changed.\n */\nfunction DirectoryTooltips() {\n  return (\n    <Tooltip\n      id=\"directory-item\"\n      place=\"bottom\"\n      delayShow={800}\n      className=\"tooltip invert light:invert-0 z-99 max-w-[300px]\"\n      render={({ content }) => {\n        const data = safeJsonParse(content, null);\n        if (!data) return null;\n        return (\n          <div className=\"text-xs\">\n            <p className=\"text-white light:invert font-medium break-all\">\n              {data.title}\n            </p>\n            <div className=\"flex flex-col mt-1\">\n              <p className=\"\">\n                Date: <b>{data.date}</b>\n              </p>\n              <p className=\"\">\n                Type: <b>{data.extension}</b>\n              </p>\n            </div>\n          </div>\n        );\n      }}\n    />\n  );\n}\n\nexport default memo(Directory);\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/Documents/Directory/utils.js",
    "content": "import strDistance from \"js-levenshtein\";\n\nconst LEVENSHTEIN_MIN = 2;\n\n// Regular expression pattern to match the v4 UUID and the ending .json\nconst uuidPattern =\n  /-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/;\nconst jsonPattern = /\\.json$/;\n\n// Function to strip UUID v4 and JSON from file names as that will impact search results.\nexport const stripUuidAndJsonFromString = (input = \"\") => {\n  return input\n    ?.replace(uuidPattern, \"\") // remove v4 uuid\n    ?.replace(jsonPattern, \"\") // remove trailing .json\n    ?.replace(\"-\", \" \"); // turn slugged names into spaces\n};\n\nexport function filterFileSearchResults(files = [], searchTerm = \"\") {\n  if (!searchTerm) return files?.items || [];\n\n  const normalizedSearchTerm = searchTerm.toLowerCase().trim();\n\n  const searchResult = [];\n  for (const folder of files?.items) {\n    const folderNameNormalized = folder.name.toLowerCase();\n\n    // Check for exact match first, then fuzzy match\n    if (folderNameNormalized.includes(normalizedSearchTerm)) {\n      searchResult.push(folder);\n      continue;\n    }\n\n    // Check children for matches\n    const fileSearchResults = [];\n    for (const file of folder?.items) {\n      const fileNameNormalized = stripUuidAndJsonFromString(\n        file.name\n      ).toLowerCase();\n\n      // Exact match check\n      if (fileNameNormalized.includes(normalizedSearchTerm)) {\n        fileSearchResults.push(file);\n      }\n      // Fuzzy match only if no exact matches found\n      else if (\n        fileSearchResults.length === 0 &&\n        strDistance(fileNameNormalized, normalizedSearchTerm) <= LEVENSHTEIN_MIN\n      ) {\n        fileSearchResults.push(file);\n      }\n    }\n\n    if (fileSearchResults.length > 0) {\n      searchResult.push({\n        ...folder,\n        items: fileSearchResults,\n      });\n    }\n  }\n\n  return searchResult;\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/Documents/UploadFile/FileUploadProgress/index.jsx",
    "content": "import React, { useState, useEffect, memo } from \"react\";\nimport truncate from \"truncate\";\nimport { CheckCircle, XCircle } from \"@phosphor-icons/react\";\nimport Workspace from \"../../../../../../models/workspace\";\nimport { humanFileSize, milliToHms } from \"../../../../../../utils/numbers\";\nimport PreLoader from \"../../../../../Preloader\";\n\nfunction FileUploadProgressComponent({\n  slug,\n  uuid,\n  file,\n  setFiles,\n  rejected = false,\n  reason = null,\n  onUploadSuccess,\n  onUploadError,\n  setLoading,\n  setLoadingMessage,\n}) {\n  const [timerMs, setTimerMs] = useState(10);\n  const [status, setStatus] = useState(\"pending\");\n  const [error, setError] = useState(\"\");\n  const [isFadingOut, setIsFadingOut] = useState(false);\n\n  const fadeOut = (cb) => {\n    setIsFadingOut(true);\n    cb?.();\n  };\n\n  const beginFadeOut = () => {\n    setIsFadingOut(false);\n    setFiles((prev) => {\n      return prev.filter((item) => item.uid !== uuid);\n    });\n  };\n\n  useEffect(() => {\n    async function uploadFile() {\n      setLoading(true);\n      setLoadingMessage(\"Uploading file...\");\n      const start = Number(new Date());\n      const formData = new FormData();\n      formData.append(\"file\", file, file.name);\n      const timer = setInterval(() => {\n        setTimerMs(Number(new Date()) - start);\n      }, 100);\n\n      // Chunk streaming not working in production so we just sit and wait\n      const { response, data } = await Workspace.uploadFile(slug, formData);\n      if (!response.ok) {\n        setStatus(\"failed\");\n        clearInterval(timer);\n        onUploadError(data.error);\n        setError(data.error);\n      } else {\n        setLoading(false);\n        setLoadingMessage(\"\");\n        setStatus(\"complete\");\n        clearInterval(timer);\n        onUploadSuccess();\n      }\n\n      // Begin fadeout timer to clear uploader queue.\n      setTimeout(() => {\n        fadeOut(() => setTimeout(() => beginFadeOut(), 300));\n      }, 5000);\n    }\n    !!file && !rejected && uploadFile();\n  }, []);\n\n  if (rejected) {\n    return (\n      <div\n        className={`${\n          isFadingOut ? \"file-upload-fadeout\" : \"file-upload\"\n        } h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-error/40 light:bg-error/30 light:border-solid light:border-error/40 border border-transparent`}\n      >\n        <div className=\"w-6 h-6 flex-shrink-0\">\n          <XCircle\n            color=\"var(--theme-bg-primary)\"\n            className=\"w-6 h-6 stroke-white bg-error rounded-full p-1 w-full h-full\"\n          />\n        </div>\n        <div className=\"flex flex-col\">\n          <p className=\"text-white light:text-red-600 text-xs font-semibold\">\n            {truncate(file.name, 30)}\n          </p>\n          <p className=\"text-red-100 light:text-red-600 text-xs font-medium\">\n            {reason || \"this file failed to upload\"}\n          </p>\n        </div>\n      </div>\n    );\n  }\n\n  if (status === \"failed\") {\n    return (\n      <div\n        className={`${\n          isFadingOut ? \"file-upload-fadeout\" : \"file-upload\"\n        } h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-error/40 light:bg-error/30 light:border-solid light:border-error/40 border border-transparent`}\n      >\n        <div className=\"w-6 h-6 flex-shrink-0\">\n          <XCircle\n            color=\"var(--theme-bg-primary)\"\n            className=\"w-6 h-6 stroke-white bg-error rounded-full p-1 w-full h-full\"\n          />\n        </div>\n        <div className=\"flex flex-col\">\n          <p className=\"text-white light:text-red-600 text-xs font-semibold\">\n            {truncate(file.name, 30)}\n          </p>\n          <p className=\"text-red-100 light:text-red-600 text-xs font-medium\">\n            {error}\n          </p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className={`${\n        isFadingOut ? \"file-upload-fadeout\" : \"file-upload\"\n      } h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-zinc-800 light:border-solid light:border-theme-modal-border light:bg-theme-bg-sidebar border border-white/20 shadow-md`}\n    >\n      <div className=\"w-6 h-6 flex-shrink-0\">\n        {status !== \"complete\" ? (\n          <div className=\"flex items-center justify-center\">\n            <PreLoader size=\"6\" />\n          </div>\n        ) : (\n          <CheckCircle\n            color=\"var(--theme-bg-primary)\"\n            className=\"w-6 h-6 stroke-white bg-green-500 rounded-full p-1 w-full h-full\"\n          />\n        )}\n      </div>\n      <div className=\"flex flex-col\">\n        <p className=\"text-white light:text-theme-text-primary text-xs font-medium\">\n          {truncate(file.name, 30)}\n        </p>\n        <p className=\"text-white/80 light:text-theme-text-secondary text-xs font-medium\">\n          {humanFileSize(file.size)} | {milliToHms(timerMs)}\n        </p>\n      </div>\n    </div>\n  );\n}\n\nexport default memo(FileUploadProgressComponent);\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/Documents/UploadFile/index.jsx",
    "content": "import { CloudArrowUp } from \"@phosphor-icons/react\";\nimport { useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport showToast from \"../../../../../utils/toast\";\nimport System from \"../../../../../models/system\";\nimport { useDropzone } from \"react-dropzone\";\nimport { v4 } from \"uuid\";\nimport FileUploadProgress from \"./FileUploadProgress\";\nimport Workspace from \"../../../../../models/workspace\";\nimport debounce from \"lodash.debounce\";\n\nexport default function UploadFile({\n  workspace,\n  fetchKeys,\n  setLoading,\n  setLoadingMessage,\n}) {\n  const { t } = useTranslation();\n  const [ready, setReady] = useState(false);\n  const [files, setFiles] = useState([]);\n  const [fetchingUrl, setFetchingUrl] = useState(false);\n\n  const handleSendLink = async (e) => {\n    e.preventDefault();\n    setLoading(true);\n    setLoadingMessage(\"Scraping link...\");\n    setFetchingUrl(true);\n    const formEl = e.target;\n    const form = new FormData(formEl);\n    const { response, data } = await Workspace.uploadLink(\n      workspace.slug,\n      form.get(\"link\")\n    );\n    if (!response.ok) {\n      showToast(`Error uploading link: ${data.error}`, \"error\");\n    } else {\n      fetchKeys(true);\n      showToast(\"Link uploaded successfully\", \"success\");\n      formEl.reset();\n    }\n    setLoading(false);\n    setFetchingUrl(false);\n  };\n\n  // Queue all fetchKeys calls through the same debouncer to prevent spamming the server.\n  // either a success or error will trigger a fetchKeys call so the UI is not stuck loading.\n  const debouncedFetchKeys = debounce(() => fetchKeys(true), 1000);\n  const handleUploadSuccess = () => debouncedFetchKeys();\n  const handleUploadError = () => debouncedFetchKeys();\n\n  const onDrop = async (acceptedFiles, rejections) => {\n    const newAccepted = acceptedFiles.map((file) => {\n      return {\n        uid: v4(),\n        file,\n      };\n    });\n    const newRejected = rejections.map((file) => {\n      return {\n        uid: v4(),\n        file: file.file,\n        rejected: true,\n        reason: file.errors[0].code,\n      };\n    });\n    setFiles([...newAccepted, ...newRejected]);\n  };\n\n  useEffect(() => {\n    async function checkProcessorOnline() {\n      const online = await System.checkDocumentProcessorOnline();\n      setReady(online);\n    }\n    checkProcessorOnline();\n  }, []);\n\n  const { getRootProps, getInputProps } = useDropzone({\n    onDrop,\n    disabled: !ready,\n  });\n\n  return (\n    <div>\n      <div\n        className={`w-[560px] border-dashed border-[2px] border-theme-modal-border light:border-[#686C6F] rounded-2xl bg-theme-bg-primary transition-colors duration-300 p-3 ${\n          ready\n            ? \" light:bg-[#E0F2FE] cursor-pointer hover:bg-theme-bg-secondary light:hover:bg-transparent\"\n            : \"cursor-not-allowed\"\n        }`}\n        {...getRootProps()}\n      >\n        <input {...getInputProps()} />\n        {ready === false ? (\n          <div className=\"flex flex-col items-center justify-center h-full\">\n            <CloudArrowUp className=\"w-8 h-8 text-white/80 light:invert\" />\n            <div className=\"text-white text-opacity-80 text-sm font-semibold py-1\">\n              {t(\"connectors.upload.processor-offline\")}\n            </div>\n            <div className=\"text-white text-opacity-60 text-xs font-medium py-1 px-20 text-center\">\n              {t(\"connectors.upload.processor-offline-desc\")}\n            </div>\n          </div>\n        ) : files.length === 0 ? (\n          <div className=\"flex flex-col items-center justify-center\">\n            <CloudArrowUp className=\"w-8 h-8 text-white/80 light:invert\" />\n            <div className=\"text-white text-opacity-80 text-sm font-semibold py-1\">\n              {t(\"connectors.upload.click-upload\")}\n            </div>\n            <div className=\"text-white text-opacity-60 text-xs font-medium py-1\">\n              {t(\"connectors.upload.file-types\")}\n            </div>\n          </div>\n        ) : (\n          <div className=\"grid grid-cols-2 gap-2 overflow-auto max-h-[180px] p-1 overflow-y-scroll no-scroll\">\n            {files.map((file) => (\n              <FileUploadProgress\n                key={file.uid}\n                file={file.file}\n                uuid={file.uid}\n                setFiles={setFiles}\n                slug={workspace.slug}\n                rejected={file?.rejected}\n                reason={file?.reason}\n                onUploadSuccess={handleUploadSuccess}\n                onUploadError={handleUploadError}\n                setLoading={setLoading}\n                setLoadingMessage={setLoadingMessage}\n              />\n            ))}\n          </div>\n        )}\n      </div>\n      <div className=\"text-center text-white text-opacity-50 text-xs font-medium w-[560px] py-2\">\n        {t(\"connectors.upload.or-submit-link\")}\n      </div>\n      <form onSubmit={handleSendLink} className=\"flex gap-x-2\">\n        <input\n          disabled={fetchingUrl}\n          name=\"link\"\n          type=\"url\"\n          className=\"border-none disabled:bg-theme-settings-input-bg disabled:text-theme-settings-input-placeholder bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-3/4 p-2.5\"\n          placeholder={t(\"connectors.upload.placeholder-link\")}\n          autoComplete=\"off\"\n        />\n        <button\n          disabled={fetchingUrl}\n          type=\"submit\"\n          className=\"disabled:bg-white/20 disabled:text-slate-300 disabled:border-slate-400 disabled:cursor-wait bg bg-transparent hover:bg-slate-200 hover:text-slate-800 w-auto border border-white light:border-theme-modal-border text-sm text-white p-2.5 rounded-lg\"\n        >\n          {fetchingUrl\n            ? t(\"connectors.upload.fetching\")\n            : t(\"connectors.upload.fetch-website\")}\n        </button>\n      </form>\n      <div className=\"mt-6 text-center text-white text-opacity-80 text-xs font-medium w-[560px]\">\n        {t(\"connectors.upload.privacy-notice\")}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx",
    "content": "import { memo, useState } from \"react\";\nimport {\n  formatDateTimeAsMoment,\n  getFileExtension,\n  middleTruncate,\n} from \"@/utils/directories\";\nimport { ArrowUUpLeft, Eye, File, PushPin } from \"@phosphor-icons/react\";\nimport Workspace from \"@/models/workspace\";\nimport showToast from \"@/utils/toast\";\nimport System from \"@/models/system\";\n\nexport default function WorkspaceFileRow({\n  item,\n  folderName,\n  workspace,\n  setLoading,\n  setLoadingMessage,\n  fetchKeys,\n  hasChanges,\n  movedItems,\n  selected,\n  toggleSelection,\n  disableSelection,\n  setSelectedItems,\n}) {\n  const onRemoveClick = async (e) => {\n    e.stopPropagation();\n    setLoading(true);\n\n    try {\n      setLoadingMessage(`Removing file from workspace`);\n      await Workspace.modifyEmbeddings(workspace.slug, {\n        adds: [],\n        deletes: [`${folderName}/${item.name}`],\n      });\n      await fetchKeys(true);\n    } catch (error) {\n      console.error(\"Failed to remove document:\", error);\n    }\n    setSelectedItems({});\n    setLoadingMessage(\"\");\n    setLoading(false);\n  };\n\n  function toggleRowSelection(e) {\n    if (disableSelection) return;\n    e.stopPropagation();\n    toggleSelection();\n  }\n\n  function handleRowSelection(e) {\n    e.stopPropagation();\n    toggleSelection();\n  }\n\n  const isMovedItem = movedItems?.some((movedItem) => movedItem.id === item.id);\n  return (\n    <div\n      className={`text-theme-text-primary text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 h-[34px] items-center file-row ${\n        !disableSelection\n          ? \"hover:bg-theme-file-picker-hover cursor-pointer\"\n          : \"\"\n      } ${isMovedItem ? \"selected light:text-white\" : \"\"} ${\n        selected ? \"selected light:text-white\" : \"\"\n      }`}\n      onClick={toggleRowSelection}\n    >\n      <div\n        className=\"col-span-10 w-fit flex gap-x-[2px] items-center relative\"\n        data-tooltip-id=\"ws-directory-item\"\n        data-tooltip-content={JSON.stringify({\n          title: item.title,\n          date: formatDateTimeAsMoment(item?.published),\n          extension: getFileExtension(item.url),\n        })}\n      >\n        <div className=\"shrink-0 w-3 h-3\">\n          {!disableSelection ? (\n            <div\n              className={`shrink-0 w-3 h-3 rounded border-[1px] border-solid border-white ${\n                selected ? \"text-white\" : \"text-theme-text-primary light:invert\"\n              } flex justify-center items-center cursor-pointer`}\n              role=\"checkbox\"\n              aria-checked={selected}\n              tabIndex={0}\n              onClick={handleRowSelection}\n            >\n              {selected && <div className=\"w-2 h-2 bg-white rounded-[2px]\" />}\n            </div>\n          ) : null}\n        </div>\n        <File\n          className=\"shrink-0 text-base font-bold w-4 h-4 mr-[3px] ml-1\"\n          weight=\"fill\"\n        />\n        <p className=\"whitespace-nowrap overflow-hidden text-ellipsis max-w-[400px]\">\n          {middleTruncate(item.title, 50)}\n        </p>\n      </div>\n      <div className=\"col-span-2 flex justify-end items-center\">\n        {hasChanges ? (\n          <div className=\"w-4 h-4 ml-2 flex-shrink-0\" />\n        ) : (\n          <div className=\"flex gap-x-2 items-center\">\n            <WatchForChanges\n              workspace={workspace}\n              docPath={`${folderName}/${item.name}`}\n              item={item}\n            />\n            <PinItemToWorkspace\n              workspace={workspace}\n              docPath={`${folderName}/${item.name}`}\n              item={item}\n            />\n            <RemoveItemFromWorkspace item={item} onClick={onRemoveClick} />\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nconst PinItemToWorkspace = memo(({ workspace, docPath, item }) => {\n  const [pinned, setPinned] = useState(\n    item?.pinnedWorkspaces?.includes(workspace.id) || false\n  );\n  const pinEvent = new CustomEvent(\"pinned_document\");\n\n  const updatePinStatus = async (e) => {\n    try {\n      e.stopPropagation();\n      if (!pinned) window.dispatchEvent(pinEvent);\n      const success = await Workspace.setPinForDocument(\n        workspace.slug,\n        docPath,\n        !pinned\n      );\n\n      if (!success) {\n        showToast(`Failed to ${!pinned ? \"pin\" : \"unpin\"} document.`, \"error\", {\n          clear: true,\n        });\n        return;\n      }\n\n      showToast(\n        `Document ${!pinned ? \"pinned to\" : \"unpinned from\"} workspace`,\n        \"success\",\n        { clear: true }\n      );\n      setPinned(!pinned);\n    } catch (error) {\n      showToast(`Failed to pin document. ${error.message}`, \"error\", {\n        clear: true,\n      });\n      return;\n    }\n  };\n\n  if (!item) return <div className=\"w-[16px] p-[2px] ml-2\" />;\n\n  return (\n    <div\n      onClick={updatePinStatus}\n      className=\"group flex items-center ml-2 cursor-pointer\"\n      data-tooltip-id=\"pin-document\"\n      data-tooltip-content={\n        pinned ? \"Un-pin from workspace\" : \"Pin to workspace\"\n      }\n    >\n      {pinned ? (\n        <div className=\"bg-theme-settings-input-active group-hover:bg-red-500/20 rounded-3xl whitespace-nowrap\">\n          <p className=\"text-xs px-2 py-0.5 group-hover:text-red-500\">\n            <span className=\"group-hover:hidden\">Pinned</span>\n            <span className=\"hidden group-hover:inline\">Un-pin</span>\n          </p>\n        </div>\n      ) : (\n        <PushPin\n          size={16}\n          weight=\"regular\"\n          className=\"outline-none text-base font-bold flex-shrink-0\"\n        />\n      )}\n    </div>\n  );\n});\n\nconst WatchForChanges = memo(({ workspace, docPath, item }) => {\n  const [watched, setWatched] = useState(item?.watched || false);\n  const watchEvent = new CustomEvent(\"watch_document_for_changes\");\n\n  const updateWatchStatus = async (e) => {\n    try {\n      e.stopPropagation();\n      if (!watched) window.dispatchEvent(watchEvent);\n      const success =\n        await System.experimentalFeatures.liveSync.setWatchStatusForDocument(\n          workspace.slug,\n          docPath,\n          !watched\n        );\n\n      if (!success) {\n        showToast(\n          `Failed to ${!watched ? \"watch\" : \"unwatch\"} document.`,\n          \"error\",\n          {\n            clear: true,\n          }\n        );\n        return;\n      }\n\n      showToast(\n        `Document ${\n          !watched\n            ? \"will be watched for changes\"\n            : \"will no longer be watched for changes\"\n        }.`,\n        \"success\",\n        { clear: true }\n      );\n      setWatched(!watched);\n    } catch (error) {\n      showToast(`Failed to watch document. ${error.message}`, \"error\", {\n        clear: true,\n      });\n      return;\n    }\n  };\n\n  if (!item || !item.canWatch) return <div className=\"w-[16px] p-[2px] ml-2\" />;\n\n  return (\n    <div\n      className=\"group flex gap-x-2 items-center hover:bg-theme-file-picker-hover p-[2px] rounded ml-2 cursor-pointer\"\n      onClick={updateWatchStatus}\n      data-tooltip-id=\"watch-changes\"\n      data-active={watched}\n      data-tooltip-content={\n        watched ? \"Stop watching for changes\" : \"Watch document for changes\"\n      }\n    >\n      <Eye\n        size={16}\n        weight=\"regular\"\n        className=\"outline-none text-base font-bold flex-shrink-0 group-hover:hidden group-data-[active=true]:hidden\"\n      />\n      <Eye\n        size={16}\n        weight=\"fill\"\n        className=\"outline-none text-base font-bold flex-shrink-0 hidden group-hover:block group-data-[active=true]:block\"\n      />\n    </div>\n  );\n});\n\nconst RemoveItemFromWorkspace = ({ item: _item, onClick }) => {\n  return (\n    <div>\n      <ArrowUUpLeft\n        data-tooltip-id=\"remove-document\"\n        data-tooltip-content=\"Remove document from workspace\"\n        onClick={onClick}\n        className=\"text-base font-bold w-4 h-4 ml-2 flex-shrink-0 cursor-pointer\"\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx",
    "content": "import PreLoader from \"@/components/Preloader\";\nimport { dollarFormat } from \"@/utils/numbers\";\nimport WorkspaceFileRow from \"./WorkspaceFileRow\";\nimport { memo, useEffect, useState } from \"react\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { Eye, PushPin } from \"@phosphor-icons/react\";\nimport { SEEN_DOC_PIN_ALERT, SEEN_WATCH_ALERT } from \"@/utils/constants\";\nimport paths from \"@/utils/paths\";\nimport { Link } from \"react-router-dom\";\nimport Workspace from \"@/models/workspace\";\nimport { Tooltip } from \"react-tooltip\";\nimport { safeJsonParse } from \"@/utils/request\";\nimport { useTranslation } from \"react-i18next\";\n\nfunction WorkspaceDirectory({\n  workspace,\n  files,\n  highlightWorkspace,\n  loading,\n  loadingMessage,\n  setLoadingMessage,\n  setLoading,\n  fetchKeys,\n  hasChanges,\n  saveChanges,\n  embeddingCosts,\n  movedItems,\n}) {\n  const { t } = useTranslation();\n  const [selectedItems, setSelectedItems] = useState({});\n  const embeddedDocCount = (files?.items ?? []).reduce(\n    (sum, folder) => sum + (folder.items?.length ?? 0),\n    0\n  );\n\n  const toggleSelection = (item) => {\n    setSelectedItems((prevSelectedItems) => {\n      const newSelectedItems = { ...prevSelectedItems };\n      if (newSelectedItems[item.id]) {\n        delete newSelectedItems[item.id];\n      } else {\n        newSelectedItems[item.id] = true;\n      }\n      return newSelectedItems;\n    });\n  };\n\n  const toggleSelectAll = () => {\n    const allItems = files.items.flatMap((folder) => folder.items);\n    const allSelected = allItems.every((item) => selectedItems[item.id]);\n    if (allSelected) {\n      setSelectedItems({});\n    } else {\n      const newSelectedItems = {};\n      allItems.forEach((item) => {\n        newSelectedItems[item.id] = true;\n      });\n      setSelectedItems(newSelectedItems);\n    }\n  };\n\n  const removeSelectedItems = async () => {\n    setLoading(true);\n    setLoadingMessage(\"Removing selected files from workspace\");\n\n    const itemsToRemove = Object.keys(selectedItems).map((itemId) => {\n      const folder = files.items.find((f) =>\n        f.items.some((i) => i.id === itemId)\n      );\n      const item = folder.items.find((i) => i.id === itemId);\n      return `${folder.name}/${item.name}`;\n    });\n\n    try {\n      await Workspace.modifyEmbeddings(workspace.slug, {\n        adds: [],\n        deletes: itemsToRemove,\n      });\n      await fetchKeys(true);\n      setSelectedItems({});\n    } catch (error) {\n      console.error(\"Failed to remove documents:\", error);\n    }\n\n    setLoadingMessage(\"\");\n    setLoading(false);\n  };\n\n  const handleSaveChanges = (e) => {\n    setSelectedItems({});\n    saveChanges(e);\n  };\n\n  if (loading) {\n    return (\n      <div className=\"px-8\">\n        <div className=\"flex items-center justify-start w-[560px]\">\n          <h3 className=\"text-white text-base font-bold ml-5\">\n            {workspace.name}\n          </h3>\n        </div>\n        <div className=\"relative w-[560px] h-[445px] bg-theme-settings-input-bg rounded-2xl mt-5 border border-theme-modal-border\">\n          <div className=\"text-white/80 text-xs grid grid-cols-12 py-2 px-3.5 border-b border-white/20 light:border-theme-modal-border bg-theme-settings-input-bg sticky top-0 z-10 rounded-t-2xl\">\n            <div className=\"col-span-10 flex items-center gap-x-[4px]\">\n              <div className=\"shrink-0 w-3 h-3\" />\n              <p className=\"ml-[7px] text-theme-text-primary\">Name</p>\n            </div>\n          </div>\n          <div className=\"w-full h-[calc(100%-40px)] flex items-center justify-center flex-col gap-y-5\">\n            <PreLoader />\n            <p className=\"text-theme-text-primary text-sm font-semibold animate-pulse text-center w-1/3\">\n              {loadingMessage}\n            </p>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <>\n      <div className=\"px-8\">\n        <div className=\"flex items-center justify-start w-[560px]\">\n          <h3 className=\"text-white text-base font-bold ml-5\">\n            {workspace.name}\n          </h3>\n        </div>\n        <div className=\"relative w-[560px] h-[445px] mt-5\">\n          <div\n            className={`absolute inset-0 rounded-2xl  ${\n              highlightWorkspace ? \"border-4 border-cyan-300/80 z-[999]\" : \"\"\n            }`}\n          />\n          <div className=\"relative w-full h-full bg-theme-settings-input-bg rounded-2xl overflow-hidden border border-theme-modal-border\">\n            <div className=\"text-white/80 text-xs grid grid-cols-12 py-2 px-3.5 border-b border-white/20 light:border-theme-modal-border bg-theme-settings-input-bg sticky top-0 z-10\">\n              <div className=\"col-span-10 flex items-center gap-x-[4px]\">\n                {!hasChanges &&\n                files.items.some((folder) => folder.items.length > 0) ? (\n                  <div\n                    className={`shrink-0 w-3 h-3 rounded border-[1px] border-solid border-white text-theme-text-primary light:invert flex justify-center items-center cursor-pointer`}\n                    role=\"checkbox\"\n                    aria-checked={\n                      Object.keys(selectedItems).length ===\n                      files.items.reduce(\n                        (sum, folder) => sum + folder.items.length,\n                        0\n                      )\n                    }\n                    tabIndex={0}\n                    onClick={toggleSelectAll}\n                  >\n                    {Object.keys(selectedItems).length ===\n                      files.items.reduce(\n                        (sum, folder) => sum + folder.items.length,\n                        0\n                      ) && <div className=\"w-2 h-2 bg-white rounded-[2px]\" />}\n                  </div>\n                ) : (\n                  <div className=\"shrink-0 w-3 h-3\" />\n                )}\n                <p className=\"ml-[7px] text-theme-text-primary\">Name</p>\n              </div>\n              {embeddedDocCount > 0 && (\n                <p className=\"col-span-2 text-right text-theme-text-secondary pr-2\">\n                  {t(`connectors.directory.total-documents`, {\n                    count: embeddedDocCount,\n                  })}\n                </p>\n              )}\n            </div>\n            <div className=\"overflow-y-auto h-[calc(100%-40px)]\">\n              {files.items.some((folder) => folder.items.length > 0) ||\n              movedItems.length > 0 ? (\n                <RenderFileRows\n                  files={files}\n                  movedItems={movedItems}\n                  workspace={workspace}\n                >\n                  {({ item, folder }) => (\n                    <WorkspaceFileRow\n                      key={item.id}\n                      item={item}\n                      folderName={folder.name}\n                      workspace={workspace}\n                      setLoading={setLoading}\n                      setLoadingMessage={setLoadingMessage}\n                      fetchKeys={fetchKeys}\n                      hasChanges={hasChanges}\n                      movedItems={movedItems}\n                      selected={selectedItems[item.id]}\n                      toggleSelection={() => toggleSelection(item)}\n                      disableSelection={hasChanges}\n                      setSelectedItems={setSelectedItems}\n                    />\n                  )}\n                </RenderFileRows>\n              ) : (\n                <div className=\"w-full h-full flex items-center justify-center\">\n                  <p className=\"text-white text-opacity-40 text-sm font-medium\">\n                    {t(\"connectors.directory.no_docs\")}\n                  </p>\n                </div>\n              )}\n            </div>\n\n            {Object.keys(selectedItems).length > 0 && !hasChanges && (\n              <div className=\"absolute bottom-[12px] left-0 right-0 flex justify-center pointer-events-none\">\n                <div className=\"mx-auto bg-white/40 light:bg-white rounded-lg py-1 px-2 pointer-events-auto light:shadow-lg\">\n                  <div className=\"flex flex-row items-center gap-x-2\">\n                    <button\n                      onClick={toggleSelectAll}\n                      className=\"border-none text-sm font-semibold bg-white light:bg-[#E0F2FE] h-[30px] px-2.5 rounded-lg hover:bg-neutral-800/80 hover:text-white light:text-[#026AA2] light:hover:bg-[#026AA2] light:hover:text-white\"\n                    >\n                      {Object.keys(selectedItems).length ===\n                      files.items.reduce(\n                        (sum, folder) => sum + folder.items.length,\n                        0\n                      )\n                        ? t(\"connectors.directory.deselect_all\")\n                        : t(\"connectors.directory.select_all\")}\n                    </button>\n                    <button\n                      onClick={removeSelectedItems}\n                      className=\"border-none text-sm font-semibold bg-white light:bg-[#E0F2FE] h-[30px] px-2.5 rounded-lg hover:bg-neutral-800/80 hover:text-white light:text-[#026AA2] light:hover:bg-[#026AA2] light:hover:text-white\"\n                    >\n                      {t(\"connectors.directory.remove_selected\")}\n                    </button>\n                  </div>\n                </div>\n              </div>\n            )}\n          </div>\n        </div>\n        {hasChanges && (\n          <div className=\"flex items-center justify-between py-6\">\n            <div className=\"text-white/80\">\n              <p className=\"text-sm font-semibold\">\n                {embeddingCosts === 0\n                  ? \"\"\n                  : `Estimated Cost: ${\n                      embeddingCosts < 0.01\n                        ? `< $0.01`\n                        : dollarFormat(embeddingCosts)\n                    }`}\n              </p>\n              <p className=\"mt-2 text-xs italic\" hidden={embeddingCosts === 0}>\n                {t(\"connectors.directory.costs\")}\n              </p>\n            </div>\n\n            <button\n              onClick={(e) => handleSaveChanges(e)}\n              className=\"border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800\"\n            >\n              {t(\"connectors.directory.save_embed\")}\n            </button>\n          </div>\n        )}\n      </div>\n      <PinAlert />\n      <DocumentWatchAlert />\n      <WorkspaceDocumentTooltips />\n    </>\n  );\n}\n\nconst PinAlert = memo(() => {\n  const { t } = useTranslation();\n  const [showAlert, setShowAlert] = useState(false);\n  function dismissAlert() {\n    setShowAlert(false);\n    window.localStorage.setItem(SEEN_DOC_PIN_ALERT, \"1\");\n    window.removeEventListener(handlePinEvent);\n  }\n\n  function handlePinEvent() {\n    if (!!window?.localStorage?.getItem(SEEN_DOC_PIN_ALERT)) return;\n    setShowAlert(true);\n  }\n\n  useEffect(() => {\n    if (!window || !!window?.localStorage?.getItem(SEEN_DOC_PIN_ALERT)) return;\n    window?.addEventListener(\"pinned_document\", handlePinEvent);\n  }, []);\n\n  return (\n    <ModalWrapper isOpen={showAlert} noPortal={true}>\n      <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"flex items-center gap-2\">\n            <PushPin\n              className=\"text-theme-text-primary text-lg w-6 h-6\"\n              weight=\"regular\"\n            />\n            <h3 className=\"text-xl font-semibold text-white\">\n              {t(\"connectors.pinning.what_pinning\")}\n            </h3>\n          </div>\n        </div>\n        <div className=\"py-7 px-9 space-y-2 flex-col\">\n          <div className=\"w-full text-white text-md flex flex-col gap-y-2\">\n            <p>\n              <span\n                dangerouslySetInnerHTML={{\n                  __html: t(\"connectors.pinning.pin_explained_block1\"),\n                }}\n              />\n            </p>\n            <p>\n              <span\n                dangerouslySetInnerHTML={{\n                  __html: t(\"connectors.pinning.pin_explained_block2\"),\n                }}\n              />\n            </p>\n            <p>{t(\"connectors.pinning.pin_explained_block3\")}</p>\n          </div>\n        </div>\n        <div className=\"flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b\">\n          <button\n            onClick={dismissAlert}\n            className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n          >\n            {t(\"connectors.pinning.accept\")}\n          </button>\n        </div>\n      </div>\n    </ModalWrapper>\n  );\n});\n\nconst DocumentWatchAlert = memo(() => {\n  const { t } = useTranslation();\n  const [showAlert, setShowAlert] = useState(false);\n  function dismissAlert() {\n    setShowAlert(false);\n    window.localStorage.setItem(SEEN_WATCH_ALERT, \"1\");\n    window.removeEventListener(handlePinEvent);\n  }\n\n  function handlePinEvent() {\n    if (!!window?.localStorage?.getItem(SEEN_WATCH_ALERT)) return;\n    setShowAlert(true);\n  }\n\n  useEffect(() => {\n    if (!window || !!window?.localStorage?.getItem(SEEN_WATCH_ALERT)) return;\n    window?.addEventListener(\"watch_document_for_changes\", handlePinEvent);\n  }, []);\n\n  return (\n    <ModalWrapper isOpen={showAlert} noPortal={true}>\n      <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"flex items-center gap-2\">\n            <Eye\n              className=\"text-theme-text-primary text-lg w-6 h-6\"\n              weight=\"regular\"\n            />\n            <h3 className=\"text-xl font-semibold text-white\">\n              {t(\"connectors.watching.what_watching\")}\n            </h3>\n          </div>\n        </div>\n        <div className=\"py-7 px-9 space-y-2 flex-col\">\n          <div className=\"w-full text-white text-md flex flex-col gap-y-2\">\n            <p>\n              <span\n                dangerouslySetInnerHTML={{\n                  __html: t(\"connectors.watching.watch_explained_block1\"),\n                }}\n              />\n            </p>\n            <p>{t(\"connectors.watching.watch_explained_block2\")}</p>\n            <p>\n              {t(\"connectors.watching.watch_explained_block3_start\")}\n              <Link\n                to={paths.experimental.liveDocumentSync.manage()}\n                className=\"text-blue-600 underline\"\n              >\n                {t(\"connectors.watching.watch_explained_block3_link\")}\n              </Link>\n              {t(\"connectors.watching.watch_explained_block3_end\")}\n            </p>\n          </div>\n        </div>\n        <div className=\"flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b\">\n          <button\n            onClick={dismissAlert}\n            className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n          >\n            {t(\"connectors.watching.accept\")}\n          </button>\n        </div>\n      </div>\n    </ModalWrapper>\n  );\n});\n\nfunction RenderFileRows({ files, movedItems, children, workspace }) {\n  function sortMovedItemsAndFiles(a, b) {\n    const aIsMovedItem = movedItems.some((movedItem) => movedItem.id === a.id);\n    const bIsMovedItem = movedItems.some((movedItem) => movedItem.id === b.id);\n    if (aIsMovedItem && !bIsMovedItem) return -1;\n    if (!aIsMovedItem && bIsMovedItem) return 1;\n\n    // Sort pinned items to the top\n    const aIsPinned = a.pinnedWorkspaces?.includes(workspace.id);\n    const bIsPinned = b.pinnedWorkspaces?.includes(workspace.id);\n    if (aIsPinned && !bIsPinned) return -1;\n    if (!aIsPinned && bIsPinned) return 1;\n\n    return 0;\n  }\n\n  return files.items\n    .flatMap((folder) => folder.items)\n    .sort(sortMovedItemsAndFiles)\n    .map((item) => {\n      const folder = files.items.find((f) => f.items.includes(item));\n      return children({ item, folder });\n    });\n}\n\n/**\n * Tooltips for the workspace directory components. Renders when the workspace directory is shown\n * or updated so that tooltips are attached as the items are changed.\n */\nfunction WorkspaceDocumentTooltips() {\n  return (\n    <>\n      <Tooltip\n        id=\"ws-directory-item\"\n        place=\"bottom\"\n        delayShow={800}\n        className=\"tooltip invert light:invert-0 z-99 max-w-[200px]\"\n        render={({ content }) => {\n          const data = safeJsonParse(content, null);\n          if (!data) return null;\n          return (\n            <div className=\"text-xs\">\n              <p className=\"text-white light:invert font-medium break-all\">\n                {data.title}\n              </p>\n              <div className=\"flex mt-1 gap-x-2\">\n                <p className=\"\">\n                  Date: <b>{data.date}</b>\n                </p>\n                <p className=\"\">\n                  Type: <b>{data.extension}</b>\n                </p>\n              </div>\n            </div>\n          );\n        }}\n      />\n      <Tooltip\n        id=\"watch-changes\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip invert !text-xs\"\n      />\n      <Tooltip\n        id=\"pin-document\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip invert !text-xs\"\n      />\n      <Tooltip\n        id=\"remove-document\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip invert !text-xs\"\n      />\n    </>\n  );\n}\n\nexport default memo(WorkspaceDirectory);\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/Documents/index.jsx",
    "content": "import { ArrowsDownUp } from \"@phosphor-icons/react\";\nimport { useEffect, useState } from \"react\";\nimport Workspace from \"../../../../models/workspace\";\nimport System from \"../../../../models/system\";\nimport showToast from \"../../../../utils/toast\";\nimport Directory from \"./Directory\";\nimport WorkspaceDirectory from \"./WorkspaceDirectory\";\n\n// OpenAI Cost per token\n// ref: https://openai.com/pricing#:~:text=%C2%A0/%201K%20tokens-,Embedding%20models,-Build%20advanced%20search\n\nconst MODEL_COSTS = {\n  \"text-embedding-ada-002\": 0.0000001, // $0.0001 / 1K tokens\n  \"text-embedding-3-small\": 0.00000002, // $0.00002 / 1K tokens\n  \"text-embedding-3-large\": 0.00000013, // $0.00013 / 1K tokens\n};\n\nexport default function DocumentSettings({ workspace, systemSettings }) {\n  const [highlightWorkspace, setHighlightWorkspace] = useState(false);\n  const [availableDocs, setAvailableDocs] = useState([]);\n  const [loading, setLoading] = useState(true);\n  const [workspaceDocs, setWorkspaceDocs] = useState([]);\n  const [selectedItems, setSelectedItems] = useState({});\n  const [hasChanges, setHasChanges] = useState(false);\n  const [movedItems, setMovedItems] = useState([]);\n  const [embeddingsCost, setEmbeddingsCost] = useState(0);\n  const [loadingMessage, setLoadingMessage] = useState(\"\");\n\n  async function fetchKeys(refetchWorkspace = false) {\n    setLoading(true);\n    const localFiles = await System.localFiles();\n    const currentWorkspace = refetchWorkspace\n      ? await Workspace.bySlug(workspace.slug)\n      : workspace;\n\n    const documentsInWorkspace =\n      currentWorkspace.documents.map((doc) => doc.docpath) || [];\n\n    // Documents that are not in the workspace\n    const availableDocs = {\n      ...localFiles,\n      items: localFiles.items.map((folder) => {\n        if (folder.items && folder.type === \"folder\") {\n          return {\n            ...folder,\n            items: folder.items.filter(\n              (file) =>\n                file.type === \"file\" &&\n                !documentsInWorkspace.includes(`${folder.name}/${file.name}`)\n            ),\n          };\n        } else {\n          return folder;\n        }\n      }),\n    };\n\n    // Documents that are already in the workspace\n    const workspaceDocs = {\n      ...localFiles,\n      items: localFiles.items.map((folder) => {\n        if (folder.items && folder.type === \"folder\") {\n          return {\n            ...folder,\n            items: folder.items.filter(\n              (file) =>\n                file.type === \"file\" &&\n                documentsInWorkspace.includes(`${folder.name}/${file.name}`)\n            ),\n          };\n        } else {\n          return folder;\n        }\n      }),\n    };\n\n    setAvailableDocs(availableDocs);\n    setWorkspaceDocs(workspaceDocs);\n    setLoading(false);\n  }\n\n  useEffect(() => {\n    fetchKeys(true);\n  }, []);\n\n  const updateWorkspace = async (e) => {\n    e.preventDefault();\n    setLoading(true);\n    showToast(\"Updating workspace...\", \"info\", { autoClose: false });\n    setLoadingMessage(\"This may take a while for large documents\");\n\n    const changesToSend = {\n      adds: movedItems.map((item) => `${item.folderName}/${item.name}`),\n    };\n\n    setSelectedItems({});\n    setHasChanges(false);\n    setHighlightWorkspace(false);\n    await Workspace.modifyEmbeddings(workspace.slug, changesToSend)\n      .then((res) => {\n        if (!!res.message) {\n          showToast(`Error: ${res.message}`, \"error\", { clear: true });\n          return;\n        }\n        showToast(\"Workspace updated successfully.\", \"success\", {\n          clear: true,\n        });\n      })\n      .catch((error) => {\n        showToast(`Workspace update failed: ${error}`, \"error\", {\n          clear: true,\n        });\n      });\n\n    setMovedItems([]);\n    await fetchKeys(true);\n    setLoading(false);\n    setLoadingMessage(\"\");\n  };\n\n  const moveSelectedItemsToWorkspace = () => {\n    setHighlightWorkspace(false);\n    setHasChanges(true);\n\n    const newMovedItems = [];\n\n    for (const itemId of Object.keys(selectedItems)) {\n      for (const folder of availableDocs.items) {\n        const foundItem = folder.items.find((file) => file.id === itemId);\n        if (foundItem) {\n          newMovedItems.push({ ...foundItem, folderName: folder.name });\n          break;\n        }\n      }\n    }\n\n    let totalTokenCount = 0;\n    newMovedItems.forEach((item) => {\n      const { cached, token_count_estimate } = item;\n      if (!cached) {\n        totalTokenCount += token_count_estimate;\n      }\n    });\n\n    // Do not do cost estimation unless the embedding engine is OpenAi.\n    if (systemSettings?.EmbeddingEngine === \"openai\") {\n      const COST_PER_TOKEN =\n        MODEL_COSTS[\n          systemSettings?.EmbeddingModelPref || \"text-embedding-ada-002\"\n        ];\n\n      const dollarAmount = (totalTokenCount / 1000) * COST_PER_TOKEN;\n      setEmbeddingsCost(dollarAmount);\n    }\n\n    setMovedItems([...movedItems, ...newMovedItems]);\n\n    let newAvailableDocs = JSON.parse(JSON.stringify(availableDocs));\n    let newWorkspaceDocs = JSON.parse(JSON.stringify(workspaceDocs));\n\n    for (const itemId of Object.keys(selectedItems)) {\n      let foundItem = null;\n      let foundFolderIndex = null;\n\n      newAvailableDocs.items = newAvailableDocs.items.map(\n        (folder, folderIndex) => {\n          const remainingItems = folder.items.filter((file) => {\n            const match = file.id === itemId;\n            if (match) {\n              foundItem = { ...file };\n              foundFolderIndex = folderIndex;\n            }\n            return !match;\n          });\n\n          return {\n            ...folder,\n            items: remainingItems,\n          };\n        }\n      );\n\n      if (foundItem) {\n        newWorkspaceDocs.items[foundFolderIndex].items.push(foundItem);\n      }\n    }\n\n    setAvailableDocs(newAvailableDocs);\n    setWorkspaceDocs(newWorkspaceDocs);\n    setSelectedItems({});\n  };\n\n  return (\n    <div className=\"flex upload-modal -mt-6 z-10 relative\">\n      <Directory\n        files={availableDocs}\n        setFiles={setAvailableDocs}\n        loading={loading}\n        loadingMessage={loadingMessage}\n        setLoading={setLoading}\n        workspace={workspace}\n        fetchKeys={fetchKeys}\n        selectedItems={selectedItems}\n        setSelectedItems={setSelectedItems}\n        updateWorkspace={updateWorkspace}\n        highlightWorkspace={highlightWorkspace}\n        setHighlightWorkspace={setHighlightWorkspace}\n        moveToWorkspace={moveSelectedItemsToWorkspace}\n        setLoadingMessage={setLoadingMessage}\n      />\n      <div className=\"upload-modal-arrow\">\n        <ArrowsDownUp className=\"text-white text-base font-bold rotate-90 w-11 h-11\" />\n      </div>\n      <WorkspaceDirectory\n        workspace={workspace}\n        files={workspaceDocs}\n        highlightWorkspace={highlightWorkspace}\n        loading={loading}\n        loadingMessage={loadingMessage}\n        setLoadingMessage={setLoadingMessage}\n        setLoading={setLoading}\n        fetchKeys={fetchKeys}\n        hasChanges={hasChanges}\n        saveChanges={updateWorkspace}\n        embeddingCosts={embeddingsCost}\n        movedItems={movedItems}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/ManageWorkspace/index.jsx",
    "content": "import React, { useState, useEffect, memo } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useParams } from \"react-router-dom\";\nimport Workspace from \"../../../models/workspace\";\nimport System from \"../../../models/system\";\nimport { isMobile } from \"react-device-detect\";\nimport useUser from \"../../../hooks/useUser\";\nimport DocumentSettings from \"./Documents\";\nimport DataConnectors from \"./DataConnectors\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\n\nconst noop = () => {};\nconst ManageWorkspace = ({ hideModal = noop, providedSlug = null }) => {\n  const { t } = useTranslation();\n  const { slug } = useParams();\n  const { user } = useUser();\n  const [workspace, setWorkspace] = useState(null);\n  const [settings, setSettings] = useState({});\n  const [selectedTab, setSelectedTab] = useState(\"documents\");\n\n  useEffect(() => {\n    async function getSettings() {\n      const _settings = await System.keys();\n      setSettings(_settings ?? {});\n    }\n    getSettings();\n  }, []);\n\n  useEffect(() => {\n    async function fetchWorkspace() {\n      const workspace = await Workspace.bySlug(providedSlug ?? slug);\n      setWorkspace(workspace);\n    }\n    fetchWorkspace();\n  }, [providedSlug, slug]);\n\n  if (!workspace) return null;\n\n  if (isMobile) {\n    return (\n      <ModalWrapper isOpen={true}>\n        <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden\">\n          <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n            <div className=\"w-full flex gap-x-2 items-center\">\n              <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n                {t(\"connectors.manage.editing\")} \"{workspace.name}\"\n              </h3>\n            </div>\n            <button\n              onClick={hideModal}\n              type=\"button\"\n              className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n            >\n              <X size={24} weight=\"bold\" className=\"text-white\" />\n            </button>\n          </div>\n          <div\n            className=\"h-full w-full overflow-y-auto\"\n            style={{ maxHeight: \"calc(100vh - 200px)\" }}\n          >\n            <div className=\"py-7 px-9 space-y-2 flex-col\">\n              <p className=\"text-white\">\n                {t(\"connectors.manage.desktop-only\")}\n              </p>\n            </div>\n          </div>\n          <div className=\"flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b\">\n            <button\n              onClick={hideModal}\n              type=\"button\"\n              className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n            >\n              {t(\"connectors.manage.dismiss\")}\n            </button>\n          </div>\n        </div>\n      </ModalWrapper>\n    );\n  }\n\n  return (\n    <div className=\"w-screen h-screen fixed top-0 left-0 flex justify-center items-center z-99\">\n      <div className=\"backdrop h-full w-full absolute top-0 z-10\" />\n      <div className=\"absolute max-h-full w-fit transition duration-300 z-20 md:overflow-y-auto py-10\">\n        <div className=\"relative bg-theme-bg-secondary rounded-[12px] shadow border-2 border-theme-modal-border\">\n          <div className=\"flex items-start justify-between p-2 rounded-t border-theme-modal-border relative\">\n            <button\n              onClick={hideModal}\n              type=\"button\"\n              className=\"z-29 text-white bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n            >\n              <X size={20} weight=\"bold\" className=\"text-white\" />\n            </button>\n          </div>\n\n          {user?.role !== \"default\" && (\n            <ModalTabSwitcher\n              selectedTab={selectedTab}\n              setSelectedTab={setSelectedTab}\n            />\n          )}\n\n          {selectedTab === \"documents\" ? (\n            <DocumentSettings workspace={workspace} systemSettings={settings} />\n          ) : (\n            <DataConnectors workspace={workspace} systemSettings={settings} />\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default memo(ManageWorkspace);\n\nconst ModalTabSwitcher = ({ selectedTab, setSelectedTab }) => {\n  const { t } = useTranslation();\n  return (\n    <div className=\"w-full flex justify-center z-10 relative\">\n      <div className=\"gap-x-2 flex justify-center -mt-[68px] mb-10 bg-theme-bg-secondary p-1 rounded-xl shadow border-2 border-theme-modal-border w-fit\">\n        <button\n          onClick={() => setSelectedTab(\"documents\")}\n          className={`border-none px-4 py-2 rounded-[8px] font-semibold hover:bg-theme-modal-border hover:bg-opacity-60 ${\n            selectedTab === \"documents\"\n              ? \"bg-theme-modal-border font-bold text-white light:bg-[#E0F2FE] light:text-[#026AA2]\"\n              : \"text-white/20 font-medium hover:text-white light:bg-white light:text-[#535862] light:hover:bg-[#E0F2FE]\"\n          }`}\n        >\n          {t(\"connectors.manage.documents\")}\n        </button>\n        <button\n          onClick={() => setSelectedTab(\"dataConnectors\")}\n          className={`border-none px-4 py-2 rounded-[8px] font-semibold hover:bg-theme-modal-border hover:bg-opacity-60 ${\n            selectedTab === \"dataConnectors\"\n              ? \"bg-theme-modal-border font-bold text-white light:bg-[#E0F2FE] light:text-[#026AA2]\"\n              : \"text-white/20 font-medium hover:text-white light:bg-white light:text-[#535862] light:hover:bg-[#E0F2FE]\"\n          }`}\n        >\n          {t(\"connectors.manage.data-connectors\")}\n        </button>\n      </div>\n    </div>\n  );\n};\n\nexport function useManageWorkspaceModal() {\n  const { user } = useUser();\n  const [showing, setShowing] = useState(false);\n\n  function showModal() {\n    if (user?.role !== \"default\") {\n      setShowing(true);\n    }\n  }\n\n  function hideModal() {\n    setShowing(false);\n  }\n\n  useEffect(() => {\n    function onEscape(event) {\n      if (!showing || event.key !== \"Escape\") return;\n      setShowing(false);\n    }\n\n    document.addEventListener(\"keydown\", onEscape);\n    return () => {\n      document.removeEventListener(\"keydown\", onEscape);\n    };\n  }, [showing]);\n\n  return { showing, showModal, hideModal };\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/NewWorkspace.jsx",
    "content": "import React, { useRef, useState } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport Workspace from \"@/models/workspace\";\nimport paths from \"@/utils/paths\";\nimport { useTranslation } from \"react-i18next\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\n\nconst noop = () => false;\nexport default function NewWorkspaceModal({ hideModal = noop }) {\n  const formEl = useRef(null);\n  const [error, setError] = useState(null);\n  const { t } = useTranslation();\n  const handleCreate = async (e) => {\n    setError(null);\n    e.preventDefault();\n    const data = {};\n    const form = new FormData(formEl.current);\n    for (var [key, value] of form.entries()) data[key] = value;\n    const { workspace, message } = await Workspace.new(data);\n    if (!!workspace) {\n      window.location.href = paths.workspace.chat(workspace.slug);\n    }\n    setError(message);\n  };\n\n  return (\n    <ModalWrapper isOpen={true}>\n      <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              {t(\"new-workspace.title\")}\n            </h3>\n          </div>\n          <button\n            onClick={hideModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div\n          className=\"h-full w-full overflow-y-auto\"\n          style={{ maxHeight: \"calc(100vh - 200px)\" }}\n        >\n          <form ref={formEl} onSubmit={handleCreate}>\n            <div className=\"py-7 px-9 space-y-2 flex-col\">\n              <div className=\"w-full flex flex-col gap-y-4\">\n                <div>\n                  <label\n                    htmlFor=\"name\"\n                    className=\"block mb-2 text-sm font-medium text-white\"\n                  >\n                    {t(\"common.workspaces-name\")}\n                  </label>\n                  <input\n                    name=\"name\"\n                    type=\"text\"\n                    id=\"name\"\n                    className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                    placeholder={t(\"new-workspace.placeholder\")}\n                    required={true}\n                    autoComplete=\"off\"\n                    autoFocus={true}\n                  />\n                </div>\n                {error && (\n                  <p className=\"text-red-400 text-sm\">Error: {error}</p>\n                )}\n              </div>\n            </div>\n            <div className=\"flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b\">\n              <button\n                type=\"submit\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                Save\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </ModalWrapper>\n  );\n}\n\nexport function useNewWorkspaceModal() {\n  const [showing, setShowing] = useState(false);\n  const showModal = () => {\n    setShowing(true);\n  };\n  const hideModal = () => {\n    setShowing(false);\n  };\n\n  return { showing, showModal, hideModal };\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/Password/MultiUserAuth.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport System from \"../../../models/system\";\nimport { AUTH_TOKEN, AUTH_USER } from \"../../../utils/constants\";\nimport paths from \"../../../utils/paths\";\nimport showToast from \"@/utils/toast\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { useModal } from \"@/hooks/useModal\";\nimport RecoveryCodeModal from \"@/components/Modals/DisplayRecoveryCodeModal\";\nimport { useTranslation } from \"react-i18next\";\nimport { t } from \"i18next\";\n\nconst RecoveryForm = ({ onSubmit, setShowRecoveryForm }) => {\n  const [username, setUsername] = useState(\"\");\n  const [recoveryCodeInputs, setRecoveryCodeInputs] = useState(\n    Array(2).fill(\"\")\n  );\n\n  const handleRecoveryCodeChange = (index, value) => {\n    const updatedCodes = [...recoveryCodeInputs];\n    updatedCodes[index] = value;\n    setRecoveryCodeInputs(updatedCodes);\n  };\n\n  const handleSubmit = (e) => {\n    e.preventDefault();\n    const recoveryCodes = recoveryCodeInputs.filter(\n      (code) => code.trim() !== \"\"\n    );\n    onSubmit(username, recoveryCodes);\n  };\n\n  return (\n    <form\n      onSubmit={handleSubmit}\n      className=\"flex flex-col justify-center items-center\"\n    >\n      <div className=\"flex items-start justify-between pt-7 pb-9\">\n        <div className=\"flex items-center flex-col gap-y-[18px] max-w-[300px]\">\n          <div className=\"flex gap-x-1\">\n            <h3 className=\"text-white light:text-slate-950 text-3xl leading-[28px] font-medium text-center white-space-nowrap block\">\n              {t(\"login.password-reset.title\")}\n            </h3>\n          </div>\n          <p className=\"text-zinc-400 light:text-zinc-600 text-sm text-center\">\n            {t(\"login.password-reset.description\")}\n          </p>\n        </div>\n      </div>\n      <div className=\"w-full px-12\">\n        <div className=\"w-full flex flex-col gap-y-3\">\n          <div className=\"w-full flex flex-col gap-y-2\">\n            <label className=\"text-zinc-300 light:text-slate-800 text-sm\">\n              {t(\"login.multi-user.placeholder-username\")}\n            </label>\n            <input\n              name=\"username\"\n              type=\"text\"\n              className=\"border-none bg-zinc-800 light:bg-slate-200 text-zinc-200 light:text-zinc-600 text-sm rounded-lg p-2.5 w-[300px] h-[34px] focus:outline-none focus:ring-1 focus:ring-sky-300\"\n              value={username}\n              onChange={(e) => setUsername(e.target.value)}\n              required\n              autoComplete=\"off\"\n            />\n          </div>\n          <div className=\"w-full flex flex-col gap-y-2\">\n            <label className=\"text-zinc-300 light:text-slate-800 text-sm\">\n              {t(\"login.password-reset.recovery-codes\")}\n            </label>\n            {recoveryCodeInputs.map((code, index) => (\n              <input\n                key={index}\n                type=\"text\"\n                name={`recoveryCode${index + 1}`}\n                className=\"border-none bg-zinc-800 light:bg-slate-200 text-zinc-200 light:text-zinc-600 text-sm rounded-lg p-2.5 w-[300px] h-[34px] focus:outline-none focus:ring-1 focus:ring-sky-300\"\n                value={code}\n                onChange={(e) =>\n                  handleRecoveryCodeChange(index, e.target.value)\n                }\n                required\n                autoComplete=\"off\"\n              />\n            ))}\n          </div>\n        </div>\n      </div>\n      <div className=\"flex items-center px-12 mt-9 space-x-2 w-full flex-col gap-y-6\">\n        <button\n          type=\"submit\"\n          className=\"text-zinc-950 bg-white hover:bg-zinc-300 light:bg-sky-200 light:text-slate-950 light:hover:bg-sky-300 text-sm font-semibold rounded-lg border-primary-button h-[34px] w-full\"\n        >\n          {t(\"login.password-reset.title\")}\n        </button>\n        <button\n          type=\"button\"\n          className=\"text-zinc-200 light:text-zinc-600 hover:text-sky-300 light:hover:text-sky-600 hover:underline text-sm flex gap-x-1\"\n          onClick={() => setShowRecoveryForm(false)}\n        >\n          {t(\"login.password-reset.back-to-login\")}\n        </button>\n      </div>\n    </form>\n  );\n};\n\nconst ResetPasswordForm = ({ onSubmit }) => {\n  const [newPassword, setNewPassword] = useState(\"\");\n  const [confirmPassword, setConfirmPassword] = useState(\"\");\n\n  const handleSubmit = (e) => {\n    e.preventDefault();\n    onSubmit(newPassword, confirmPassword);\n  };\n\n  return (\n    <form\n      onSubmit={handleSubmit}\n      className=\"flex flex-col justify-center items-center\"\n    >\n      <div className=\"flex items-start justify-between pt-7 pb-9\">\n        <div className=\"flex items-center flex-col gap-y-[18px] max-w-[300px]\">\n          <div className=\"flex gap-x-1\">\n            <h3 className=\"text-white light:text-slate-950 text-[38px] leading-[28px] font-medium text-center white-space-nowrap block\">\n              Reset Password\n            </h3>\n          </div>\n          <p className=\"text-zinc-400 light:text-zinc-600 text-sm text-center\">\n            Enter your new password.\n          </p>\n        </div>\n      </div>\n      <div className=\"w-full px-12\">\n        <div className=\"w-full flex flex-col gap-y-3\">\n          <div className=\"w-full flex flex-col gap-y-2\">\n            <label className=\"text-zinc-300 light:text-slate-800 text-sm\">\n              New Password\n            </label>\n            <input\n              type=\"password\"\n              name=\"newPassword\"\n              className=\"border-none bg-zinc-800 light:bg-slate-200 text-zinc-200 light:text-zinc-600 text-sm rounded-lg p-2.5 w-[300px] h-[34px] focus:outline-none focus:ring-1 focus:ring-sky-300\"\n              value={newPassword}\n              onChange={(e) => setNewPassword(e.target.value)}\n              required\n            />\n          </div>\n          <div className=\"w-full flex flex-col gap-y-2\">\n            <label className=\"text-zinc-300 light:text-slate-800 text-sm\">\n              Confirm Password\n            </label>\n            <input\n              type=\"password\"\n              name=\"confirmPassword\"\n              className=\"border-none bg-zinc-800 light:bg-slate-200 text-zinc-200 light:text-zinc-600 text-sm rounded-lg p-2.5 w-[300px] h-[34px] focus:outline-none focus:ring-1 focus:ring-sky-300\"\n              value={confirmPassword}\n              onChange={(e) => setConfirmPassword(e.target.value)}\n              required\n            />\n          </div>\n        </div>\n      </div>\n      <div className=\"flex items-center px-12 mt-9 space-x-2 w-full flex-col gap-y-6\">\n        <button\n          type=\"submit\"\n          className=\"text-zinc-950 bg-white hover:bg-zinc-300 light:bg-sky-200 light:text-slate-950 light:hover:bg-sky-300 text-sm font-semibold rounded-lg border-primary-button h-[34px] w-full\"\n        >\n          Reset Password\n        </button>\n      </div>\n    </form>\n  );\n};\n\nexport default function MultiUserAuth() {\n  const { t } = useTranslation();\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState(null);\n  const [recoveryCodes, setRecoveryCodes] = useState([]);\n  const [downloadComplete, setDownloadComplete] = useState(false);\n  const [user, setUser] = useState(null);\n  const [token, setToken] = useState(null);\n  const [showRecoveryForm, setShowRecoveryForm] = useState(false);\n  const [showResetPasswordForm, setShowResetPasswordForm] = useState(false);\n  const [customAppName, setCustomAppName] = useState(null);\n\n  const {\n    isOpen: isRecoveryCodeModalOpen,\n    openModal: openRecoveryCodeModal,\n    closeModal: closeRecoveryCodeModal,\n  } = useModal();\n\n  const handleLogin = async (e) => {\n    setError(null);\n    setLoading(true);\n    e.preventDefault();\n    const data = {};\n    const form = new FormData(e.target);\n    for (var [key, value] of form.entries()) data[key] = value;\n    const { valid, user, token, message, recoveryCodes } =\n      await System.requestToken(data);\n    if (valid && !!token && !!user) {\n      setUser(user);\n      setToken(token);\n\n      if (recoveryCodes) {\n        setRecoveryCodes(recoveryCodes);\n        openRecoveryCodeModal();\n      } else {\n        window.localStorage.setItem(AUTH_USER, JSON.stringify(user));\n        window.localStorage.setItem(AUTH_TOKEN, token);\n        window.location = paths.home();\n      }\n    } else {\n      setError(message);\n      setLoading(false);\n    }\n    setLoading(false);\n  };\n\n  const handleDownloadComplete = () => setDownloadComplete(true);\n  const handleResetPassword = () => setShowRecoveryForm(true);\n  const handleRecoverySubmit = async (username, recoveryCodes) => {\n    const { success, resetToken, error } = await System.recoverAccount(\n      username,\n      recoveryCodes\n    );\n\n    if (success && resetToken) {\n      window.localStorage.setItem(\"resetToken\", resetToken);\n      setShowRecoveryForm(false);\n      setShowResetPasswordForm(true);\n    } else {\n      showToast(error, \"error\", { clear: true });\n    }\n  };\n\n  const handleResetSubmit = async (newPassword, confirmPassword) => {\n    const resetToken = window.localStorage.getItem(\"resetToken\");\n\n    if (resetToken) {\n      const { success, error } = await System.resetPassword(\n        resetToken,\n        newPassword,\n        confirmPassword\n      );\n\n      if (success) {\n        window.localStorage.removeItem(\"resetToken\");\n        setShowResetPasswordForm(false);\n        showToast(\"Password reset successful\", \"success\", { clear: true });\n      } else {\n        showToast(error, \"error\", { clear: true });\n      }\n    } else {\n      showToast(\"Invalid reset token\", \"error\", { clear: true });\n    }\n  };\n\n  useEffect(() => {\n    if (downloadComplete && user && token) {\n      window.localStorage.setItem(AUTH_USER, JSON.stringify(user));\n      window.localStorage.setItem(AUTH_TOKEN, token);\n      window.location = paths.home();\n    }\n  }, [downloadComplete, user, token]);\n\n  useEffect(() => {\n    const fetchCustomAppName = async () => {\n      const { appName } = await System.fetchCustomAppName();\n      setCustomAppName(appName || \"\");\n      setLoading(false);\n    };\n    fetchCustomAppName();\n  }, []);\n\n  if (showRecoveryForm) {\n    return (\n      <RecoveryForm\n        onSubmit={handleRecoverySubmit}\n        setShowRecoveryForm={setShowRecoveryForm}\n      />\n    );\n  }\n\n  if (showResetPasswordForm)\n    return <ResetPasswordForm onSubmit={handleResetSubmit} />;\n  return (\n    <>\n      <form\n        onSubmit={handleLogin}\n        className=\"flex flex-col justify-center items-center\"\n      >\n        <div className=\"flex items-start justify-between pt-7 pb-9\">\n          <div className=\"flex items-center flex-col gap-y-[18px] max-w-[300px]\">\n            <div className=\"flex gap-x-1\">\n              <h3 className=\"text-white light:text-slate-950 text-[38px] leading-[28px] font-medium text-center white-space-nowrap block\">\n                {t(\"login.multi-user.welcome\")}\n              </h3>\n            </div>\n            <p className=\"text-zinc-400 light:text-zinc-600 text-sm text-center\">\n              {t(\"login.sign-in\", { appName: customAppName || \"AnythingLLM\" })}\n            </p>\n          </div>\n        </div>\n        <div className=\"w-full px-12\">\n          <div className=\"w-full flex flex-col gap-y-3\">\n            <div className=\"w-full flex flex-col gap-y-2\">\n              <label className=\"text-zinc-300 light:text-slate-800 text-sm\">\n                {t(\"login.multi-user.placeholder-username\")}\n              </label>\n              <input\n                name=\"username\"\n                type=\"text\"\n                className=\"border-none bg-zinc-800 light:bg-slate-200 text-zinc-200 light:text-zinc-600 text-sm rounded-lg p-2.5 w-[300px] h-[34px] focus:outline-none focus:ring-1 focus:ring-sky-300\"\n                required={true}\n                autoComplete=\"off\"\n              />\n            </div>\n            <div className=\"w-full px-0 flex flex-col gap-y-2\">\n              <label className=\"text-zinc-300 light:text-slate-800 text-sm\">\n                {t(\"login.multi-user.placeholder-password\")}\n              </label>\n              <input\n                name=\"password\"\n                type=\"password\"\n                className=\"border-none bg-zinc-800 light:bg-slate-200 text-zinc-200 light:text-zinc-600 text-sm rounded-lg p-2.5 w-[300px] h-[34px] focus:outline-none focus:ring-1 focus:ring-sky-300\"\n                required={true}\n                autoComplete=\"off\"\n              />\n            </div>\n            {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n          </div>\n        </div>\n        <div className=\"flex items-center px-12 mt-9 space-x-2 w-full flex-col gap-y-6\">\n          <button\n            disabled={loading}\n            type=\"submit\"\n            className=\"text-zinc-950 bg-white hover:bg-zinc-300 light:bg-sky-200 light:text-slate-950 light:hover:bg-sky-300 text-sm font-semibold rounded-lg border-primary-button h-[34px] w-full\"\n          >\n            {loading\n              ? t(\"login.multi-user.validating\")\n              : t(\"login.multi-user.login\")}\n          </button>\n          <button\n            type=\"button\"\n            className=\"text-zinc-200 light:text-zinc-600 hover:text-sky-300 light:hover:text-sky-600 hover:underline text-sm flex gap-x-1\"\n            onClick={handleResetPassword}\n          >\n            {t(\"login.multi-user.forgot-pass\")}?\n            <b className=\"font-semibold text-sky-300 light:text-sky-600\">\n              {t(\"login.multi-user.reset\")}\n            </b>\n          </button>\n        </div>\n      </form>\n\n      <ModalWrapper isOpen={isRecoveryCodeModalOpen} noPortal={true}>\n        <RecoveryCodeModal\n          recoveryCodes={recoveryCodes}\n          onDownloadComplete={handleDownloadComplete}\n          onClose={closeRecoveryCodeModal}\n        />\n      </ModalWrapper>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/Password/SingleUserAuth.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport System from \"../../../models/system\";\nimport { AUTH_TOKEN } from \"../../../utils/constants\";\nimport paths from \"../../../utils/paths\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { useModal } from \"@/hooks/useModal\";\nimport RecoveryCodeModal from \"@/components/Modals/DisplayRecoveryCodeModal\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function SingleUserAuth() {\n  const { t } = useTranslation();\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState(null);\n  const [recoveryCodes, setRecoveryCodes] = useState([]);\n  const [downloadComplete, setDownloadComplete] = useState(false);\n  const [token, setToken] = useState(null);\n  const [customAppName, setCustomAppName] = useState(null);\n\n  const {\n    isOpen: isRecoveryCodeModalOpen,\n    openModal: openRecoveryCodeModal,\n    closeModal: closeRecoveryCodeModal,\n  } = useModal();\n\n  const handleLogin = async (e) => {\n    setError(null);\n    setLoading(true);\n    e.preventDefault();\n    const data = {};\n    const form = new FormData(e.target);\n    for (var [key, value] of form.entries()) data[key] = value;\n    const { valid, token, message, recoveryCodes } =\n      await System.requestToken(data);\n    if (valid && !!token) {\n      setToken(token);\n      if (recoveryCodes) {\n        setRecoveryCodes(recoveryCodes);\n        openRecoveryCodeModal();\n      } else {\n        window.localStorage.setItem(AUTH_TOKEN, token);\n        window.location = paths.home();\n      }\n    } else {\n      setError(message);\n      setLoading(false);\n    }\n    setLoading(false);\n  };\n\n  const handleDownloadComplete = () => {\n    setDownloadComplete(true);\n  };\n\n  useEffect(() => {\n    if (downloadComplete && token) {\n      window.localStorage.setItem(AUTH_TOKEN, token);\n      window.location = paths.home();\n    }\n  }, [downloadComplete, token]);\n\n  useEffect(() => {\n    const fetchCustomAppName = async () => {\n      const { appName } = await System.fetchCustomAppName();\n      setCustomAppName(appName || \"\");\n      setLoading(false);\n    };\n    fetchCustomAppName();\n  }, []);\n\n  return (\n    <>\n      <form\n        onSubmit={handleLogin}\n        className=\"flex flex-col justify-center items-center\"\n      >\n        <div className=\"flex items-start justify-between pt-7 pb-9\">\n          <div className=\"flex items-center flex-col gap-y-[18px] max-w-[300px]\">\n            <div className=\"flex gap-x-1\">\n              <h3 className=\"text-white light:text-slate-950 text-3xl leading-[28px] font-medium text-center white-space-nowrap block\">\n                {t(\"login.multi-user.welcome\")}\n              </h3>\n            </div>\n            <p className=\"text-zinc-400 light:text-zinc-600 text-sm text-center\">\n              {t(\"login.sign-in\", { appName: customAppName || \"AnythingLLM\" })}\n            </p>\n          </div>\n        </div>\n        <div className=\"w-full px-12\">\n          <div className=\"w-full flex flex-col gap-y-3\">\n            <div className=\"w-full flex flex-col gap-y-2\">\n              <label className=\"text-zinc-300 light:text-slate-800 text-sm\">\n                Password\n              </label>\n              <input\n                name=\"password\"\n                type=\"password\"\n                className=\"border-none bg-zinc-800 light:bg-slate-200 text-zinc-200 light:text-zinc-600 text-sm rounded-lg p-2.5 w-[300px] h-[34px] focus:outline-none focus:ring-1 focus:ring-sky-300\"\n                required={true}\n                autoComplete=\"off\"\n              />\n            </div>\n            {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n          </div>\n        </div>\n        <div className=\"flex items-center px-12 mt-9 space-x-2 w-full flex-col gap-y-6\">\n          <button\n            disabled={loading}\n            type=\"submit\"\n            className=\"text-zinc-950 bg-white hover:bg-zinc-300 light:bg-sky-200 light:text-slate-950 light:hover:bg-sky-300 text-sm font-semibold rounded-lg border-primary-button h-[34px] w-full\"\n          >\n            {loading\n              ? t(\"login.multi-user.validating\")\n              : t(\"login.multi-user.login\")}\n          </button>\n        </div>\n      </form>\n\n      <ModalWrapper isOpen={isRecoveryCodeModalOpen} noPortal={true}>\n        <RecoveryCodeModal\n          recoveryCodes={recoveryCodes}\n          onDownloadComplete={handleDownloadComplete}\n          onClose={closeRecoveryCodeModal}\n        />\n      </ModalWrapper>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Modals/Password/index.jsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport System from \"../../../models/system\";\nimport SingleUserAuth from \"./SingleUserAuth\";\nimport MultiUserAuth from \"./MultiUserAuth\";\nimport {\n  AUTH_TOKEN,\n  AUTH_USER,\n  AUTH_TIMESTAMP,\n} from \"../../../utils/constants\";\nimport useLogo from \"../../../hooks/useLogo\";\n\nexport default function PasswordModal({ mode = \"single\" }) {\n  const { loginLogo, isCustomLogo } = useLogo();\n  return (\n    <div className=\"fixed inset-0 bg-zinc-950 light:bg-slate-50 flex flex-col items-center justify-center overflow-hidden\">\n      <img\n        src={loginLogo}\n        alt=\"Logo\"\n        className={`max-h-[80px] ${isCustomLogo ? \"rounded-lg\" : \"\"}`}\n        style={{ objectFit: \"contain\" }}\n      />\n      {mode === \"single\" ? <SingleUserAuth /> : <MultiUserAuth />}\n    </div>\n  );\n}\n\nexport function usePasswordModal(notry = false) {\n  const [auth, setAuth] = useState({\n    loading: true,\n    requiresAuth: false,\n    mode: \"single\",\n  });\n\n  useEffect(() => {\n    async function checkAuthReq() {\n      if (!window) return;\n\n      // If the last validity check is still valid\n      // we can skip the loading.\n      if (!System.needsAuthCheck() && notry === false) {\n        setAuth({\n          loading: false,\n          requiresAuth: false,\n          mode: \"multi\",\n        });\n        return;\n      }\n\n      const settings = await System.keys();\n      if (settings?.MultiUserMode) {\n        const currentToken = window.localStorage.getItem(AUTH_TOKEN);\n        if (!!currentToken) {\n          const valid = notry ? false : await System.checkAuth(currentToken);\n          if (!valid) {\n            setAuth({\n              loading: false,\n              requiresAuth: true,\n              mode: \"multi\",\n            });\n            window.localStorage.removeItem(AUTH_USER);\n            window.localStorage.removeItem(AUTH_TOKEN);\n            window.localStorage.removeItem(AUTH_TIMESTAMP);\n            return;\n          } else {\n            setAuth({\n              loading: false,\n              requiresAuth: false,\n              mode: \"multi\",\n            });\n            return;\n          }\n        } else {\n          setAuth({\n            loading: false,\n            requiresAuth: true,\n            mode: \"multi\",\n          });\n          return;\n        }\n      } else {\n        // Running token check in single user Auth mode.\n        // If Single user Auth is disabled - skip check\n        const requiresAuth = settings?.RequiresAuth || false;\n        if (!requiresAuth) {\n          setAuth({\n            loading: false,\n            requiresAuth: false,\n            mode: \"single\",\n          });\n          return;\n        }\n\n        const currentToken = window.localStorage.getItem(AUTH_TOKEN);\n        if (!!currentToken) {\n          const valid = notry ? false : await System.checkAuth(currentToken);\n          if (!valid) {\n            setAuth({\n              loading: false,\n              requiresAuth: true,\n              mode: \"single\",\n            });\n            window.localStorage.removeItem(AUTH_TOKEN);\n            window.localStorage.removeItem(AUTH_USER);\n            window.localStorage.removeItem(AUTH_TIMESTAMP);\n            return;\n          } else {\n            setAuth({\n              loading: false,\n              requiresAuth: false,\n              mode: \"single\",\n            });\n            return;\n          }\n        } else {\n          setAuth({\n            loading: false,\n            requiresAuth: true,\n            mode: \"single\",\n          });\n          return;\n        }\n      }\n    }\n    checkAuthReq();\n  }, []);\n\n  return auth;\n}\n"
  },
  {
    "path": "frontend/src/components/Preloader.jsx",
    "content": "export default function PreLoader({ size = \"16\" }) {\n  return (\n    <div\n      className={`h-${size} w-${size} animate-spin rounded-full border-4 border-solid border-primary border-t-transparent`}\n    ></div>\n  );\n}\n\nexport function FullScreenLoader() {\n  return (\n    <div\n      id=\"preloader\"\n      className=\"fixed left-0 top-0 z-999999 flex h-screen w-screen items-center justify-center bg-theme-bg-primary\"\n    >\n      <div className=\"h-16 w-16 animate-spin rounded-full border-4 border-solid border-[var(--theme-loader)] border-t-transparent\"></div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/PrivateRoute/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { Navigate } from \"react-router-dom\";\nimport { FullScreenLoader } from \"../Preloader\";\nimport validateSessionTokenForUser from \"@/utils/session\";\nimport paths from \"@/utils/paths\";\nimport { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from \"@/utils/constants\";\nimport { userFromStorage } from \"@/utils/request\";\nimport System from \"@/models/system\";\nimport UserMenu from \"../UserMenu\";\nimport { KeyboardShortcutWrapper } from \"@/utils/keyboardShortcuts\";\n\n// Used only for Multi-user mode only as we permission specific pages based on auth role.\n// When in single user mode we just bypass any authchecks.\nfunction useIsAuthenticated() {\n  const [isAuthd, setIsAuthed] = useState(null);\n  const [shouldRedirectToOnboarding, setShouldRedirectToOnboarding] =\n    useState(false);\n  const [multiUserMode, setMultiUserMode] = useState(false);\n\n  useEffect(() => {\n    const validateSession = async () => {\n      const onboardingComplete = await System.isOnboardingComplete();\n      const { MultiUserMode, RequiresAuth } = await System.keys();\n      setMultiUserMode(MultiUserMode);\n\n      // Check for the onboarding redirect condition\n      if (onboardingComplete === false) {\n        setShouldRedirectToOnboarding(true);\n        setIsAuthed(true);\n        return;\n      }\n\n      // Single User mode without password - no auth required\n      if (!MultiUserMode && !RequiresAuth) {\n        setIsAuthed(true);\n        return;\n      }\n\n      // Single User password mode check\n      if (!MultiUserMode && RequiresAuth) {\n        const localAuthToken = localStorage.getItem(AUTH_TOKEN);\n        if (!localAuthToken) {\n          setIsAuthed(false);\n          return;\n        }\n\n        const isValid = await validateSessionTokenForUser();\n        setIsAuthed(isValid);\n        return;\n      }\n\n      // Multi-user mode checks\n      const localUser = localStorage.getItem(AUTH_USER);\n      const localAuthToken = localStorage.getItem(AUTH_TOKEN);\n      if (!localUser || !localAuthToken) {\n        setIsAuthed(false);\n        return;\n      }\n\n      const isValid = await validateSessionTokenForUser();\n      if (!isValid) {\n        localStorage.removeItem(AUTH_USER);\n        localStorage.removeItem(AUTH_TOKEN);\n        localStorage.removeItem(AUTH_TIMESTAMP);\n        setIsAuthed(false);\n        return;\n      }\n\n      setIsAuthed(true);\n    };\n    validateSession();\n  }, []);\n\n  return { isAuthd, shouldRedirectToOnboarding, multiUserMode };\n}\n\n// Allows only admin to access the route and if in single user mode,\n// allows all users to access the route\nexport function AdminRoute({ Component, hideUserMenu = false }) {\n  const { isAuthd, shouldRedirectToOnboarding, multiUserMode } =\n    useIsAuthenticated();\n  if (isAuthd === null) return <FullScreenLoader />;\n\n  if (shouldRedirectToOnboarding) {\n    return <Navigate to={paths.onboarding.home()} />;\n  }\n\n  const user = userFromStorage();\n  return isAuthd && (user?.role === \"admin\" || !multiUserMode) ? (\n    hideUserMenu ? (\n      <KeyboardShortcutWrapper>\n        <Component />\n      </KeyboardShortcutWrapper>\n    ) : (\n      <KeyboardShortcutWrapper>\n        <UserMenu>\n          <Component />\n        </UserMenu>\n      </KeyboardShortcutWrapper>\n    )\n  ) : (\n    <Navigate to={paths.home()} />\n  );\n}\n\n// Allows manager and admin to access the route and if in single user mode,\n// allows all users to access the route\nexport function ManagerRoute({ Component }) {\n  const { isAuthd, shouldRedirectToOnboarding, multiUserMode } =\n    useIsAuthenticated();\n  if (isAuthd === null) return <FullScreenLoader />;\n\n  if (shouldRedirectToOnboarding) {\n    return <Navigate to={paths.onboarding.home()} />;\n  }\n\n  const user = userFromStorage();\n  return isAuthd && (user?.role !== \"default\" || !multiUserMode) ? (\n    <KeyboardShortcutWrapper>\n      <UserMenu>\n        <Component />\n      </UserMenu>\n    </KeyboardShortcutWrapper>\n  ) : (\n    <Navigate to={paths.home()} />\n  );\n}\n\nexport default function PrivateRoute({ Component }) {\n  const { isAuthd, shouldRedirectToOnboarding } = useIsAuthenticated();\n  if (isAuthd === null) return <FullScreenLoader />;\n\n  if (shouldRedirectToOnboarding) {\n    return <Navigate to=\"/onboarding\" />;\n  }\n\n  return isAuthd ? (\n    <KeyboardShortcutWrapper>\n      <UserMenu>\n        <Component />\n      </UserMenu>\n    </KeyboardShortcutWrapper>\n  ) : (\n    <Navigate to={paths.login(true)} />\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/ProviderPrivacy/constants.js",
    "content": "import AnythingLLMIcon from \"@/media/logo/anything-llm-icon.png\";\nimport OpenAiLogo from \"@/media/llmprovider/openai.png\";\nimport GenericOpenAiLogo from \"@/media/llmprovider/generic-openai.png\";\nimport AzureOpenAiLogo from \"@/media/llmprovider/azure.png\";\nimport AnthropicLogo from \"@/media/llmprovider/anthropic.png\";\nimport GeminiLogo from \"@/media/llmprovider/gemini.png\";\nimport OllamaLogo from \"@/media/llmprovider/ollama.png\";\nimport TogetherAILogo from \"@/media/llmprovider/togetherai.png\";\nimport FireworksAILogo from \"@/media/llmprovider/fireworksai.jpeg\";\nimport NvidiaNimLogo from \"@/media/llmprovider/nvidia-nim.png\";\nimport LMStudioLogo from \"@/media/llmprovider/lmstudio.png\";\nimport LocalAiLogo from \"@/media/llmprovider/localai.png\";\nimport MistralLogo from \"@/media/llmprovider/mistral.jpeg\";\nimport HuggingFaceLogo from \"@/media/llmprovider/huggingface.png\";\nimport PerplexityLogo from \"@/media/llmprovider/perplexity.png\";\nimport OpenRouterLogo from \"@/media/llmprovider/openrouter.jpeg\";\nimport NovitaLogo from \"@/media/llmprovider/novita.png\";\nimport GroqLogo from \"@/media/llmprovider/groq.png\";\nimport KoboldCPPLogo from \"@/media/llmprovider/koboldcpp.png\";\nimport TextGenWebUILogo from \"@/media/llmprovider/text-generation-webui.png\";\nimport LiteLLMLogo from \"@/media/llmprovider/litellm.png\";\nimport AWSBedrockLogo from \"@/media/llmprovider/bedrock.png\";\nimport DeepSeekLogo from \"@/media/llmprovider/deepseek.png\";\nimport APIPieLogo from \"@/media/llmprovider/apipie.png\";\nimport XAILogo from \"@/media/llmprovider/xai.png\";\nimport ZAiLogo from \"@/media/llmprovider/zai.png\";\nimport CohereLogo from \"@/media/llmprovider/cohere.png\";\nimport ZillizLogo from \"@/media/vectordbs/zilliz.png\";\nimport AstraDBLogo from \"@/media/vectordbs/astraDB.png\";\nimport ChromaLogo from \"@/media/vectordbs/chroma.png\";\nimport PineconeLogo from \"@/media/vectordbs/pinecone.png\";\nimport LanceDbLogo from \"@/media/vectordbs/lancedb.png\";\nimport WeaviateLogo from \"@/media/vectordbs/weaviate.png\";\nimport QDrantLogo from \"@/media/vectordbs/qdrant.png\";\nimport MilvusLogo from \"@/media/vectordbs/milvus.png\";\nimport VoyageAiLogo from \"@/media/embeddingprovider/voyageai.png\";\nimport PPIOLogo from \"@/media/llmprovider/ppio.png\";\nimport PGVectorLogo from \"@/media/vectordbs/pgvector.png\";\nimport DPAISLogo from \"@/media/llmprovider/dpais.png\";\nimport MoonshotAiLogo from \"@/media/llmprovider/moonshotai.png\";\nimport CometApiLogo from \"@/media/llmprovider/cometapi.png\";\nimport FoundryLogo from \"@/media/llmprovider/foundry-local.png\";\nimport GiteeAILogo from \"@/media/llmprovider/giteeai.png\";\nimport DockerModelRunnerLogo from \"@/media/llmprovider/docker-model-runner.png\";\nimport PrivateModeLogo from \"@/media/llmprovider/privatemode.png\";\nimport SambaNovaLogo from \"@/media/llmprovider/sambanova.png\";\nimport LemonadeLogo from \"@/media/llmprovider/lemonade.png\";\n\nconst LLM_PROVIDER_PRIVACY_MAP = {\n  openai: {\n    name: \"OpenAI\",\n    policyUrl: \"https://openai.com/policies/privacy-policy/\",\n    logo: OpenAiLogo,\n  },\n  azure: {\n    name: \"Azure OpenAI\",\n    policyUrl: \"https://privacy.microsoft.com/privacystatement\",\n    logo: AzureOpenAiLogo,\n  },\n  anthropic: {\n    name: \"Anthropic\",\n    policyUrl: \"https://www.anthropic.com/privacy\",\n    logo: AnthropicLogo,\n  },\n  gemini: {\n    name: \"Google Gemini\",\n    policyUrl: \"https://policies.google.com/privacy\",\n    logo: GeminiLogo,\n  },\n  \"nvidia-nim\": {\n    name: \"NVIDIA NIM\",\n    description: [\n      \"Your model and chats are only accessible on the machine running the NVIDIA NIM.\",\n    ],\n    logo: NvidiaNimLogo,\n  },\n  lmstudio: {\n    name: \"LMStudio\",\n    description: [\n      \"Your model and chats are only accessible on the server running LMStudio.\",\n    ],\n    logo: LMStudioLogo,\n  },\n  localai: {\n    name: \"LocalAI\",\n    description: [\n      \"Your model and chats are only accessible on the server running LocalAI.\",\n    ],\n    logo: LocalAiLogo,\n  },\n  ollama: {\n    name: \"Ollama\",\n    description: [\n      \"Your model and chats are only accessible on the machine running Ollama models.\",\n    ],\n    logo: OllamaLogo,\n  },\n  togetherai: {\n    name: \"TogetherAI\",\n    policyUrl: \"https://www.together.ai/privacy\",\n    logo: TogetherAILogo,\n  },\n  fireworksai: {\n    name: \"FireworksAI\",\n    policyUrl: \"https://fireworks.ai/privacy-policy\",\n    logo: FireworksAILogo,\n  },\n  mistral: {\n    name: \"Mistral\",\n    policyUrl: \"https://legal.mistral.ai/terms/privacy-policy\",\n    logo: MistralLogo,\n  },\n  huggingface: {\n    name: \"HuggingFace\",\n    policyUrl: \"https://huggingface.co/privacy\",\n    logo: HuggingFaceLogo,\n  },\n  perplexity: {\n    name: \"Perplexity AI\",\n    policyUrl: \"https://www.perplexity.ai/privacy\",\n    logo: PerplexityLogo,\n  },\n  openrouter: {\n    name: \"OpenRouter\",\n    policyUrl: \"https://openrouter.ai/privacy\",\n    logo: OpenRouterLogo,\n  },\n  novita: {\n    name: \"Novita AI\",\n    policyUrl: \"https://novita.ai/legal/privacy-policy\",\n    logo: NovitaLogo,\n  },\n  groq: {\n    name: \"Groq\",\n    policyUrl: \"https://groq.com/privacy-policy/\",\n    logo: GroqLogo,\n  },\n  koboldcpp: {\n    name: \"KoboldCPP\",\n    description: [\n      \"Your model and chats are only accessible on the server running KoboldCPP\",\n    ],\n    logo: KoboldCPPLogo,\n  },\n  textgenwebui: {\n    name: \"Oobabooga Web UI\",\n    description: [\n      \"Your model and chats are only accessible on the server running the Oobabooga Text Generation Web UI\",\n    ],\n    logo: TextGenWebUILogo,\n  },\n  \"generic-openai\": {\n    name: \"Generic OpenAI compatible service\",\n    description: [\n      \"Data is shared according to the terms of service applicable with your generic endpoint provider.\",\n    ],\n    logo: GenericOpenAiLogo,\n  },\n  cohere: {\n    name: \"Cohere\",\n    policyUrl: \"https://cohere.com/privacy\",\n    logo: CohereLogo,\n  },\n  litellm: {\n    name: \"LiteLLM\",\n    description: [\n      \"Your model and chats are only accessible on the server running LiteLLM\",\n    ],\n    logo: LiteLLMLogo,\n  },\n  bedrock: {\n    name: \"AWS Bedrock\",\n    policyUrl: \"https://aws.amazon.com/bedrock/security-compliance/\",\n    logo: AWSBedrockLogo,\n  },\n  deepseek: {\n    name: \"DeepSeek\",\n    policyUrl:\n      \"https://cdn.deepseek.com/policies/en-US/deepseek-privacy-policy.html\",\n    logo: DeepSeekLogo,\n  },\n  apipie: {\n    name: \"APIpie.AI\",\n    policyUrl: \"https://apipie.ai/docs/Terms/privacy\",\n    logo: APIPieLogo,\n  },\n  xai: {\n    name: \"xAI\",\n    policyUrl: \"https://x.ai/legal/privacy-policy\",\n    logo: XAILogo,\n  },\n  zai: {\n    name: \"Z.AI\",\n    policyUrl: \"https://docs.z.ai/legal-agreement/privacy-policy\",\n    logo: ZAiLogo,\n  },\n  ppio: {\n    name: \"PPIO\",\n    policyUrl: \"https://www.pipio.ai/privacy-policy\",\n    logo: PPIOLogo,\n  },\n  dpais: {\n    name: \"Dell Pro AI Studio\",\n    description: [\n      \"Your model and chat contents are only accessible on the computer running Dell Pro AI Studio.\",\n    ],\n    logo: DPAISLogo,\n  },\n  moonshotai: {\n    name: \"Moonshot AI\",\n    policyUrl: \"https://platform.moonshot.ai/docs/agreement/userprivacy\",\n    logo: MoonshotAiLogo,\n  },\n  cometapi: {\n    name: \"CometAPI\",\n    policyUrl: \"https://apidoc.cometapi.com/privacy-policy-873819m0\",\n    logo: CometApiLogo,\n  },\n  foundry: {\n    name: \"Microsoft Foundry Local\",\n    description: [\n      \"Your model and chats are only accessible on the machine running Foundry Local.\",\n    ],\n    logo: FoundryLogo,\n  },\n  giteeai: {\n    name: \"GiteeAI\",\n    policyUrl: \"https://ai.gitee.com/docs/appendix/privacy\",\n    logo: GiteeAILogo,\n  },\n  \"docker-model-runner\": {\n    name: \"Docker Model Runner\",\n    description: [\n      \"Your model and chats are only accessible on the machine running Docker Model Runner.\",\n    ],\n    logo: DockerModelRunnerLogo,\n  },\n  privatemode: {\n    name: \"Privatemode\",\n    policyUrl: \"https://docs.privatemode.ai/getting-started/faq#q2\",\n    logo: PrivateModeLogo,\n  },\n  sambanova: {\n    name: \"SambaNova\",\n    policyUrl: \"https://sambanova.ai/privacy-policy\",\n    logo: SambaNovaLogo,\n  },\n  lemonade: {\n    name: \"Lemonade\",\n    description: [\n      \"Your model and chats are only accessible on the machine running the Lemonade server.\",\n    ],\n    logo: LemonadeLogo,\n  },\n};\n\nconst VECTOR_DB_PROVIDER_PRIVACY_MAP = {\n  pgvector: {\n    name: \"PGVector\",\n    description: [\n      \"Your vectors and document text are stored on your PostgreSQL instance.\",\n      \"Access to your instance is managed by you.\",\n    ],\n    logo: PGVectorLogo,\n  },\n  chroma: {\n    name: \"Chroma\",\n    description: [\n      \"Your vectors and document text are stored on your Chroma instance.\",\n      \"Access to your instance is managed by you.\",\n    ],\n    logo: ChromaLogo,\n  },\n  chromacloud: {\n    name: \"Chroma Cloud\",\n    policyUrl: \"https://www.trychroma.com/privacy\",\n    logo: ChromaLogo,\n  },\n  pinecone: {\n    name: \"Pinecone\",\n    policyUrl: \"https://www.pinecone.io/privacy/\",\n    logo: PineconeLogo,\n  },\n  qdrant: {\n    name: \"Qdrant\",\n    policyUrl: \"https://qdrant.tech/legal/privacy-policy/\",\n    logo: QDrantLogo,\n  },\n  weaviate: {\n    name: \"Weaviate\",\n    policyUrl: \"https://weaviate.io/privacy\",\n    logo: WeaviateLogo,\n  },\n  milvus: {\n    name: \"Milvus\",\n    description: [\n      \"Your vectors and document text are stored on your Milvus instance (cloud or self-hosted).\",\n    ],\n    logo: MilvusLogo,\n  },\n  zilliz: {\n    name: \"Zilliz Cloud\",\n    policyUrl: \"https://zilliz.com/privacy-policy\",\n    logo: ZillizLogo,\n  },\n  astra: {\n    name: \"AstraDB\",\n    policyUrl: \"https://www.ibm.com/us-en/privacy\",\n    logo: AstraDBLogo,\n  },\n  lancedb: {\n    name: \"LanceDB\",\n    description: [\n      \"Your vectors and document text are stored privately on this instance of AnythingLLM.\",\n    ],\n    logo: LanceDbLogo,\n  },\n};\n\nconst EMBEDDING_ENGINE_PROVIDER_PRIVACY_MAP = {\n  native: {\n    name: \"AnythingLLM Embedder\",\n    description: [\n      \"Your document text is embedded privately on this instance of AnythingLLM.\",\n    ],\n    logo: AnythingLLMIcon,\n  },\n  openai: {\n    name: \"OpenAI\",\n    policyUrl: \"https://openai.com/policies/privacy-policy/\",\n    logo: OpenAiLogo,\n  },\n  azure: {\n    name: \"Azure OpenAI\",\n    policyUrl: \"https://privacy.microsoft.com/privacystatement\",\n    logo: AzureOpenAiLogo,\n  },\n  localai: {\n    name: \"LocalAI\",\n    description: [\n      \"Your document text is embedded privately on the server running LocalAI.\",\n    ],\n    logo: LocalAiLogo,\n  },\n  ollama: {\n    name: \"Ollama\",\n    description: [\n      \"Your document text is embedded privately on the server running Ollama.\",\n    ],\n    logo: OllamaLogo,\n  },\n  lmstudio: {\n    name: \"LMStudio\",\n    description: [\n      \"Your document text is embedded privately on the server running LMStudio.\",\n    ],\n    logo: LMStudioLogo,\n  },\n  openrouter: {\n    name: \"OpenRouter\",\n    policyUrl: \"https://openrouter.ai/privacy\",\n    logo: OpenRouterLogo,\n  },\n  cohere: {\n    name: \"Cohere\",\n    policyUrl: \"https://cohere.com/privacy\",\n    logo: CohereLogo,\n  },\n  voyageai: {\n    name: \"Voyage AI\",\n    policyUrl: \"https://www.voyageai.com/privacy\",\n    logo: VoyageAiLogo,\n  },\n  mistral: {\n    name: \"Mistral AI\",\n    policyUrl: \"https://legal.mistral.ai/terms/privacy-policy\",\n    logo: MistralLogo,\n  },\n  litellm: {\n    name: \"LiteLLM\",\n    description: [\n      \"Your document text is only accessible on the server running LiteLLM and to the providers you configured in LiteLLM.\",\n    ],\n    logo: LiteLLMLogo,\n  },\n  \"generic-openai\": {\n    name: \"Generic OpenAI compatible service\",\n    description: [\n      \"Data is shared according to the terms of service applicable with your generic endpoint provider.\",\n    ],\n    logo: GenericOpenAiLogo,\n  },\n  gemini: {\n    name: \"Google Gemini\",\n    policyUrl: \"https://policies.google.com/privacy\",\n    logo: GeminiLogo,\n  },\n  lemonade: {\n    name: \"Lemonade\",\n    description: [\n      \"Your document text is embedded privately on the machine running the Lemonade server.\",\n    ],\n    logo: LemonadeLogo,\n  },\n};\n\nexport const PROVIDER_PRIVACY_MAP = {\n  llm: LLM_PROVIDER_PRIVACY_MAP,\n  embeddingEngine: EMBEDDING_ENGINE_PROVIDER_PRIVACY_MAP,\n  vectorDb: VECTOR_DB_PROVIDER_PRIVACY_MAP,\n};\n"
  },
  {
    "path": "frontend/src/components/ProviderPrivacy/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\nimport { PROVIDER_PRIVACY_MAP } from \"./constants\";\nimport { ArrowSquareOut } from \"@phosphor-icons/react\";\nimport AnythingLLMIcon from \"@/media/logo/anything-llm-icon.png\";\nimport { Link } from \"react-router-dom\";\nimport { titleCase, sentenceCase } from \"text-case\";\n\nfunction defaultProvider(providerString) {\n  return {\n    name: providerString\n      ? titleCase(sentenceCase(String(providerString)))\n      : \"Unknown\",\n    description: [\n      `\"${providerString}\" has no known data handling policy defined in AnythingLLM.`,\n    ],\n    logo: AnythingLLMIcon,\n  };\n}\n\nexport default function ProviderPrivacy() {\n  const [loading, setLoading] = useState(true);\n  const [providers, setProviders] = useState({\n    llmProvider: null,\n    embeddingEngine: null,\n    vectorDb: null,\n  });\n\n  useEffect(() => {\n    async function fetchProviders() {\n      const _settings = await System.keys();\n      const providerDefinition =\n        PROVIDER_PRIVACY_MAP.llm[_settings?.LLMProvider] ||\n        defaultProvider(_settings?.LLMProvider);\n      const embeddingEngineDefinition =\n        PROVIDER_PRIVACY_MAP.embeddingEngine[_settings?.EmbeddingEngine] ||\n        defaultProvider(_settings?.EmbeddingEngine);\n      const vectorDbDefinition =\n        PROVIDER_PRIVACY_MAP.vectorDb[_settings?.VectorDB] ||\n        defaultProvider(_settings?.VectorDB);\n\n      setProviders({\n        llmProvider: providerDefinition,\n        embeddingEngine: embeddingEngineDefinition,\n        vectorDb: vectorDbDefinition,\n      });\n      setLoading(false);\n    }\n    fetchProviders();\n  }, []);\n\n  if (loading) return null;\n  return (\n    <div className=\"flex flex-col gap-8 w-full max-w-2xl\">\n      <ProviderPrivacyItem\n        title=\"LLM Provider\"\n        provider={providers.llmProvider}\n        altText=\"LLM Logo\"\n      />\n      <ProviderPrivacyItem\n        title=\"Embedding Preference\"\n        provider={providers.embeddingEngine}\n        altText=\"Embedding Logo\"\n      />\n      <ProviderPrivacyItem\n        title=\"Vector Database\"\n        provider={providers.vectorDb}\n        altText=\"Vector DB Logo\"\n      />\n    </div>\n  );\n}\n\nfunction ProviderPrivacyItem({ title, provider, altText }) {\n  return (\n    <div className=\"flex flex-col items-start gap-y-3 pb-4 border-b border-theme-sidebar-border\">\n      <div className=\"text-theme-text-primary text-base font-bold\">{title}</div>\n      <div className=\"flex items-start gap-3\">\n        <img\n          src={provider.logo}\n          alt={altText}\n          className=\"w-8 h-8 rounded flex-shrink-0 mt-0.5\"\n        />\n        <div className=\"flex flex-col gap-2 flex-1\">\n          <div className=\"flex items-center gap-2 flex-wrap\">\n            <span className=\"text-theme-text-primary text-sm font-semibold\">\n              {provider.name}\n            </span>\n          </div>\n          {provider.policyUrl ? (\n            <div className=\"text-theme-text-secondary text-sm\">\n              Your usage, chats, and data are subject to the service&apos;s{\" \"}\n              <Link\n                className=\"text-theme-text-secondary hover:text-theme-text-primary text-sm font-medium underline transition-colors inline-flex items-center gap-1\"\n                to={provider.policyUrl}\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                privacy policy\n                <ArrowSquareOut size={12} />\n              </Link>\n              .\n            </div>\n          ) : (\n            provider.description && (\n              <ul className=\"flex flex-col list-none gap-1\">\n                {provider.description.map((desc, idx) => (\n                  <li key={idx} className=\"text-theme-text-secondary text-sm\">\n                    {desc}\n                  </li>\n                ))}\n              </ul>\n            )\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/SettingsButton/index.jsx",
    "content": "import useUser from \"@/hooks/useUser\";\nimport paths from \"@/utils/paths\";\nimport { ArrowUUpLeft, Wrench } from \"@phosphor-icons/react\";\nimport { Link } from \"react-router-dom\";\nimport { useMatch } from \"react-router-dom\";\n\nexport default function SettingsButton() {\n  const isInSettings = !!useMatch(\"/settings/*\");\n  const { user } = useUser();\n\n  if (user && user?.role === \"default\") return null;\n\n  if (isInSettings)\n    return (\n      <div className=\"flex w-fit\">\n        <Link\n          to={paths.home()}\n          className=\"transition-all duration-300 p-2 rounded-full bg-theme-sidebar-footer-icon hover:bg-theme-sidebar-footer-icon-hover\"\n          aria-label=\"Home\"\n          data-tooltip-id=\"footer-item\"\n          data-tooltip-content=\"Back to workspaces\"\n        >\n          <ArrowUUpLeft\n            className=\"h-5 w-5 text-white light:text-slate-800\"\n            weight=\"fill\"\n          />\n        </Link>\n      </div>\n    );\n\n  return (\n    <div className=\"flex w-fit\">\n      <Link\n        to={paths.settings.interface()}\n        className=\"transition-all duration-300 p-2 rounded-full bg-theme-sidebar-footer-icon hover:bg-theme-sidebar-footer-icon-hover\"\n        aria-label=\"Settings\"\n        data-tooltip-id=\"footer-item\"\n        data-tooltip-content=\"Open settings\"\n      >\n        <Wrench\n          className=\"h-5 w-5 text-white light:text-slate-800\"\n          weight=\"fill\"\n        />\n      </Link>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/SettingsSidebar/MenuOption/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { CaretRight } from \"@phosphor-icons/react\";\nimport { Link, useLocation } from \"react-router-dom\";\nimport { safeJsonParse } from \"@/utils/request\";\nimport useScrollActiveItemIntoView from \"@/hooks/useScrollActiveItemIntoView\";\n\nexport default function MenuOption({\n  btnText,\n  icon,\n  href,\n  childOptions = [],\n  flex = false,\n  user = null,\n  roles = [],\n  hidden = false,\n  isChild = false,\n}) {\n  const storageKey = generateStorageKey({ key: btnText });\n  const location = useLocation();\n  const hasChildren = childOptions.length > 0;\n  const hasVisibleChildren = hasVisibleOptions(user, childOptions);\n  const { isExpanded, setIsExpanded } = useIsExpanded({\n    storageKey,\n    hasVisibleChildren,\n    childOptions,\n    location: location.pathname,\n  });\n\n  const isActive = hasChildren\n    ? (!isExpanded &&\n        childOptions.some((child) => child.href === location.pathname)) ||\n      location.pathname === href\n    : location.pathname === href;\n\n  const { ref } = useScrollActiveItemIntoView({\n    isActive,\n    behavior: \"instant\",\n    block: \"center\",\n  });\n\n  if (hidden) return null;\n\n  // If this option is a parent level option\n  if (!isChild) {\n    // and has no children then use its flex props and roles prop directly\n    if (!hasChildren) {\n      if (!flex && !roles.includes(user?.role)) return null;\n      if (flex && !!user && !roles.includes(user?.role)) return null;\n    }\n\n    // if has children and no visible children - remove it.\n    if (hasChildren && !hasVisibleChildren) return null;\n  } else {\n    // is a child so we use it's permissions\n    if (!flex && !roles.includes(user?.role)) return null;\n    if (flex && !!user && !roles.includes(user?.role)) return null;\n  }\n\n  const handleClick = (e) => {\n    if (hasChildren) {\n      e.preventDefault();\n      const newExpandedState = !isExpanded;\n      setIsExpanded(newExpandedState);\n      localStorage.setItem(storageKey, JSON.stringify(newExpandedState));\n    }\n  };\n\n  return (\n    <div>\n      <div\n        className={`\n          flex items-center justify-between w-full\n          transition-all duration-300\n          rounded-[6px]\n          ${\n            isActive\n              ? \"bg-theme-sidebar-subitem-selected font-medium border-outline\"\n              : \"hover:bg-theme-sidebar-subitem-hover\"\n          }\n        `}\n      >\n        <Link\n          ref={ref}\n          to={href}\n          className={`flex flex-grow items-center px-[12px] h-[32px] font-medium ${\n            isChild ? \"hover:text-white\" : \"text-white light:text-black\"\n          }`}\n          onClick={hasChildren ? handleClick : undefined}\n        >\n          {icon}\n          <p\n            className={`${\n              isChild ? \"text-xs\" : \"text-sm\"\n            } leading-loose whitespace-nowrap overflow-hidden ml-2 ${\n              isActive\n                ? \"text-white font-semibold\"\n                : \"text-white light:text-black\"\n            } ${!icon && \"pl-5\"}`}\n          >\n            {btnText}\n          </p>\n        </Link>\n        {hasChildren && (\n          <button onClick={handleClick} className=\"p-2 text-white\">\n            <CaretRight\n              size={16}\n              weight=\"bold\"\n              // color={isExpanded ? \"#000000\" : \"var(--theme-sidebar-subitem-icon)\"}\n              className={`transition-transform text-white light:text-black ${\n                isExpanded ? \"rotate-90\" : \"\"\n              }`}\n            />\n          </button>\n        )}\n      </div>\n      {isExpanded && hasChildren && (\n        <div className=\"mt-1 rounded-r-lg w-full\">\n          {childOptions.map((childOption, index) => (\n            <MenuOption\n              key={index}\n              {...childOption} // flex and roles go here.\n              user={user}\n              isChild={true}\n            />\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction useIsExpanded({\n  storageKey = \"\",\n  hasVisibleChildren = false,\n  childOptions = [],\n  location = null,\n}) {\n  const [isExpanded, setIsExpanded] = useState(() => {\n    if (hasVisibleChildren) {\n      const storedValue = localStorage.getItem(storageKey);\n      if (storedValue !== null) {\n        return safeJsonParse(storedValue, false);\n      }\n      return childOptions.some((child) => child.href === location);\n    }\n    return false;\n  });\n\n  useEffect(() => {\n    if (hasVisibleChildren) {\n      const shouldExpand = childOptions.some(\n        (child) => child.href === location\n      );\n      if (shouldExpand && !isExpanded) {\n        setIsExpanded(true);\n        localStorage.setItem(storageKey, JSON.stringify(true));\n      }\n    }\n  }, [location]);\n\n  return { isExpanded, setIsExpanded };\n}\n\n/**\n * Checks if the child options are visible to the user.\n * This hides the top level options if the child options are not visible\n * for either the users permissions or the child options hidden prop is set to true by other means.\n * If all child options return false for `isVisible` then the parent option will not be visible as well.\n * @param {object} user - The user object.\n * @param {array} childOptions - The child options.\n * @returns {boolean} - True if the child options are visible, false otherwise.\n */\nfunction hasVisibleOptions(user = null, childOptions = []) {\n  if (!Array.isArray(childOptions) || childOptions?.length === 0) return false;\n\n  function isVisible({\n    roles = [],\n    user = null,\n    flex = false,\n    hidden = false,\n  }) {\n    if (hidden) return false;\n    if (!flex && !roles.includes(user?.role)) return false;\n    if (flex && !!user && !roles.includes(user?.role)) return false;\n    return true;\n  }\n\n  return childOptions.some((opt) =>\n    isVisible({ roles: opt.roles, user, flex: opt.flex, hidden: opt.hidden })\n  );\n}\n\nfunction generateStorageKey({ key = \"\" }) {\n  const _key = key.replace(/\\s+/g, \"_\").toLowerCase();\n  return `anything_llm_menu_${_key}_expanded`;\n}\n"
  },
  {
    "path": "frontend/src/components/SettingsSidebar/index.jsx",
    "content": "import React, { useEffect, useRef, useState } from \"react\";\nimport paths from \"@/utils/paths\";\nimport useLogo from \"@/hooks/useLogo\";\nimport {\n  House,\n  List,\n  Flask,\n  Gear,\n  UserCircleGear,\n  PencilSimpleLine,\n  Nut,\n  Toolbox,\n} from \"@phosphor-icons/react\";\nimport AgentIcon from \"@/media/animations/agent-static.png\";\nimport CommunityHubIcon from \"@/media/illustrations/community-hub.png\";\nimport useUser from \"@/hooks/useUser\";\nimport { isMobile } from \"react-device-detect\";\nimport Footer from \"../Footer\";\nimport { Link } from \"react-router-dom\";\nimport { useTranslation } from \"react-i18next\";\nimport showToast from \"@/utils/toast\";\nimport System from \"@/models/system\";\nimport Option from \"./MenuOption\";\nimport { CanViewChatHistoryProvider } from \"../CanViewChatHistory\";\nimport useAppVersion from \"@/hooks/useAppVersion\";\n\nexport default function SettingsSidebar() {\n  const { t } = useTranslation();\n  const { logo } = useLogo();\n  const { user } = useUser();\n  const sidebarRef = useRef(null);\n  const [showSidebar, setShowSidebar] = useState(false);\n  const [showBgOverlay, setShowBgOverlay] = useState(false);\n\n  useEffect(() => {\n    function handleBg() {\n      if (showSidebar) {\n        setTimeout(() => {\n          setShowBgOverlay(true);\n        }, 300);\n      } else {\n        setShowBgOverlay(false);\n      }\n    }\n    handleBg();\n  }, [showSidebar]);\n\n  if (isMobile) {\n    return (\n      <>\n        <div className=\"fixed top-0 left-0 right-0 z-10 flex justify-between items-center px-4 py-2 bg-theme-bg-sidebar light:bg-white text-theme-text-secondary shadow-lg h-16\">\n          <button\n            onClick={() => setShowSidebar(true)}\n            className=\"rounded-md p-2 flex items-center justify-center text-theme-text-secondary\"\n          >\n            <List className=\"h-6 w-6\" />\n          </button>\n          <div className=\"flex items-center justify-center flex-grow\">\n            <img\n              src={logo}\n              alt=\"Logo\"\n              className=\"block mx-auto h-6 w-auto\"\n              style={{ maxHeight: \"40px\", objectFit: \"contain\" }}\n            />\n          </div>\n          <div className=\"w-12\"></div>\n        </div>\n        <div\n          style={{\n            transform: showSidebar ? `translateX(0vw)` : `translateX(-100vw)`,\n          }}\n          className={`z-99 fixed top-0 left-0 transition-all duration-500 w-[100vw] h-[100vh]`}\n        >\n          <div\n            className={`${\n              showBgOverlay\n                ? \"transition-all opacity-1\"\n                : \"transition-none opacity-0\"\n            }  duration-500 fixed top-0 left-0 bg-theme-bg-secondary bg-opacity-75 w-screen h-screen`}\n            onClick={() => setShowSidebar(false)}\n          />\n          <div\n            ref={sidebarRef}\n            className=\"h-[100vh] fixed top-0 left-0 rounded-r-[26px] bg-theme-bg-sidebar w-[80%] p-[18px]\"\n          >\n            <div className=\"w-full h-full flex flex-col overflow-x-hidden items-between\">\n              {/* Header Information */}\n              <div className=\"flex w-full items-center justify-between gap-x-4\">\n                <div className=\"flex shrink-1 w-fit items-center justify-start\">\n                  <img\n                    src={logo}\n                    alt=\"Logo\"\n                    className=\"rounded w-full max-h-[40px]\"\n                    style={{ objectFit: \"contain\" }}\n                  />\n                </div>\n                <div className=\"flex gap-x-2 items-center text-slate-500 shrink-0\">\n                  <a\n                    href={paths.home()}\n                    className=\"transition-all duration-300 p-2 rounded-full text-white bg-theme-action-menu-bg hover:bg-theme-action-menu-item-hover hover:border-slate-100 hover:border-opacity-50 border-transparent border\"\n                  >\n                    <House className=\"h-4 w-4\" />\n                  </a>\n                </div>\n              </div>\n\n              {/* Primary Body */}\n              <div className=\"h-full flex flex-col w-full justify-between pt-4 overflow-y-scroll no-scroll\">\n                <div className=\"h-auto md:sidebar-items\">\n                  <div className=\"flex flex-col gap-y-4 pb-[60px] overflow-y-scroll no-scroll\">\n                    <SidebarOptions user={user} t={t} />\n                    <div className=\"h-[1.5px] bg-[#3D4147] mx-3 mt-[14px]\" />\n                    <SupportEmail />\n                    <Link\n                      hidden={\n                        user?.hasOwnProperty(\"role\") && user.role !== \"admin\"\n                      }\n                      to={paths.settings.privacy()}\n                      className=\"text-theme-text-secondary hover:text-white text-xs leading-[18px] mx-3\"\n                    >\n                      {t(\"settings.privacy\")}\n                    </Link>\n                    <AppVersion />\n                  </div>\n                </div>\n              </div>\n              <div className=\"absolute bottom-2 left-0 right-0 pt-2 bg-theme-bg-sidebar bg-opacity-80 backdrop-filter backdrop-blur-md\">\n                <Footer />\n              </div>\n            </div>\n          </div>\n        </div>\n      </>\n    );\n  }\n\n  return (\n    <>\n      <div>\n        <Link\n          to={paths.home()}\n          className=\"flex shrink-0 max-w-[55%] items-center justify-start mx-[20.5px] my-[18px]\"\n        >\n          <img\n            src={logo}\n            alt=\"Logo\"\n            className=\"rounded max-h-[24px]\"\n            style={{ objectFit: \"contain\" }}\n          />\n        </Link>\n        <div\n          ref={sidebarRef}\n          className=\"transition-all duration-500 relative m-[16px] rounded-[16px] bg-theme-bg-sidebar border-[2px] border-theme-sidebar-border light:border-none min-w-[250px] p-[10px] h-[calc(100%-76px)]\"\n        >\n          <div className=\"w-full h-full flex flex-col overflow-x-hidden items-between min-w-[235px]\">\n            <div className=\"text-theme-text-secondary text-sm font-medium uppercase mt-[4px] mb-0 ml-2\">\n              {t(\"settings.title\")}\n            </div>\n            <div className=\"relative h-[calc(100%-60px)] flex flex-col w-full justify-between pt-[10px] overflow-y-scroll no-scroll\">\n              <div className=\"h-auto sidebar-items\">\n                <div className=\"flex flex-col gap-y-2 pb-[60px] overflow-y-scroll no-scroll\">\n                  <SidebarOptions user={user} t={t} />\n                  <div className=\"h-[1.5px] bg-[#3D4147] mx-3 mt-[14px]\" />\n                  <SupportEmail />\n                  <Link\n                    hidden={\n                      user?.hasOwnProperty(\"role\") && user.role !== \"admin\"\n                    }\n                    to={paths.settings.privacy()}\n                    className=\"text-theme-text-secondary hover:text-white hover:light:text-theme-text-primary text-xs leading-[18px] mx-3\"\n                  >\n                    {t(\"settings.privacy\")}\n                  </Link>\n                  <AppVersion />\n                </div>\n              </div>\n            </div>\n            <div className=\"absolute bottom-0 left-0 right-0 pt-4 pb-3 rounded-b-[16px] bg-theme-bg-sidebar bg-opacity-80 backdrop-filter backdrop-blur-md z-10\">\n              <Footer />\n            </div>\n          </div>\n        </div>\n      </div>\n    </>\n  );\n}\n\nfunction SupportEmail() {\n  const [supportEmail, setSupportEmail] = useState(paths.mailToMintplex());\n  const { t } = useTranslation();\n\n  useEffect(() => {\n    const fetchSupportEmail = async () => {\n      const supportEmail = await System.fetchSupportEmail();\n      setSupportEmail(\n        supportEmail?.email\n          ? `mailto:${supportEmail.email}`\n          : paths.mailToMintplex()\n      );\n    };\n    fetchSupportEmail();\n  }, []);\n\n  return (\n    <Link\n      to={supportEmail}\n      className=\"text-theme-text-secondary hover:text-white hover:light:text-theme-text-primary text-xs leading-[18px] mx-3 mt-1\"\n    >\n      {t(\"settings.contact\")}\n    </Link>\n  );\n}\n\nconst SidebarOptions = ({ user = null, t }) => (\n  <CanViewChatHistoryProvider>\n    {({ viewable: canViewChatHistory }) => (\n      <>\n        <Option\n          btnText={t(\"settings.ai-providers\")}\n          icon={<Gear className=\"h-5 w-5 flex-shrink-0\" />}\n          user={user}\n          childOptions={[\n            {\n              btnText: t(\"settings.llm\"),\n              href: paths.settings.llmPreference(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n            {\n              btnText: t(\"settings.vector-database\"),\n              href: paths.settings.vectorDatabase(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n            {\n              btnText: t(\"settings.embedder\"),\n              href: paths.settings.embedder.modelPreference(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n            {\n              btnText: t(\"settings.text-splitting\"),\n              href: paths.settings.embedder.chunkingPreference(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n            {\n              btnText: t(\"settings.voice-speech\"),\n              href: paths.settings.audioPreference(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n            {\n              btnText: t(\"settings.transcription\"),\n              href: paths.settings.transcriptionPreference(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n          ]}\n        />\n        <Option\n          btnText={t(\"settings.admin\")}\n          icon={<UserCircleGear className=\"h-5 w-5 flex-shrink-0\" />}\n          user={user}\n          childOptions={[\n            {\n              btnText: t(\"settings.users\"),\n              href: paths.settings.users(),\n              roles: [\"admin\", \"manager\"],\n            },\n            {\n              btnText: t(\"settings.workspaces\"),\n              href: paths.settings.workspaces(),\n              roles: [\"admin\", \"manager\"],\n            },\n            {\n              hidden: !canViewChatHistory,\n              btnText: t(\"settings.workspace-chats\"),\n              href: paths.settings.chats(),\n              flex: true,\n              roles: [\"admin\", \"manager\"],\n            },\n            {\n              btnText: t(\"settings.invites\"),\n              href: paths.settings.invites(),\n              roles: [\"admin\", \"manager\"],\n            },\n            {\n              btnText: \"Default System Prompt\",\n              href: paths.settings.defaultSystemPrompt(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n          ]}\n        />\n        <Option\n          btnText={t(\"settings.agent-skills\")}\n          icon={\n            <img\n              src={AgentIcon}\n              alt=\"Agent\"\n              className=\"h-5 w-5 flex-shrink-0 light:invert\"\n            />\n          }\n          href={paths.settings.agentSkills()}\n          user={user}\n          flex={true}\n          roles={[\"admin\"]}\n        />\n        <Option\n          btnText={t(\"settings.community-hub.title\")}\n          icon={\n            <img\n              src={CommunityHubIcon}\n              alt=\"Community Hub\"\n              className=\"h-5 w-5 flex-shrink-0 light:invert\"\n            />\n          }\n          user={user}\n          childOptions={[\n            {\n              btnText: t(\"settings.community-hub.trending\"),\n              href: paths.communityHub.trending(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n            {\n              btnText: t(\"settings.community-hub.your-account\"),\n              href: paths.communityHub.authentication(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n            {\n              btnText: t(\"settings.community-hub.import-item\"),\n              href: paths.communityHub.importItem(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n          ]}\n        />\n        <Option\n          btnText={t(\"settings.customization\")}\n          icon={<PencilSimpleLine className=\"h-5 w-5 flex-shrink-0\" />}\n          user={user}\n          childOptions={[\n            {\n              btnText: t(\"settings.interface\"),\n              href: paths.settings.interface(),\n              flex: true,\n              roles: [\"admin\", \"manager\"],\n            },\n            {\n              btnText: t(\"settings.branding\"),\n              href: paths.settings.branding(),\n              flex: true,\n              roles: [\"admin\", \"manager\"],\n            },\n            {\n              btnText: t(\"settings.chat\"),\n              href: paths.settings.chat(),\n              flex: true,\n              roles: [\"admin\", \"manager\"],\n            },\n          ]}\n        />\n        <Option\n          btnText={t(\"settings.tools\")}\n          icon={<Toolbox className=\"h-5 w-5 flex-shrink-0\" />}\n          user={user}\n          childOptions={[\n            {\n              hidden: !canViewChatHistory,\n              btnText: t(\"settings.embeds\"),\n              href: paths.settings.embedChatWidgets(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n            {\n              btnText: t(\"settings.event-logs\"),\n              href: paths.settings.logs(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n            {\n              btnText: t(\"settings.api-keys\"),\n              href: paths.settings.apiKeys(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n            {\n              btnText: t(\"settings.system-prompt-variables\"),\n              href: paths.settings.systemPromptVariables(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n            {\n              btnText: t(\"settings.browser-extension\"),\n              href: paths.settings.browserExtension(),\n              flex: true,\n              roles: [\"admin\", \"manager\"],\n            },\n            {\n              btnText: t(\"settings.mobile-app\"),\n              href: paths.settings.mobile(),\n              flex: true,\n              roles: [\"admin\"],\n            },\n          ]}\n        />\n        <Option\n          btnText={t(\"settings.security\")}\n          icon={<Nut className=\"h-5 w-5 flex-shrink-0\" />}\n          href={paths.settings.security()}\n          user={user}\n          flex={true}\n          roles={[\"admin\", \"manager\"]}\n          hidden={user?.role}\n        />\n        <HoldToReveal key=\"exp_features\">\n          <Option\n            btnText={t(\"settings.experimental-features\")}\n            icon={<Flask className=\"h-5 w-5 flex-shrink-0\" />}\n            href={paths.settings.experimental()}\n            user={user}\n            flex={true}\n            roles={[\"admin\"]}\n          />\n        </HoldToReveal>\n      </>\n    )}\n  </CanViewChatHistoryProvider>\n);\n\nfunction HoldToReveal({ children, holdForMs = 3_000 }) {\n  let timeout = null;\n  const [showing, setShowing] = useState(\n    window.localStorage.getItem(\n      \"anythingllm_experimental_feature_preview_unlocked\"\n    )\n  );\n\n  useEffect(() => {\n    const onPress = (e) => {\n      if (![\"Control\", \"Meta\"].includes(e.key) || timeout !== null) return;\n      timeout = setTimeout(() => {\n        setShowing(true);\n        // Setting toastId prevents hook spam from holding control too many times or the event not detaching\n        showToast(\"Experimental feature previews unlocked!\");\n        window.localStorage.setItem(\n          \"anythingllm_experimental_feature_preview_unlocked\",\n          \"enabled\"\n        );\n        window.removeEventListener(\"keypress\", onPress);\n        window.removeEventListener(\"keyup\", onRelease);\n        clearTimeout(timeout);\n      }, holdForMs);\n    };\n    const onRelease = (e) => {\n      if (![\"Control\", \"Meta\"].includes(e.key)) return;\n      if (showing) {\n        window.removeEventListener(\"keypress\", onPress);\n        window.removeEventListener(\"keyup\", onRelease);\n        clearTimeout(timeout);\n        return;\n      }\n      clearTimeout(timeout);\n    };\n\n    if (!showing) {\n      window.addEventListener(\"keydown\", onPress);\n      window.addEventListener(\"keyup\", onRelease);\n    }\n    return () => {\n      window.removeEventListener(\"keydown\", onPress);\n      window.removeEventListener(\"keyup\", onRelease);\n    };\n  }, []);\n\n  if (!showing) return null;\n  return children;\n}\n\nfunction AppVersion() {\n  const { version, isLoading } = useAppVersion();\n  if (isLoading) return null;\n  return (\n    <Link\n      to={`https://github.com/Mintplex-Labs/anything-llm/releases/tag/v${version}`}\n      target=\"_blank\"\n      rel=\"noreferrer\"\n      className=\"text-theme-text-secondary light:opacity-80 opacity-50 text-xs mx-3\"\n    >\n      v{version}\n    </Link>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/ThreadItem/index.jsx",
    "content": "import useScrollActiveItemIntoView from \"@/hooks/useScrollActiveItemIntoView\";\nimport Workspace from \"@/models/workspace\";\nimport paths from \"@/utils/paths\";\nimport showToast from \"@/utils/toast\";\nimport {\n  ArrowCounterClockwise,\n  DotsThree,\n  PencilSimple,\n  Trash,\n  X,\n} from \"@phosphor-icons/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useParams } from \"react-router-dom\";\n\nconst THREAD_CALLOUT_DETAIL_WIDTH = 26;\nexport default function ThreadItem({\n  idx,\n  activeIdx,\n  isActive,\n  workspace,\n  thread,\n  onRemove,\n  toggleMarkForDeletion,\n  hasNext,\n  ctrlPressed = false,\n}) {\n  const { slug: urlSlug, threadSlug = null } = useParams();\n  const workspaceSlug = workspace?.slug ?? urlSlug;\n  const optionsContainer = useRef(null);\n  const [showOptions, setShowOptions] = useState(false);\n  const linkTo = thread.virtual\n    ? \"/\"\n    : !thread.slug\n      ? paths.workspace.chat(workspaceSlug)\n      : paths.workspace.thread(workspaceSlug, thread.slug);\n\n  const { ref } = useScrollActiveItemIntoView({\n    isActive,\n    behavior: \"instant\",\n    block: \"center\",\n  });\n  return (\n    <div\n      className=\"w-full relative flex h-[38px] items-center border-none rounded-lg\"\n      role=\"listitem\"\n    >\n      {/* Curved line Element and leader if required */}\n      <div\n        style={{ width: THREAD_CALLOUT_DETAIL_WIDTH / 2 }}\n        className={`${\n          isActive\n            ? \"border-l-2 border-b-2 border-white light:border-blue-800 z-[2]\"\n            : \"border-l border-b border-zinc-500 light:border-slate-400 z-[1]\"\n        } h-[50%] absolute top-0 left-3 rounded-bl-lg`}\n      ></div>\n      {/* Downstroke border for next item */}\n      {hasNext && (\n        <div\n          style={{ width: THREAD_CALLOUT_DETAIL_WIDTH / 2 }}\n          className={`${\n            idx <= activeIdx && !isActive\n              ? \"border-l-2 border-white light:border-blue-800 z-[2]\"\n              : \"border-l border-zinc-500 light:border-slate-400 z-[1]\"\n          } h-[100%] absolute top-0 left-3`}\n        ></div>\n      )}\n\n      {/* Curved line inline placeholder for spacing - not visible */}\n      <div\n        style={{ width: THREAD_CALLOUT_DETAIL_WIDTH + 8 }}\n        className=\"h-full\"\n      />\n      <div\n        className={`flex w-full items-center justify-between pr-2 group relative ${isActive ? \"bg-[var(--theme-sidebar-thread-selected)] light:bg-blue-200\" : \"hover:bg-theme-sidebar-subitem-hover light:hover:bg-slate-300\"} rounded-[4px]`}\n      >\n        {thread.deleted ? (\n          <div className=\"w-full flex justify-between\">\n            <div className=\"w-full pl-2 py-1\">\n              <p\n                className={`text-left text-sm text-slate-400/50 light:text-slate-500 italic`}\n              >\n                deleted thread\n              </p>\n            </div>\n            {ctrlPressed && (\n              <button\n                type=\"button\"\n                className=\"border-none\"\n                onClick={() => toggleMarkForDeletion(thread.id)}\n              >\n                <ArrowCounterClockwise\n                  className=\"text-zinc-300 hover:text-white light:text-theme-text-secondary hover:light:text-theme-text-primary\"\n                  size={18}\n                />\n              </button>\n            )}\n          </div>\n        ) : (\n          <a\n            ref={ref}\n            href={\n              window.location.pathname === linkTo || ctrlPressed ? \"#\" : linkTo\n            }\n            data-tooltip-id=\"workspace-thread-name\"\n            data-tooltip-content={thread.name}\n            className=\"w-full pl-2 py-1 overflow-hidden\"\n            aria-current={isActive ? \"page\" : \"\"}\n          >\n            <p\n              className={`text-left text-sm truncate max-w-[150px] ${\n                isActive\n                  ? \"font-semibold text-theme-text-primary light:text-blue-900\"\n                  : \"text-theme-text-primary font-medium light:text-slate-800\"\n              }`}\n            >\n              {thread.name}\n            </p>\n          </a>\n        )}\n        {!!thread.slug && !thread.deleted && !thread.virtual && (\n          <div ref={optionsContainer} className=\"flex items-center\">\n            {\" \"}\n            {/* Added flex and items-center */}\n            {ctrlPressed ? (\n              <button\n                type=\"button\"\n                className=\"border-none\"\n                onClick={() => toggleMarkForDeletion(thread.id)}\n              >\n                <X\n                  className=\"text-zinc-300 light:text-theme-text-secondary hover:text-white hover:light:text-theme-text-primary\"\n                  weight=\"bold\"\n                  size={18}\n                />\n              </button>\n            ) : (\n              <div className=\"flex items-center w-fit group-hover:visible md:invisible gap-x-1\">\n                <button\n                  type=\"button\"\n                  className=\"border-none\"\n                  onClick={() => setShowOptions(!showOptions)}\n                  aria-label=\"Thread options\"\n                >\n                  <DotsThree\n                    className=\"text-slate-300 light:text-theme-text-secondary hover:text-white hover:light:text-theme-text-primary\"\n                    size={25}\n                  />\n                </button>\n              </div>\n            )}\n            {showOptions && (\n              <OptionsMenu\n                containerRef={optionsContainer}\n                workspace={workspace}\n                thread={thread}\n                onRemove={onRemove}\n                close={() => setShowOptions(false)}\n                currentThreadSlug={threadSlug}\n              />\n            )}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction OptionsMenu({\n  containerRef,\n  workspace,\n  thread,\n  onRemove,\n  close,\n  currentThreadSlug,\n}) {\n  const menuRef = useRef(null);\n\n  // Ref menu options\n  const outsideClick = (e) => {\n    if (!menuRef.current) return false;\n    if (\n      !menuRef.current?.contains(e.target) &&\n      !containerRef.current?.contains(e.target)\n    )\n      close();\n    return false;\n  };\n\n  const isEsc = (e) => {\n    if (e.key === \"Escape\" || e.key === \"Esc\") close();\n  };\n\n  function cleanupListeners() {\n    window.removeEventListener(\"click\", outsideClick);\n    window.removeEventListener(\"keyup\", isEsc);\n  }\n  // end Ref menu options\n\n  useEffect(() => {\n    function setListeners() {\n      if (!menuRef?.current || !containerRef.current) return false;\n      window.document.addEventListener(\"click\", outsideClick);\n      window.document.addEventListener(\"keyup\", isEsc);\n    }\n\n    setListeners();\n    return cleanupListeners;\n  }, [menuRef.current, containerRef.current]);\n\n  const renameThread = async () => {\n    const name = window\n      .prompt(\"What would you like to rename this thread to?\")\n      ?.trim();\n    if (!name || name.length === 0) {\n      close();\n      return;\n    }\n\n    const { message } = await Workspace.threads.update(\n      workspace.slug,\n      thread.slug,\n      { name }\n    );\n    if (!!message) {\n      showToast(`Thread could not be updated! ${message}`, \"error\", {\n        clear: true,\n      });\n      close();\n      return;\n    }\n\n    thread.name = name;\n    close();\n  };\n\n  const handleDelete = async () => {\n    if (\n      !window.confirm(\n        \"Are you sure you want to delete this thread? All of its chats will be deleted. You cannot undo this.\"\n      )\n    )\n      return;\n    const success = await Workspace.threads.delete(workspace.slug, thread.slug);\n    if (!success) {\n      showToast(\"Thread could not be deleted!\", \"error\", { clear: true });\n      return;\n    }\n    if (success) {\n      showToast(\"Thread deleted successfully!\", \"success\", { clear: true });\n      onRemove(thread.id);\n      // Redirect if deleting the active thread\n      if (currentThreadSlug === thread.slug) {\n        window.location.href = paths.workspace.chat(workspace.slug);\n      }\n      return;\n    }\n  };\n\n  return (\n    <div\n      ref={menuRef}\n      className=\"absolute w-fit z-[20] top-[25px] right-[10px] bg-zinc-900 light:bg-theme-bg-sidebar light:border-[1px] light:border-theme-sidebar-border rounded-lg p-1\"\n    >\n      <button\n        onClick={renameThread}\n        type=\"button\"\n        className=\"w-full rounded-md flex items-center p-2 gap-x-2 hover:bg-slate-500/20 text-slate-300 light:text-theme-text-primary\"\n      >\n        <PencilSimple size={18} />\n        <p className=\"text-sm\">Rename</p>\n      </button>\n      <button\n        onClick={handleDelete}\n        type=\"button\"\n        className=\"w-full rounded-md flex items-center p-2 gap-x-2 hover:bg-red-500/20 text-slate-300 light:text-theme-text-primary hover:text-red-100\"\n      >\n        <Trash size={18} />\n        <p className=\"text-sm\">Delete Thread</p>\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Sidebar/ActiveWorkspaces/ThreadContainer/index.jsx",
    "content": "import Workspace from \"@/models/workspace\";\nimport paths from \"@/utils/paths\";\nimport showToast from \"@/utils/toast\";\nimport { Plus, CircleNotch, Trash } from \"@phosphor-icons/react\";\nimport { useEffect, useState } from \"react\";\nimport ThreadItem from \"./ThreadItem\";\nimport { useParams } from \"react-router-dom\";\nexport const THREAD_RENAME_EVENT = \"renameThread\";\n\nexport default function ThreadContainer({\n  workspace,\n  isVirtualThread = false,\n}) {\n  const { threadSlug = null } = useParams();\n  const [threads, setThreads] = useState([]);\n  const [loading, setLoading] = useState(true);\n  const [ctrlPressed, setCtrlPressed] = useState(false);\n\n  useEffect(() => {\n    const chatHandler = (event) => {\n      const { threadSlug, newName } = event.detail;\n      setThreads((prevThreads) =>\n        prevThreads.map((thread) => {\n          if (thread.slug === threadSlug) {\n            return { ...thread, name: newName };\n          }\n          return thread;\n        })\n      );\n    };\n\n    window.addEventListener(THREAD_RENAME_EVENT, chatHandler);\n\n    return () => {\n      window.removeEventListener(THREAD_RENAME_EVENT, chatHandler);\n    };\n  }, []);\n\n  useEffect(() => {\n    async function fetchThreads() {\n      if (!workspace.slug) return;\n      const { threads } = await Workspace.threads.all(workspace.slug);\n      setLoading(false);\n      setThreads(threads);\n    }\n    fetchThreads();\n  }, [workspace.slug]);\n\n  // Enable toggling of bulk-deletion by holding meta-key (ctrl on win and cmd/fn on others)\n  useEffect(() => {\n    const handleKeyDown = (event) => {\n      if ([\"Control\", \"Meta\"].includes(event.key)) {\n        setCtrlPressed(true);\n      }\n    };\n\n    const handleKeyUp = (event) => {\n      if ([\"Control\", \"Meta\"].includes(event.key)) {\n        setCtrlPressed(false);\n        // when toggling, unset bulk progress so\n        // previously marked threads that were never deleted\n        // come back to life.\n        setThreads((prev) =>\n          prev.map((t) => {\n            return { ...t, deleted: false };\n          })\n        );\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    window.addEventListener(\"keyup\", handleKeyUp);\n\n    return () => {\n      window.removeEventListener(\"keydown\", handleKeyDown);\n      window.removeEventListener(\"keyup\", handleKeyUp);\n    };\n  }, []);\n\n  const toggleForDeletion = (id) => {\n    setThreads((prev) =>\n      prev.map((t) => {\n        if (t.id !== id) return t;\n        return { ...t, deleted: !t.deleted };\n      })\n    );\n  };\n\n  const handleDeleteAll = async () => {\n    const slugs = threads.filter((t) => t.deleted === true).map((t) => t.slug);\n    await Workspace.threads.deleteBulk(workspace.slug, slugs);\n    setThreads((prev) => prev.filter((t) => !t.deleted));\n\n    // Only redirect if current thread is being deleted\n    if (slugs.includes(threadSlug)) {\n      window.location.href = paths.workspace.chat(workspace.slug);\n    }\n  };\n\n  function removeThread(threadId) {\n    setThreads((prev) =>\n      prev.map((_t) => {\n        if (_t.id !== threadId) return _t;\n        return { ..._t, deleted: true };\n      })\n    );\n\n    // Show thread was deleted, but then remove from threads entirely so it will\n    // not appear in bulk-selection.\n    setTimeout(() => {\n      setThreads((prev) => prev.filter((t) => !t.deleted));\n    }, 500);\n  }\n\n  function getActiveThreadIdx() {\n    if (isVirtualThread) return threads.length + 1;\n    const idx = threads.findIndex((t) => t?.slug === threadSlug);\n    return idx >= 0 ? idx + 1 : 0;\n  }\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col bg-pulse w-full h-10 items-center justify-center\">\n        <p className=\"text-xs text-white animate-pulse\">loading threads....</p>\n      </div>\n    );\n  }\n\n  const activeThreadIdx = getActiveThreadIdx();\n\n  return (\n    <div className=\"flex flex-col\" role=\"list\" aria-label=\"Threads\">\n      <ThreadItem\n        idx={0}\n        activeIdx={activeThreadIdx}\n        isActive={activeThreadIdx === 0}\n        workspace={workspace}\n        thread={{ slug: null, name: \"default\" }}\n        hasNext={threads.length > 0 || isVirtualThread}\n      />\n      {threads.map((thread, i) => (\n        <ThreadItem\n          key={thread.slug}\n          idx={i + 1}\n          ctrlPressed={ctrlPressed}\n          toggleMarkForDeletion={toggleForDeletion}\n          activeIdx={activeThreadIdx}\n          isActive={activeThreadIdx === i + 1}\n          workspace={workspace}\n          onRemove={removeThread}\n          thread={thread}\n          hasNext={i !== threads.length - 1 || isVirtualThread}\n        />\n      ))}\n      {isVirtualThread && (\n        <ThreadItem\n          idx={activeThreadIdx}\n          activeIdx={activeThreadIdx}\n          isActive={true}\n          workspace={workspace}\n          thread={{ slug: null, name: \"*New Thread\", virtual: true }}\n          hasNext={false}\n        />\n      )}\n      <DeleteAllThreadButton\n        ctrlPressed={ctrlPressed}\n        threads={threads}\n        onDelete={handleDeleteAll}\n      />\n      <NewThreadButton workspace={workspace} />\n    </div>\n  );\n}\n\nfunction NewThreadButton({ workspace }) {\n  const [loading, setLoading] = useState(false);\n  const onClick = async () => {\n    setLoading(true);\n    const { thread, error } = await Workspace.threads.new(workspace.slug);\n    if (!!error) {\n      showToast(`Could not create thread - ${error}`, \"error\", { clear: true });\n      setLoading(false);\n      return;\n    }\n    window.location.replace(\n      paths.workspace.thread(workspace.slug, thread.slug)\n    );\n  };\n\n  return (\n    <button\n      onClick={onClick}\n      className=\"w-full relative flex h-[40px] items-center border-none hover:bg-[var(--theme-sidebar-thread-selected)] light:hover:bg-slate-300 hover:light:bg-theme-sidebar-subitem-hover rounded-lg\"\n    >\n      <div className=\"flex w-full gap-x-2 items-center pl-4\">\n        <div className=\"bg-zinc-800 light:bg-slate-50 p-2 rounded-lg h-[24px] w-[24px] flex items-center justify-center\">\n          {loading ? (\n            <CircleNotch\n              weight=\"bold\"\n              size={14}\n              className=\"shrink-0 animate-spin text-white light:text-theme-text-primary\"\n            />\n          ) : (\n            <Plus\n              weight=\"bold\"\n              size={14}\n              className=\"shrink-0 text-white light:text-theme-text-primary\"\n            />\n          )}\n        </div>\n\n        {loading ? (\n          <p className=\"text-left text-white light:text-theme-text-primary text-sm\">\n            Starting Thread...\n          </p>\n        ) : (\n          <p className=\"text-left text-white light:text-theme-text-primary text-sm font-semibold\">\n            New Thread\n          </p>\n        )}\n      </div>\n    </button>\n  );\n}\n\nfunction DeleteAllThreadButton({ ctrlPressed, threads, onDelete }) {\n  if (!ctrlPressed || threads.filter((t) => t.deleted).length === 0)\n    return null;\n  return (\n    <button\n      type=\"button\"\n      onClick={onDelete}\n      className=\"w-full relative flex h-[40px] items-center border-none hover:bg-red-400/20 rounded-lg group\"\n    >\n      <div className=\"flex w-full gap-x-2 items-center pl-4\">\n        <div className=\"bg-transparent p-2 rounded-lg h-[24px] w-[24px] flex items-center justify-center\">\n          <Trash\n            weight=\"bold\"\n            size={14}\n            className=\"shrink-0 text-white light:text-red-500/50 group-hover:text-red-400\"\n          />\n        </div>\n        <p className=\"text-white light:text-theme-text-secondary text-left text-sm group-hover:text-red-400\">\n          Delete Selected\n        </p>\n      </div>\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Sidebar/ActiveWorkspaces/index.jsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport Workspace from \"@/models/workspace\";\nimport ManageWorkspace, {\n  useManageWorkspaceModal,\n} from \"../../Modals/ManageWorkspace\";\nimport paths from \"@/utils/paths\";\nimport { useParams, useNavigate, useMatch } from \"react-router-dom\";\nimport { GearSix, UploadSimple, DotsSixVertical } from \"@phosphor-icons/react\";\nimport useUser from \"@/hooks/useUser\";\nimport ThreadContainer from \"./ThreadContainer\";\nimport { DragDropContext, Droppable, Draggable } from \"react-beautiful-dnd\";\nimport showToast from \"@/utils/toast\";\nimport { LAST_VISITED_WORKSPACE } from \"@/utils/constants\";\nimport { safeJsonParse } from \"@/utils/request\";\n\nexport default function ActiveWorkspaces() {\n  const navigate = useNavigate();\n  const { slug } = useParams();\n  const [loading, setLoading] = useState(true);\n  const [workspaces, setWorkspaces] = useState([]);\n  const [selectedWs, setSelectedWs] = useState(null);\n  const { showing, showModal, hideModal } = useManageWorkspaceModal();\n  const { user } = useUser();\n  const isInWorkspaceSettings = !!useMatch(\"/workspace/:slug/settings/:tab\");\n  const isHomePage = !!useMatch(\"/\");\n\n  useEffect(() => {\n    async function getWorkspaces() {\n      const workspaces = await Workspace.all();\n      setLoading(false);\n      setWorkspaces(Workspace.orderWorkspaces(workspaces));\n    }\n    getWorkspaces();\n  }, []);\n\n  if (loading) {\n    return (\n      <Skeleton.default\n        height={40}\n        width=\"100%\"\n        count={5}\n        baseColor=\"var(--theme-sidebar-item-default)\"\n        highlightColor=\"var(--theme-sidebar-item-hover)\"\n        enableAnimation={true}\n        className=\"my-1\"\n      />\n    );\n  }\n\n  /**\n   * Reorders workspaces in the UI via localstorage on client side.\n   * @param {number} startIndex - the index of the workspace to move\n   * @param {number} endIndex - the index to move the workspace to\n   */\n  function reorderWorkspaces(startIndex, endIndex) {\n    const reorderedWorkspaces = Array.from(workspaces);\n    const [removed] = reorderedWorkspaces.splice(startIndex, 1);\n    reorderedWorkspaces.splice(endIndex, 0, removed);\n    setWorkspaces(reorderedWorkspaces);\n    const success = Workspace.storeWorkspaceOrder(\n      reorderedWorkspaces.map((w) => w.id)\n    );\n    if (!success) {\n      showToast(\"Failed to reorder workspaces\", \"error\");\n      Workspace.all().then((workspaces) => setWorkspaces(workspaces));\n    }\n  }\n\n  const onDragEnd = (result) => {\n    if (!result.destination) return;\n    reorderWorkspaces(result.source.index, result.destination.index);\n  };\n\n  // When on the home page, resolve which workspace should be virtually active\n  const virtualActiveSlug = (() => {\n    if (!isHomePage || workspaces.length === 0) return null;\n    const lastVisited = safeJsonParse(\n      localStorage.getItem(LAST_VISITED_WORKSPACE)\n    );\n    if (\n      lastVisited?.slug &&\n      workspaces.some((ws) => ws.slug === lastVisited.slug)\n    )\n      return lastVisited.slug;\n    return workspaces[0]?.slug ?? null;\n  })();\n\n  return (\n    <DragDropContext onDragEnd={onDragEnd}>\n      <Droppable droppableId=\"workspaces\">\n        {(provided) => (\n          <div\n            role=\"list\"\n            aria-label=\"Workspaces\"\n            className=\"flex flex-col gap-y-2\"\n            ref={provided.innerRef}\n            {...provided.droppableProps}\n          >\n            {workspaces.map((workspace, index) => {\n              const isVirtuallyActive = workspace.slug === virtualActiveSlug;\n              const isActive = workspace.slug === slug || isVirtuallyActive;\n              return (\n                <Draggable\n                  key={workspace.id}\n                  draggableId={workspace.id.toString()}\n                  index={index}\n                >\n                  {(provided, snapshot) => (\n                    <div\n                      ref={provided.innerRef}\n                      {...provided.draggableProps}\n                      className={`flex flex-col w-full group ${\n                        snapshot.isDragging ? \"opacity-50\" : \"\"\n                      }`}\n                      role=\"listitem\"\n                    >\n                      <div className=\"flex gap-x-2 items-center justify-between\">\n                        <a\n                          href={\n                            isActive\n                              ? null\n                              : paths.workspace.chat(workspace.slug)\n                          }\n                          data-tooltip-id=\"workspace-name\"\n                          data-tooltip-content={workspace.name}\n                          aria-current={isActive ? \"page\" : \"\"}\n                          className={`\n                            transition-all duration-[200ms]\n                            flex flex-grow w-[75%] gap-x-2 py-[6px] pl-[4px] pr-[6px] rounded-[4px] text-white justify-start items-center\n                            bg-theme-sidebar-item-default\n                            ${isActive ? \"light:bg-blue-200 font-bold\" : \"hover:bg-theme-sidebar-subitem-hover light:hover:bg-slate-300\"}\n                          `}\n                        >\n                          <div className=\"flex flex-row justify-between w-full items-center\">\n                            <div\n                              {...provided.dragHandleProps}\n                              className=\"cursor-grab mr-[3px]\"\n                            >\n                              <DotsSixVertical\n                                size={20}\n                                className={`${isActive ? \"text-white light:text-blue-800\" : \"\"}`}\n                                weight=\"bold\"\n                              />\n                            </div>\n                            <div className=\"flex items-center space-x-2 overflow-hidden flex-grow\">\n                              <div className=\"w-[130px] overflow-hidden\">\n                                <p\n                                  className={`\n                                  text-[14px] leading-loose whitespace-nowrap overflow-hidden\n                                  ${isActive ? \"font-bold text-white light:text-blue-900\" : \"font-medium \"} truncate\n                                  w-full group-hover:w-[130px] group-hover:duration-200\n                                `}\n                                >\n                                  {workspace.name}\n                                </p>\n                              </div>\n                            </div>\n                            {user?.role !== \"default\" && (\n                              <div\n                                className={`flex items-center gap-x-[2px] transition-opacity duration-200 ${isActive ? \"opacity-100\" : \"opacity-0 group-hover:opacity-100\"}`}\n                              >\n                                <button\n                                  type=\"button\"\n                                  onClick={(e) => {\n                                    e.preventDefault();\n                                    setSelectedWs(workspace);\n                                    showModal();\n                                  }}\n                                  className={`group/upload border-none rounded-md flex items-center justify-center ml-auto p-[2px] ${isActive ? \"hover:bg-zinc-500 light:hover:bg-sky-800/30\" : \"hover:bg-zinc-500 light:hover:bg-slate-400\"}`}\n                                >\n                                  <UploadSimple\n                                    className={`h-[20px] w-[20px] ${isActive ? \"text-zinc-400 hover:text-white light:text-blue-700 light:group-hover/upload:text-blue-900\" : \"text-zinc-400 hover:text-white light:text-slate-600 light:group-hover/upload:text-slate-950\"}`}\n                                  />\n                                </button>\n                                <button\n                                  onClick={(e) => {\n                                    e.preventDefault();\n                                    e.stopPropagation();\n                                    navigate(\n                                      isInWorkspaceSettings\n                                        ? paths.workspace.chat(workspace.slug)\n                                        : paths.workspace.settings.generalAppearance(\n                                            workspace.slug\n                                          )\n                                    );\n                                  }}\n                                  className={`group/gear rounded-md flex items-center justify-center ml-auto p-[2px] ${isActive ? \"hover:bg-zinc-500 light:hover:bg-sky-800/30\" : \"hover:bg-zinc-500 light:hover:bg-slate-400\"}`}\n                                  aria-label=\"General appearance settings\"\n                                >\n                                  <GearSix\n                                    color={\n                                      isInWorkspaceSettings &&\n                                      workspace.slug === slug\n                                        ? \"#46C8FF\"\n                                        : undefined\n                                    }\n                                    className={`h-[20px] w-[20px] ${isActive ? \"text-zinc-400 hover:text-white light:text-blue-700 light:group-hover/gear:text-blue-900\" : \"text-zinc-400 hover:text-white light:text-slate-600 light:group-hover/gear:text-slate-950\"}`}\n                                  />\n                                </button>\n                              </div>\n                            )}\n                          </div>\n                        </a>\n                      </div>\n                      {isActive && (\n                        <ThreadContainer\n                          workspace={workspace}\n                          isActive={isActive}\n                          isVirtualThread={isVirtuallyActive}\n                        />\n                      )}\n                    </div>\n                  )}\n                </Draggable>\n              );\n            })}\n            {provided.placeholder}\n            {showing && (\n              <ManageWorkspace\n                hideModal={hideModal}\n                providedSlug={selectedWs ? selectedWs.slug : null}\n              />\n            )}\n          </div>\n        )}\n      </Droppable>\n    </DragDropContext>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Sidebar/SearchBox/index.jsx",
    "content": "import { useState, useEffect, useRef } from \"react\";\nimport { Plus, MagnifyingGlass } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Link } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\nimport Preloader from \"@/components/Preloader\";\nimport debounce from \"lodash.debounce\";\nimport Workspace from \"@/models/workspace\";\nimport { Tooltip } from \"react-tooltip\";\n\nconst DEFAULT_SEARCH_RESULTS = {\n  workspaces: [],\n  threads: [],\n};\n\nconst SEARCH_RESULT_SELECTED = \"search-result-selected\";\nexport default function SearchBox({ user, showNewWsModal }) {\n  const { t } = useTranslation();\n  const searchRef = useRef(null);\n  const [searchTerm, setSearchTerm] = useState(\"\");\n  const [loading, setLoading] = useState(false);\n  const [searchResults, setSearchResults] = useState(DEFAULT_SEARCH_RESULTS);\n  const handleSearch = debounce(handleSearchDebounced, 500);\n\n  async function handleSearchDebounced(e) {\n    try {\n      const searchValue = e.target.value;\n      setSearchTerm(searchValue);\n      setLoading(true);\n      const searchResults =\n        await Workspace.searchWorkspaceOrThread(searchValue);\n      setSearchResults(searchResults);\n    } catch (error) {\n      console.error(error);\n      setSearchResults(DEFAULT_SEARCH_RESULTS);\n    } finally {\n      setLoading(false);\n    }\n  }\n\n  function handleReset() {\n    searchRef.current.value = \"\";\n    setSearchTerm(\"\");\n    setLoading(false);\n    setSearchResults(DEFAULT_SEARCH_RESULTS);\n  }\n\n  useEffect(() => {\n    window.addEventListener(SEARCH_RESULT_SELECTED, handleReset);\n    return () =>\n      window.removeEventListener(SEARCH_RESULT_SELECTED, handleReset);\n  }, []);\n\n  return (\n    <div className=\"flex gap-x-[5px] w-full items-center h-[32px]\">\n      <div className=\"relative h-full w-full flex\">\n        <input\n          ref={searchRef}\n          type=\"search\"\n          placeholder={t(\"common.search\")}\n          onChange={handleSearch}\n          onReset={handleReset}\n          onFocus={(e) => e.target.select()}\n          className=\"border-none w-full h-full rounded-lg bg-theme-sidebar-item-default pl-9 focus:pl-4 pr-1 placeholder:text-white/50 light:placeholder:text-slate-500 placeholder:font-semibold outline-none text-theme-text-primary search-input peer text-sm\"\n        />\n        <MagnifyingGlass\n          size={14}\n          className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-theme-settings-input-placeholder peer-focus:invisible\"\n          weight=\"bold\"\n          hidden={!!searchTerm}\n        />\n      </div>\n      <ShortWidthNewWorkspaceButton\n        user={user}\n        showNewWsModal={showNewWsModal}\n      />\n      <SearchResults\n        searchResults={searchResults}\n        searchTerm={searchTerm}\n        loading={loading}\n      />\n    </div>\n  );\n}\n\nfunction SearchResultWrapper({ children }) {\n  return (\n    <div className=\"absolute right-0 top-[6.2%] w-full flex flex-col gap-y-[24px] h-auto bg-theme-modal-border light:bg-theme-bg-primary light:border-2 light:border-theme-modal-border rounded-lg p-[16px] z-10 max-h-[calc(100%-24px)] overflow-y-scroll no-scroll\">\n      {children}\n    </div>\n  );\n}\n\nfunction SearchResults({ searchResults, searchTerm, loading }) {\n  if (!searchTerm || searchTerm.length < 3) return null;\n  if (loading)\n    return (\n      <SearchResultWrapper>\n        <div className=\"flex flex-col gap-y-[8px] h-[200px] justify-center items-center\">\n          <Preloader size={5} />\n          <p className=\"text-theme-text-secondary text-xs font-semibold text-center\">\n            Searching for \"{searchTerm}\"\n          </p>\n        </div>\n      </SearchResultWrapper>\n    );\n\n  if (\n    searchResults.workspaces.length === 0 &&\n    searchResults.threads.length === 0\n  ) {\n    return (\n      <SearchResultWrapper>\n        <div className=\"flex flex-col gap-y-[8px] h-[200px] justify-center items-center\">\n          <p className=\"text-theme-text-secondary text-xs font-semibold text-center\">\n            No results found for\n            <br />\n            <span className=\"text-theme-text-primary font-semibold text-sm\">\n              \"{searchTerm}\"\n            </span>\n          </p>\n        </div>\n      </SearchResultWrapper>\n    );\n  }\n\n  return (\n    <SearchResultWrapper>\n      <SearchResultCategory\n        name=\"Workspaces\"\n        items={searchResults.workspaces?.map((workspace) => ({\n          id: workspace.slug,\n          to: paths.workspace.chat(workspace.slug),\n          name: workspace.name,\n        }))}\n      />\n      <SearchResultCategory\n        name=\"Threads\"\n        items={searchResults.threads?.map((thread) => ({\n          id: thread.slug,\n          to: paths.workspace.thread(thread.workspace.slug, thread.slug),\n          name: thread.name,\n          hint: thread.workspace.name,\n        }))}\n      />\n    </SearchResultWrapper>\n  );\n}\n\nfunction SearchResultCategory({ items, name }) {\n  if (!items?.length) return null;\n  return (\n    <div className=\"flex flex-col gap-y-[8px]\">\n      <p className=\"text-theme-text-secondary text-xs uppercase font-semibold px-[4px]\">\n        {name}\n      </p>\n      <div className=\"flex flex-col gap-y-[6px]\">\n        {items.map((item) => (\n          <SearchResultItem\n            key={item.id}\n            to={item.to}\n            name={item.name}\n            hint={item.hint}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction SearchResultItem({ to, name, hint }) {\n  return (\n    <Link\n      to={to}\n      reloadDocument={true}\n      onClick={() => window.dispatchEvent(new Event(SEARCH_RESULT_SELECTED))}\n      className=\"hover:bg-[#FFF]/10 light:hover:bg-[#000]/10 transition-all duration-300 rounded-sm px-[8px] py-[2px]\"\n    >\n      <p className=\"text-theme-text-primary text-sm truncate w-[80%]\">\n        {name}\n        {hint && (\n          <span className=\"text-theme-text-secondary text-xs ml-[4px]\">\n            | {hint}\n          </span>\n        )}\n      </p>\n    </Link>\n  );\n}\n\nfunction ShortWidthNewWorkspaceButton({ user, showNewWsModal }) {\n  const { t } = useTranslation();\n  if (!!user && user?.role === \"default\") return null;\n\n  return (\n    <>\n      <button\n        data-tooltip-id=\"new-workspace-tooltip\"\n        data-tooltip-content={t(\"new-workspace.title\")}\n        onClick={showNewWsModal}\n        className=\"border-none flex items-center justify-center bg-white  rounded-lg p-[8px] hover:bg-white/80 light:hover:bg-slate-300 transition-all duration-300\"\n      >\n        <Plus\n          size={16}\n          weight=\"bold\"\n          className=\"text-black light:text-slate-500\"\n        />\n      </button>\n      <Tooltip\n        id=\"new-workspace-tooltip\"\n        place=\"top\"\n        delayShow={300}\n        className=\"tooltip !text-xs\"\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Sidebar/SidebarToggle/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { SidebarSimple } from \"@phosphor-icons/react\";\nimport paths from \"@/utils/paths\";\nimport { Tooltip } from \"react-tooltip\";\nconst SIDEBAR_TOGGLE_STORAGE_KEY = \"anythingllm_sidebar_toggle\";\nexport const SIDEBAR_TOGGLE_EVENT = \"sidebar-toggle\";\n\n/**\n * Returns the previous state of the sidebar from localStorage.\n * If the sidebar was closed, returns false.\n * If the sidebar was open, returns true.\n * If the sidebar state is not set, returns true.\n * @returns {boolean}\n */\nfunction previousSidebarState() {\n  const previousState = window.localStorage.getItem(SIDEBAR_TOGGLE_STORAGE_KEY);\n  if (previousState === \"closed\") return false;\n  return true;\n}\n\nexport function useSidebarToggle() {\n  const [showSidebar, setShowSidebar] = useState(previousSidebarState());\n  const [canToggleSidebar, setCanToggleSidebar] = useState(true);\n\n  useEffect(() => {\n    function checkPath() {\n      const currentPath = window.location.pathname;\n      const isVisible =\n        currentPath === paths.home() ||\n        /^\\/workspace\\/[^\\/]+$/.test(currentPath) ||\n        /^\\/workspace\\/[^\\/]+\\/t\\/[^\\/]+$/.test(currentPath);\n      setCanToggleSidebar(isVisible);\n    }\n    checkPath();\n  }, [window.location.pathname]);\n\n  useEffect(() => {\n    function toggleSidebar(e) {\n      if (!canToggleSidebar) return;\n      if (\n        (e.ctrlKey || e.metaKey) &&\n        e.shiftKey &&\n        e.key.toLowerCase() === \"s\"\n      ) {\n        setShowSidebar((prev) => {\n          const newState = !prev;\n          window.localStorage.setItem(\n            SIDEBAR_TOGGLE_STORAGE_KEY,\n            newState ? \"open\" : \"closed\"\n          );\n          return newState;\n        });\n      }\n    }\n    window.addEventListener(\"keydown\", toggleSidebar);\n    return () => {\n      window.removeEventListener(\"keydown\", toggleSidebar);\n    };\n  }, []);\n\n  useEffect(() => {\n    window.localStorage.setItem(\n      SIDEBAR_TOGGLE_STORAGE_KEY,\n      showSidebar ? \"open\" : \"closed\"\n    );\n    window.dispatchEvent(\n      new CustomEvent(SIDEBAR_TOGGLE_EVENT, {\n        detail: { open: showSidebar },\n      })\n    );\n  }, [showSidebar]);\n\n  return { showSidebar, setShowSidebar, canToggleSidebar };\n}\n\nexport function ToggleSidebarButton({ showSidebar, setShowSidebar }) {\n  const isMac = navigator.userAgent.includes(\"Mac\");\n  const shortcut = isMac ? \"⌘ + Shift + S\" : \"Ctrl + Shift + S\";\n\n  return (\n    <>\n      <button\n        type=\"button\"\n        className={`hidden md:block border-none bg-transparent outline-none ring-0 absolute transition-all duration-500 z-10 ${showSidebar ? \"top-[18px] left-[248px]\" : \"top-[20px] left-[30px]\"}`}\n        onClick={() => setShowSidebar((prev) => !prev)}\n        data-tooltip-id=\"sidebar-toggle\"\n        data-tooltip-content={\n          showSidebar\n            ? `Hide Sidebar (${shortcut})`\n            : `Show Sidebar (${shortcut})`\n        }\n        aria-label={\n          showSidebar\n            ? `Hide Sidebar (${shortcut})`\n            : `Show Sidebar (${shortcut})`\n        }\n      >\n        <SidebarSimple\n          className=\"text-theme-text-secondary hover:text-theme-text-primary\"\n          size={24}\n        />\n      </button>\n      <Tooltip\n        id=\"sidebar-toggle\"\n        place=\"top\"\n        delayShow={300}\n        className=\"tooltip !text-xs z-99\"\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/Sidebar/index.jsx",
    "content": "import React, { useEffect, useRef, useState } from \"react\";\nimport { List, Plus } from \"@phosphor-icons/react\";\nimport NewWorkspaceModal, {\n  useNewWorkspaceModal,\n} from \"../Modals/NewWorkspace\";\nimport ActiveWorkspaces from \"./ActiveWorkspaces\";\nimport useLogo from \"@/hooks/useLogo\";\nimport useUser from \"@/hooks/useUser\";\nimport Footer from \"../Footer\";\nimport SettingsButton from \"../SettingsButton\";\nimport { Link } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\nimport { useTranslation } from \"react-i18next\";\nimport { useSidebarToggle, ToggleSidebarButton } from \"./SidebarToggle\";\nimport SearchBox from \"./SearchBox\";\nimport { Tooltip } from \"react-tooltip\";\nimport { createPortal } from \"react-dom\";\n\nexport default function Sidebar() {\n  const { user } = useUser();\n  const { logo } = useLogo();\n  const sidebarRef = useRef(null);\n  const { showSidebar, setShowSidebar, canToggleSidebar } = useSidebarToggle();\n  const {\n    showing: showingNewWsModal,\n    showModal: showNewWsModal,\n    hideModal: hideNewWsModal,\n  } = useNewWorkspaceModal();\n\n  return (\n    <>\n      <div\n        style={{\n          width: showSidebar ? \"292px\" : \"0px\",\n          paddingLeft: showSidebar ? \"0px\" : \"16px\",\n        }}\n        className=\"relative transition-all duration-500\"\n      >\n        {canToggleSidebar && (\n          <ToggleSidebarButton\n            showSidebar={showSidebar}\n            setShowSidebar={setShowSidebar}\n          />\n        )}\n        <div className=\"overflow-hidden h-full\">\n          <div className=\"flex shrink-0 w-full justify-center my-[18px]\">\n            <div className=\"flex w-[250px] min-w-[250px]\">\n              <Link to={paths.home()} aria-label=\"Home\">\n                <img\n                  src={logo}\n                  alt=\"Logo\"\n                  className={`rounded max-h-[24px] object-contain transition-opacity duration-500 ${showSidebar ? \"opacity-100\" : \"opacity-0\"}`}\n                />\n              </Link>\n            </div>\n          </div>\n          <div\n            ref={sidebarRef}\n            className=\"relative m-[16px] rounded-[16px] bg-theme-bg-sidebar light:bg-slate-200 border-[2px] border-theme-sidebar-border light:border-none min-w-[250px] p-[10px] h-[calc(100%-76px)]\"\n          >\n            <div className=\"flex flex-col h-full overflow-hidden\">\n              <div className=\"flex-grow flex flex-col min-w-[235px] min-h-0\">\n                <div className=\"relative h-[calc(100%-60px)] flex flex-col w-full justify-between pt-[10px] overflow-y-scroll no-scroll\">\n                  <div className=\"flex flex-col gap-y-[14px]\">\n                    <SearchBox user={user} showNewWsModal={showNewWsModal} />\n                    <ActiveWorkspaces />\n                  </div>\n                </div>\n                <div className=\"absolute bottom-0 left-0 right-0 pb-3 rounded-b-[16px] bg-theme-bg-sidebar light:bg-slate-200 bg-opacity-80 backdrop-filter backdrop-blur-md z-10\">\n                  <Footer />\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n        {showingNewWsModal && <NewWorkspaceModal hideModal={hideNewWsModal} />}\n      </div>\n      <WorkspaceAndThreadTooltips />\n    </>\n  );\n}\n\nexport function SidebarMobileHeader() {\n  const { logo } = useLogo();\n  const sidebarRef = useRef(null);\n  const [showSidebar, setShowSidebar] = useState(false);\n  const [showBgOverlay, setShowBgOverlay] = useState(false);\n  const {\n    showing: showingNewWsModal,\n    showModal: showNewWsModal,\n    hideModal: hideNewWsModal,\n  } = useNewWorkspaceModal();\n  const { user } = useUser();\n\n  useEffect(() => {\n    // Darkens the rest of the screen\n    // when sidebar is open.\n    function handleBg() {\n      if (showSidebar) {\n        setTimeout(() => {\n          setShowBgOverlay(true);\n        }, 300);\n      } else {\n        setShowBgOverlay(false);\n      }\n    }\n    handleBg();\n  }, [showSidebar]);\n\n  return (\n    <>\n      <div\n        aria-label=\"Show sidebar\"\n        className=\"fixed top-0 left-0 right-0 z-10 flex justify-between items-center px-4 py-2 bg-theme-bg-sidebar light:bg-white text-slate-200 shadow-lg h-16\"\n      >\n        <button\n          onClick={() => setShowSidebar(true)}\n          className=\"rounded-md p-2 flex items-center justify-center text-theme-text-secondary\"\n        >\n          <List className=\"h-6 w-6\" />\n        </button>\n        <div className=\"flex items-center justify-center flex-grow\">\n          <img\n            src={logo}\n            alt=\"Logo\"\n            className=\"block mx-auto h-6 w-auto\"\n            style={{ maxHeight: \"40px\", objectFit: \"contain\" }}\n          />\n        </div>\n        <div className=\"w-12\"></div>\n      </div>\n      <div\n        style={{\n          transform: showSidebar ? `translateX(0vw)` : `translateX(-100vw)`,\n        }}\n        className={`z-99 fixed top-0 left-0 transition-all duration-500 w-[100vw] h-[100vh]`}\n      >\n        <div\n          className={`${\n            showBgOverlay\n              ? \"transition-all opacity-1\"\n              : \"transition-none opacity-0\"\n          }  duration-500 fixed top-0 left-0 bg-theme-bg-secondary bg-opacity-75 w-screen h-screen`}\n          onClick={() => setShowSidebar(false)}\n        />\n        <div\n          ref={sidebarRef}\n          className=\"relative h-[100vh] fixed top-0 left-0  rounded-r-[26px] bg-theme-bg-sidebar w-[80%] p-[18px] \"\n        >\n          <div className=\"w-full h-full flex flex-col overflow-x-hidden items-between\">\n            {/* Header Information */}\n            <div className=\"flex w-full items-center justify-between gap-x-4\">\n              <div className=\"flex shrink-1 w-fit items-center justify-start\">\n                <img\n                  src={logo}\n                  alt=\"Logo\"\n                  className=\"rounded w-full max-h-[40px]\"\n                  style={{ objectFit: \"contain\" }}\n                />\n              </div>\n              {(!user || user?.role !== \"default\") && (\n                <div className=\"flex gap-x-2 items-center text-slate-500 shink-0\">\n                  <SettingsButton />\n                </div>\n              )}\n            </div>\n\n            {/* Primary Body */}\n            <div className=\"h-full flex flex-col w-full justify-between pt-4 \">\n              <div className=\"h-auto md:sidebar-items\">\n                <div className=\" flex flex-col gap-y-4 overflow-y-scroll no-scroll pb-[60px]\">\n                  <NewWorkspaceButton\n                    user={user}\n                    showNewWsModal={showNewWsModal}\n                  />\n                  <ActiveWorkspaces />\n                </div>\n              </div>\n              <div className=\"z-99 absolute bottom-0 left-0 right-0 pt-2 pb-6 rounded-br-[26px] bg-theme-bg-sidebar bg-opacity-80 backdrop-filter backdrop-blur-md\">\n                <Footer />\n              </div>\n            </div>\n          </div>\n        </div>\n        {showingNewWsModal && <NewWorkspaceModal hideModal={hideNewWsModal} />}\n      </div>\n    </>\n  );\n}\n\nfunction NewWorkspaceButton({ user, showNewWsModal }) {\n  const { t } = useTranslation();\n  if (!!user && user?.role === \"default\") return null;\n\n  return (\n    <div className=\"flex gap-x-2 items-center justify-between\">\n      <button\n        onClick={showNewWsModal}\n        className=\"flex flex-grow w-[75%] h-[44px] gap-x-2 py-[5px] px-4 bg-white rounded-lg text-sidebar justify-center items-center hover:bg-opacity-80 transition-all duration-300\"\n      >\n        <Plus className=\"h-5 w-5\" />\n        <p className=\"text-sidebar text-sm font-semibold\">\n          {t(\"new-workspace.title\")}\n        </p>\n      </button>\n    </div>\n  );\n}\n\nfunction WorkspaceAndThreadTooltips() {\n  return createPortal(\n    <React.Fragment>\n      <Tooltip\n        id=\"workspace-name\"\n        place=\"right\"\n        delayShow={800}\n        className=\"tooltip !text-xs z-99\"\n      />\n      <Tooltip\n        id=\"workspace-thread-name\"\n        place=\"right\"\n        delayShow={800}\n        className=\"tooltip !text-xs z-99\"\n      />\n    </React.Fragment>,\n    document.body\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/SpeechToText/BrowserNative/index.jsx",
    "content": "export default function BrowserNative() {\n  return (\n    <div className=\"w-full h-10 items-center flex\">\n      <p className=\"text-sm font-base text-white text-opacity-60\">\n        There is no configuration needed for this provider.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/TextToSpeech/BrowserNative/index.jsx",
    "content": "export default function BrowserNative() {\n  return (\n    <div className=\"w-full h-10 items-center flex\">\n      <p className=\"text-sm font-base text-white text-opacity-60\">\n        There is no configuration needed for this provider.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/TextToSpeech/ElevenLabsOptions/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function ElevenLabsOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.TTSElevenLabsKey);\n  const [elevenLabsKey, setElevenLabsKey] = useState(\n    settings?.TTSElevenLabsKey\n  );\n\n  return (\n    <div className=\"flex gap-x-4\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"TTSElevenLabsKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"ElevenLabs API Key\"\n          defaultValue={settings?.TTSElevenLabsKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n          onChange={(e) => setInputValue(e.target.value)}\n          onBlur={() => setElevenLabsKey(inputValue)}\n        />\n      </div>\n      {!settings?.credentialsOnly && (\n        <ElevenLabsModelSelection settings={settings} apiKey={elevenLabsKey} />\n      )}\n    </div>\n  );\n}\n\nfunction ElevenLabsModelSelection({ apiKey, settings }) {\n  const [groupedModels, setGroupedModels] = useState({});\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function findCustomModels() {\n      setLoading(true);\n      const { models } = await System.customModels(\n        \"elevenlabs-tts\",\n        typeof apiKey === \"boolean\" ? null : apiKey\n      );\n\n      if (models?.length > 0) {\n        const modelsByOrganization = models.reduce((acc, model) => {\n          acc[model.organization] = acc[model.organization] || [];\n          acc[model.organization].push(model);\n          return acc;\n        }, {});\n        setGroupedModels(modelsByOrganization);\n      }\n\n      setLoading(false);\n    }\n    findCustomModels();\n  }, [apiKey]);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Chat Model Selection\n        </label>\n        <select\n          name=\"TTSElevenLabsVoiceModel\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-60\">\n      <label className=\"text-white text-sm font-semibold block mb-3\">\n        Chat Model Selection\n      </label>\n      <select\n        name=\"TTSElevenLabsVoiceModel\"\n        required={true}\n        className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n      >\n        {Object.keys(groupedModels)\n          .sort()\n          .map((organization) => (\n            <optgroup key={organization} label={organization}>\n              {groupedModels[organization].map((model) => (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={model.id === settings?.TTSElevenLabsVoiceModel}\n                >\n                  {model.name}\n                </option>\n              ))}\n            </optgroup>\n          ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/TextToSpeech/OpenAiGenericOptions/index.jsx",
    "content": "import React from \"react\";\n\nexport default function OpenAiGenericTextToSpeechOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"flex gap-x-4\">\n        <div className=\"flex flex-col w-60\">\n          <div className=\"flex justify-between items-start mb-2\">\n            <label className=\"text-white text-sm font-semibold\">Base URL</label>\n          </div>\n          <input\n            type=\"url\"\n            name=\"TTSOpenAICompatibleEndpoint\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"http://localhost:7851/v1\"\n            defaultValue={settings?.TTSOpenAICompatibleEndpoint}\n            required={false}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n          <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n            This should be the base URL of the OpenAI compatible TTS service you\n            will generate TTS responses from.\n          </p>\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-2\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"TTSOpenAICompatibleKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"API Key\"\n            defaultValue={\n              settings?.TTSOpenAICompatibleKey ? \"*\".repeat(20) : \"\"\n            }\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n          <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n            Some TTS services require an API key to generate TTS responses -\n            this is optional if your service does not require one.\n          </p>\n        </div>\n      </div>\n      <div className=\"flex gap-x-4\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            TTS Model\n          </label>\n          <input\n            type=\"text\"\n            name=\"TTSOpenAICompatibleModel\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Your TTS model identifier\"\n            defaultValue={settings?.TTSOpenAICompatibleModel}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n          <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n            Most TTS services will have several models available. This is the{\" \"}\n            <code>model</code> parameter you will use to select the model you\n            want to use. Note: This is not the same as the voice model.\n          </p>\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Voice Model\n          </label>\n          <input\n            type=\"text\"\n            name=\"TTSOpenAICompatibleVoiceModel\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Your voice model identifier\"\n            defaultValue={settings?.TTSOpenAICompatibleVoiceModel}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n          <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60 mt-2\">\n            Most TTS services will have several voice models available, this is\n            the identifier for the voice model you want to use.\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/TextToSpeech/OpenAiOptions/index.jsx",
    "content": "function toProperCase(string) {\n  return string.replace(/\\w\\S*/g, function (txt) {\n    return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();\n  });\n}\n\nexport default function OpenAiTextToSpeechOptions({ settings }) {\n  const apiKey = settings?.TTSOpenAIKey;\n\n  return (\n    <div className=\"flex gap-x-4\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"TTSOpenAIKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"OpenAI API Key\"\n          defaultValue={apiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n        />\n      </div>\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Voice Model\n        </label>\n        <select\n          name=\"TTSOpenAIVoiceModel\"\n          defaultValue={settings?.TTSOpenAIVoiceModel ?? \"alloy\"}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          {[\"alloy\", \"echo\", \"fable\", \"onyx\", \"nova\", \"shimmer\"].map(\n            (voice) => {\n              return (\n                <option key={voice} value={voice}>\n                  {toProperCase(voice)}\n                </option>\n              );\n            }\n          )}\n        </select>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/TextToSpeech/PiperTTSOptions/index.jsx",
    "content": "import { useState, useEffect, useRef } from \"react\";\nimport PiperTTSClient from \"@/utils/piperTTS\";\nimport { titleCase } from \"text-case\";\nimport { humanFileSize } from \"@/utils/numbers\";\nimport showToast from \"@/utils/toast\";\nimport { CircleNotch, PauseCircle, PlayCircle } from \"@phosphor-icons/react\";\n\nexport default function PiperTTSOptions({ settings }) {\n  return (\n    <>\n      <p className=\"text-sm font-base text-white text-opacity-60 mb-4\">\n        All PiperTTS models will run in your browser locally. This can be\n        resource intensive on lower-end devices.\n      </p>\n      <div className=\"flex gap-x-4 items-center\">\n        <PiperTTSModelSelection settings={settings} />\n      </div>\n    </>\n  );\n}\n\nfunction voicesByLanguage(voices = []) {\n  const voicesByLanguage = voices.reduce((acc, voice) => {\n    const langName = voice?.language?.name_english ?? \"Unlisted\";\n    acc[langName] = acc[langName] || [];\n    acc[langName].push(voice);\n    return acc;\n  }, {});\n  return Object.entries(voicesByLanguage);\n}\n\nfunction voiceDisplayName(voice) {\n  const { is_stored, name, quality, files } = voice;\n  const onnxFileKey = Object.keys(files).find((key) => key.endsWith(\".onnx\"));\n  const fileSize = files?.[onnxFileKey]?.size_bytes || 0;\n  return `${is_stored ? \"✔ \" : \"\"}${titleCase(name)}-${quality === \"low\" ? \"Low\" : \"HQ\"} (${humanFileSize(fileSize)})`;\n}\n\nfunction PiperTTSModelSelection({ settings }) {\n  const [loading, setLoading] = useState(true);\n  const [voices, setVoices] = useState([]);\n  const [selectedVoice, setSelectedVoice] = useState(\n    settings?.TTSPiperTTSVoiceModel\n  );\n\n  function flushVoices() {\n    PiperTTSClient.flush()\n      .then(() =>\n        showToast(\"All voices flushed from browser storage\", \"info\", {\n          clear: true,\n        })\n      )\n      .catch((e) => console.error(e));\n  }\n\n  useEffect(() => {\n    PiperTTSClient.voices()\n      .then((voices) => {\n        if (voices?.length !== 0) return setVoices(voices);\n        throw new Error(\"Could not fetch voices from web worker.\");\n      })\n      .catch((e) => {\n        console.error(e);\n      })\n      .finally(() => setLoading(false));\n  }, []);\n\n  if (loading) {\n    return (\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Voice Model Selection\n        </label>\n        <select\n          name=\"TTSPiperTTSVoiceModel\"\n          value=\"\"\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option value=\"\" disabled={true}>\n            -- loading available models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-fit\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Voice Model Selection\n        </label>\n        <div className=\"flex items-center w-fit gap-x-4 mb-2\">\n          <select\n            name=\"TTSPiperTTSVoiceModel\"\n            required={true}\n            onChange={(e) => setSelectedVoice(e.target.value)}\n            value={selectedVoice}\n            className=\"border-none flex-shrink-0 bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n          >\n            {voicesByLanguage(voices).map(([lang, voices]) => {\n              return (\n                <optgroup key={lang} label={lang}>\n                  {voices.map((voice) => (\n                    <option key={voice.key} value={voice.key}>\n                      {voiceDisplayName(voice)}\n                    </option>\n                  ))}\n                </optgroup>\n              );\n            })}\n          </select>\n          <DemoVoiceSample voiceId={selectedVoice} />\n        </div>\n        <p className=\"text-xs text-white/40\">\n          The \"✔\" indicates this model is already stored locally and does not\n          need to be downloaded when run.\n        </p>\n      </div>\n      {!!voices.find((voice) => voice.is_stored) && (\n        <button\n          type=\"button\"\n          onClick={flushVoices}\n          className=\"w-fit border-none hover:text-white hover:underline text-white/40 text-sm my-4\"\n        >\n          Flush voice cache\n        </button>\n      )}\n    </div>\n  );\n}\n\nfunction DemoVoiceSample({ voiceId }) {\n  const playerRef = useRef(null);\n  const [speaking, setSpeaking] = useState(false);\n  const [loading, setLoading] = useState(false);\n  const [audioSrc, setAudioSrc] = useState(null);\n\n  async function speakMessage(e) {\n    e.preventDefault();\n    if (speaking) {\n      playerRef?.current?.pause();\n      return;\n    }\n\n    try {\n      if (!audioSrc) {\n        setLoading(true);\n        const client = new PiperTTSClient({ voiceId });\n        const blobUrl = await client.getAudioBlobForText(\n          \"Hello, welcome to AnythingLLM!\"\n        );\n        setAudioSrc(blobUrl);\n        setLoading(false);\n        client.worker?.terminate();\n        PiperTTSClient._instance = null;\n      } else {\n        playerRef.current.play();\n      }\n    } catch (e) {\n      console.error(e);\n      setLoading(false);\n      setSpeaking(false);\n    }\n  }\n\n  useEffect(() => {\n    function setupPlayer() {\n      if (!playerRef?.current) return;\n      playerRef.current.addEventListener(\"play\", () => {\n        setSpeaking(true);\n      });\n\n      playerRef.current.addEventListener(\"pause\", () => {\n        playerRef.current.currentTime = 0;\n        setSpeaking(false);\n        setAudioSrc(null);\n      });\n    }\n    setupPlayer();\n  }, []);\n\n  return (\n    <button\n      type=\"button\"\n      onClick={speakMessage}\n      disabled={loading}\n      className=\"border-none text-zinc-300 flex items-center gap-x-1\"\n    >\n      {speaking ? (\n        <>\n          <PauseCircle size={20} className=\"flex-shrink-0\" />\n          <p className=\"text-sm flex-shrink-0\">Stop demo</p>\n        </>\n      ) : (\n        <>\n          {loading ? (\n            <>\n              <CircleNotch size={20} className=\"animate-spin flex-shrink-0\" />\n              <p className=\"text-sm flex-shrink-0\">Loading voice</p>\n            </>\n          ) : (\n            <>\n              <PlayCircle size={20} className=\"flex-shrink-0 text-white\" />\n              <p className=\"text-white text-sm flex-shrink-0\">Play sample</p>\n            </>\n          )}\n        </>\n      )}\n      <audio\n        ref={playerRef}\n        hidden={true}\n        src={audioSrc}\n        autoPlay={true}\n        controls={false}\n      />\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/TranscriptionSelection/NativeTranscriptionOptions/index.jsx",
    "content": "import { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Gauge } from \"@phosphor-icons/react\";\n\nexport default function NativeTranscriptionOptions({ settings }) {\n  const { t } = useTranslation();\n  const [model, setModel] = useState(settings?.WhisperModelPref);\n\n  return (\n    <div className=\"w-full flex flex-col gap-y-4\">\n      <LocalWarning model={model} />\n      <div className=\"w-full flex items-center gap-4\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            {t(\"common.selection\")}\n          </label>\n          <select\n            name=\"WhisperModelPref\"\n            defaultValue={model}\n            onChange={(e) => setModel(e.target.value)}\n            className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n          >\n            {[\"Xenova/whisper-small\", \"Xenova/whisper-large\"].map(\n              (value, i) => {\n                return (\n                  <option key={i} value={value}>\n                    {value}\n                  </option>\n                );\n              }\n            )}\n          </select>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction LocalWarning({ model }) {\n  switch (model) {\n    case \"Xenova/whisper-small\":\n      return <WhisperSmall />;\n    case \"Xenova/whisper-large\":\n      return <WhisperLarge />;\n    default:\n      return <WhisperSmall />;\n  }\n}\n\nfunction WhisperSmall() {\n  const { t } = useTranslation();\n\n  return (\n    <div className=\"flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2\">\n      <div className=\"gap-x-2 flex items-center\">\n        <Gauge size={25} />\n        <p className=\"text-sm\">\n          {t(\"transcription.warn-start\")}\n          <br />\n          {t(\"transcription.warn-recommend\")}\n          <br />\n          <br />\n          <i>{t(\"transcription.warn-end\")} (250mb)</i>\n        </p>\n      </div>\n    </div>\n  );\n}\n\nfunction WhisperLarge() {\n  const { t } = useTranslation();\n\n  return (\n    <div className=\"flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2\">\n      <div className=\"gap-x-2 flex items-center\">\n        <Gauge size={25} />\n        <p className=\"text-sm\">\n          {t(\"transcription.warn-start\")}\n          <br />\n          {t(\"transcription.warn-recommend\")}\n          <br />\n          <br />\n          <i>{t(\"transcription.warn-end\")} (1.56GB)</i>\n        </p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/TranscriptionSelection/OpenAiOptions/index.jsx",
    "content": "import { useState } from \"react\";\n\nexport default function OpenAiWhisperOptions({ settings }) {\n  const [inputValue, setInputValue] = useState(settings?.OpenAiKey);\n  const [_openAIKey, setOpenAIKey] = useState(settings?.OpenAiKey);\n\n  return (\n    <div className=\"flex gap-x-7 gap-[36px] mt-1.5\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          API Key\n        </label>\n        <input\n          type=\"password\"\n          name=\"OpenAiKey\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"OpenAI API Key\"\n          defaultValue={settings?.OpenAiKey ? \"*\".repeat(20) : \"\"}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n          onChange={(e) => setInputValue(e.target.value)}\n          onBlur={() => setOpenAIKey(inputValue)}\n        />\n      </div>\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          Whisper Model\n        </label>\n        <select\n          disabled={true}\n          className=\"border-none flex-shrink-0 bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            Whisper Large\n          </option>\n        </select>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/UserIcon/index.jsx",
    "content": "import React, { memo } from \"react\";\nimport usePfp from \"../../hooks/usePfp\";\nimport UserDefaultPfp from \"./user.svg\";\nimport WorkspaceDefaultPfp from \"./workspace.svg\";\n\nconst UserIcon = memo(({ role }) => {\n  const { pfp } = usePfp();\n\n  return (\n    <div className=\"relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden\">\n      {role === \"user\" && <RenderUserPfp pfp={pfp} />}\n      {role !== \"user\" && (\n        <img\n          src={WorkspaceDefaultPfp}\n          alt=\"system profile picture\"\n          className=\"flex items-center justify-center rounded-full border-solid border border-white/40 light:border-theme-sidebar-border light:bg-theme-bg-chat-input\"\n        />\n      )}\n    </div>\n  );\n});\n\nfunction RenderUserPfp({ pfp }) {\n  if (!pfp)\n    return (\n      <img\n        src={UserDefaultPfp}\n        alt=\"User profile picture\"\n        className=\"rounded-full border-none\"\n      />\n    );\n\n  return (\n    <img\n      src={pfp}\n      alt=\"User profile picture\"\n      className=\"absolute top-0 left-0 w-full h-full object-cover rounded-full border-none\"\n    />\n  );\n}\n\nexport default UserIcon;\n"
  },
  {
    "path": "frontend/src/components/UserMenu/AccountModal/index.jsx",
    "content": "import { useLanguageOptions } from \"@/hooks/useLanguageOptions\";\nimport usePfp from \"@/hooks/usePfp\";\nimport System from \"@/models/system\";\nimport Appearance from \"@/models/appearance\";\nimport { AUTH_USER } from \"@/utils/constants\";\nimport showToast from \"@/utils/toast\";\nimport { Info, Plus, X } from \"@phosphor-icons/react\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { useTheme } from \"@/hooks/useTheme\";\nimport { useTranslation } from \"react-i18next\";\nimport { useState, useEffect } from \"react\";\nimport { Tooltip } from \"react-tooltip\";\nimport { safeJsonParse } from \"@/utils/request\";\nimport Toggle from \"@/components/lib/Toggle\";\nimport {\n  USERNAME_MIN_LENGTH,\n  USERNAME_MAX_LENGTH,\n  USERNAME_PATTERN,\n} from \"@/utils/username\";\n\nexport default function AccountModal({ user, hideModal }) {\n  const { pfp, setPfp } = usePfp();\n  const { t } = useTranslation();\n\n  const handleFileUpload = async (event) => {\n    const file = event.target.files[0];\n    if (!file) return false;\n\n    const formData = new FormData();\n    formData.append(\"file\", file);\n    const { success, error } = await System.uploadPfp(formData);\n    if (!success) {\n      showToast(t(\"profile_settings.failed_upload\", { error }), \"error\");\n      return;\n    }\n\n    const pfpUrl = await System.fetchPfp(user.id);\n    setPfp(pfpUrl);\n    showToast(t(\"profile_settings.upload_success\"), \"success\");\n  };\n\n  const handleRemovePfp = async () => {\n    const { success, error } = await System.removePfp();\n    if (!success) {\n      showToast(t(\"profile_settings.failed_remove\", { error }), \"error\");\n      return;\n    }\n\n    setPfp(null);\n  };\n\n  const handleUpdate = async (e) => {\n    e.preventDefault();\n\n    const data = {};\n    const form = new FormData(e.target);\n    for (var [key, value] of form.entries()) {\n      if (!value || value === null) continue;\n      data[key] = value;\n    }\n\n    const { success, error } = await System.updateUser(data);\n    if (success) {\n      let storedUser = safeJsonParse(localStorage.getItem(AUTH_USER), null);\n      if (storedUser) {\n        storedUser.username = data.username;\n        storedUser.bio = data.bio;\n        localStorage.setItem(AUTH_USER, JSON.stringify(storedUser));\n      }\n      showToast(t(\"profile_settings.profile_updated\"), \"success\", {\n        clear: true,\n      });\n      hideModal();\n    } else {\n      showToast(t(\"profile_settings.failed_update_user\", { error }), \"error\");\n    }\n  };\n  return (\n    <ModalWrapper isOpen={true}>\n      <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              {t(\"profile_settings.edit_account\")}\n            </h3>\n          </div>\n          <button\n            onClick={hideModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div\n          className=\"h-full w-full overflow-y-auto\"\n          style={{ maxHeight: \"calc(100vh - 200px)\" }}\n        >\n          <form onSubmit={handleUpdate} className=\"space-y-6\">\n            <div className=\"flex flex-col md:flex-row items-center justify-center gap-8\">\n              <div className=\"flex flex-col items-center\">\n                <label className=\"group w-48 h-48 flex flex-col items-center justify-center bg-theme-bg-primary hover:bg-theme-bg-secondary transition-colors duration-300 rounded-full mt-8 border-2 border-dashed border-white light:border-[#686C6F] light:bg-[#E0F2FE] light:hover:bg-transparent cursor-pointer hover:opacity-60\">\n                  <input\n                    id=\"logo-upload\"\n                    type=\"file\"\n                    accept=\"image/*\"\n                    className=\"hidden\"\n                    onChange={handleFileUpload}\n                  />\n                  {pfp ? (\n                    <img\n                      src={pfp}\n                      alt=\"User profile picture\"\n                      className=\"w-48 h-48 rounded-full object-cover bg-white\"\n                    />\n                  ) : (\n                    <div className=\"flex flex-col items-center justify-center p-3\">\n                      <Plus className=\"w-8 h-8 text-theme-text-secondary m-2\" />\n                      <span className=\"text-theme-text-secondary text-opacity-80 text-sm font-semibold\">\n                        {t(\"profile_settings.profile_picture\")}\n                      </span>\n                      <span className=\"text-theme-text-secondary text-opacity-60 text-xs\">\n                        800 x 800\n                      </span>\n                    </div>\n                  )}\n                </label>\n                {pfp && (\n                  <button\n                    type=\"button\"\n                    onClick={handleRemovePfp}\n                    className=\"mt-3 text-theme-text-secondary text-opacity-60 text-sm font-medium hover:underline\"\n                  >\n                    {t(\"profile_settings.remove_profile_picture\")}\n                  </button>\n                )}\n              </div>\n            </div>\n            <div className=\"flex flex-col gap-y-4 px-6\">\n              <div>\n                <label\n                  htmlFor=\"username\"\n                  className=\"block mb-2 text-sm font-medium text-theme-text-primary\"\n                >\n                  {t(\"profile_settings.username\")}\n                </label>\n                <input\n                  name=\"username\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg placeholder:text-theme-settings-input-placeholder border-gray-500 text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"User's username\"\n                  minLength={USERNAME_MIN_LENGTH}\n                  maxLength={USERNAME_MAX_LENGTH}\n                  pattern={USERNAME_PATTERN}\n                  defaultValue={user.username}\n                  required\n                  autoComplete=\"off\"\n                />\n                <p className=\"mt-2 text-xs text-white/60\">\n                  {t(\"common.username_requirements\")}\n                </p>\n              </div>\n              <div>\n                <label\n                  htmlFor=\"password\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  {t(\"profile_settings.new_password\")}\n                </label>\n                <input\n                  name=\"password\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg placeholder:text-theme-settings-input-placeholder border-gray-500 text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder={`${user.username}'s new password`}\n                  minLength={8}\n                />\n                <p className=\"mt-2 text-xs text-white/60\">\n                  {t(\"profile_settings.password_description\")}\n                </p>\n              </div>\n              <div>\n                <label\n                  htmlFor=\"bio\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Bio\n                </label>\n                <textarea\n                  name=\"bio\"\n                  className=\"border-none bg-theme-settings-input-bg placeholder:text-theme-settings-input-placeholder border-gray-500 text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 min-h-[100px] resize-y\"\n                  placeholder=\"Tell us about yourself...\"\n                  defaultValue={user.bio}\n                />\n              </div>\n              <div className=\"flex gap-x-16\">\n                <div className=\"flex flex-col gap-y-6\">\n                  <ThemePreference />\n                  <LanguagePreference />\n                </div>\n                <div className=\"flex flex-col gap-y-6\">\n                  <AutoSubmitPreference />\n                  <AutoSpeakPreference />\n                </div>\n              </div>\n            </div>\n            <div className=\"flex justify-between items-center border-t border-theme-modal-border pt-4 p-6\">\n              <button\n                onClick={hideModal}\n                type=\"button\"\n                className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n              >\n                {t(\"profile_settings.cancel\")}\n              </button>\n              <button\n                type=\"submit\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                {t(\"profile_settings.update_account\")}\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </ModalWrapper>\n  );\n}\n\nfunction LanguagePreference() {\n  const {\n    currentLanguage,\n    supportedLanguages,\n    getLanguageName,\n    changeLanguage,\n  } = useLanguageOptions();\n  const { t } = useTranslation();\n  return (\n    <div>\n      <label\n        htmlFor=\"userLang\"\n        className=\"block mb-2 text-sm font-medium text-white\"\n      >\n        {t(\"profile_settings.language\")}\n      </label>\n      <select\n        name=\"userLang\"\n        className=\"border-none bg-theme-settings-input-bg w-fit mt-2 px-4 focus:outline-primary-button active:outline-primary-button outline-none text-white text-sm rounded-lg block py-2\"\n        defaultValue={currentLanguage || \"en\"}\n        onChange={(e) => changeLanguage(e.target.value)}\n      >\n        {supportedLanguages.map((lang) => {\n          return (\n            <option key={lang} value={lang}>\n              {getLanguageName(lang)}\n            </option>\n          );\n        })}\n      </select>\n    </div>\n  );\n}\n\nfunction ThemePreference() {\n  const { theme, setTheme, availableThemes } = useTheme();\n  const { t } = useTranslation();\n  return (\n    <div>\n      <label\n        htmlFor=\"theme\"\n        className=\"block mb-2 text-sm font-medium text-white\"\n      >\n        {t(\"profile_settings.theme\")}\n      </label>\n      <select\n        name=\"theme\"\n        value={theme}\n        onChange={(e) => setTheme(e.target.value)}\n        className=\"border-none bg-theme-settings-input-bg w-fit px-4 focus:outline-primary-button active:outline-primary-button outline-none text-white text-sm rounded-lg block py-2\"\n      >\n        {Object.entries(availableThemes).map(([key, value]) => (\n          <option key={key} value={key}>\n            {value}\n          </option>\n        ))}\n      </select>\n    </div>\n  );\n}\n\nfunction AutoSubmitPreference() {\n  const [autoSubmitSttInput, setAutoSubmitSttInput] = useState(true);\n  const { t } = useTranslation();\n\n  useEffect(() => {\n    const settings = Appearance.getSettings();\n    setAutoSubmitSttInput(settings.autoSubmitSttInput ?? true);\n  }, []);\n\n  const handleChange = (checked) => {\n    setAutoSubmitSttInput(checked);\n    Appearance.updateSettings({ autoSubmitSttInput: checked });\n  };\n\n  return (\n    <div>\n      <div className=\"flex items-center gap-x-1 mb-2\">\n        <label\n          htmlFor=\"autoSubmit\"\n          className=\"block text-sm font-medium text-white\"\n        >\n          {t(\"customization.chat.auto_submit.title\")}\n        </label>\n        <div\n          data-tooltip-id=\"auto-submit-info\"\n          data-tooltip-content={t(\"customization.chat.auto_submit.description\")}\n          className=\"cursor-pointer h-fit\"\n        >\n          <Info size={16} weight=\"bold\" className=\"text-white\" />\n        </div>\n      </div>\n      <Toggle size=\"lg\" enabled={autoSubmitSttInput} onChange={handleChange} />\n      <Tooltip\n        id=\"auto-submit-info\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"allm-tooltip !allm-text-xs\"\n      />\n    </div>\n  );\n}\n\nfunction AutoSpeakPreference() {\n  const [autoPlayAssistantTtsResponse, setAutoPlayAssistantTtsResponse] =\n    useState(false);\n  const { t } = useTranslation();\n\n  useEffect(() => {\n    const settings = Appearance.getSettings();\n    setAutoPlayAssistantTtsResponse(\n      settings.autoPlayAssistantTtsResponse ?? false\n    );\n  }, []);\n\n  const handleChange = (checked) => {\n    setAutoPlayAssistantTtsResponse(checked);\n    Appearance.updateSettings({ autoPlayAssistantTtsResponse: checked });\n  };\n\n  return (\n    <div>\n      <div className=\"flex items-center gap-x-1 mb-2\">\n        <label\n          htmlFor=\"autoSpeak\"\n          className=\"block text-sm font-medium text-white\"\n        >\n          {t(\"customization.chat.auto_speak.title\")}\n        </label>\n        <div\n          data-tooltip-id=\"auto-speak-info\"\n          data-tooltip-content={t(\"customization.chat.auto_speak.description\")}\n          className=\"cursor-pointer h-fit\"\n        >\n          <Info size={16} weight=\"bold\" className=\"text-white\" />\n        </div>\n      </div>\n      <Toggle\n        size=\"lg\"\n        enabled={autoPlayAssistantTtsResponse}\n        onChange={handleChange}\n      />\n      <Tooltip\n        id=\"auto-speak-info\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"allm-tooltip !allm-text-xs\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/UserMenu/UserButton/index.jsx",
    "content": "import useLoginMode from \"@/hooks/useLoginMode\";\nimport usePfp from \"@/hooks/usePfp\";\nimport useUser from \"@/hooks/useUser\";\nimport System from \"@/models/system\";\nimport paths from \"@/utils/paths\";\nimport { userFromStorage } from \"@/utils/request\";\nimport { Person } from \"@phosphor-icons/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport AccountModal from \"../AccountModal\";\nimport {\n  AUTH_TIMESTAMP,\n  AUTH_TOKEN,\n  AUTH_USER,\n  LAST_VISITED_WORKSPACE,\n  USER_PROMPT_INPUT_MAP,\n} from \"@/utils/constants\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function UserButton() {\n  const { t } = useTranslation();\n  const mode = useLoginMode();\n  const { user } = useUser();\n  const menuRef = useRef();\n  const buttonRef = useRef();\n  const [showMenu, setShowMenu] = useState(false);\n  const [showAccountSettings, setShowAccountSettings] = useState(false);\n  const [supportEmail, setSupportEmail] = useState(\"\");\n\n  const handleClose = (event) => {\n    if (\n      menuRef.current &&\n      !menuRef.current.contains(event.target) &&\n      !buttonRef.current.contains(event.target)\n    ) {\n      setShowMenu(false);\n    }\n  };\n\n  const handleOpenAccountModal = () => {\n    setShowAccountSettings(true);\n    setShowMenu(false);\n  };\n\n  useEffect(() => {\n    if (showMenu) {\n      document.addEventListener(\"mousedown\", handleClose);\n    }\n    return () => document.removeEventListener(\"mousedown\", handleClose);\n  }, [showMenu]);\n\n  useEffect(() => {\n    const fetchSupportEmail = async () => {\n      const supportEmail = await System.fetchSupportEmail();\n      setSupportEmail(\n        supportEmail?.email\n          ? `mailto:${supportEmail.email}`\n          : paths.mailToMintplex()\n      );\n    };\n    fetchSupportEmail();\n  }, []);\n\n  if (mode === null) return null;\n  return (\n    <div className=\"absolute top-3 right-4 md:top-9 md:right-10 w-fit h-fit z-40\">\n      <button\n        ref={buttonRef}\n        onClick={() => setShowMenu(!showMenu)}\n        type=\"button\"\n        className=\"uppercase transition-all duration-300 w-[35px] h-[35px] text-base font-semibold rounded-full flex items-center bg-theme-action-menu-bg hover:bg-theme-action-menu-item-hover justify-center text-white p-2 hover:border-slate-100 hover:border-opacity-50 border-transparent border\"\n      >\n        {mode === \"multi\" ? <UserDisplay /> : <Person size={14} />}\n      </button>\n\n      {showMenu && (\n        <div\n          ref={menuRef}\n          className=\"w-fit rounded-lg absolute top-12 right-0 bg-theme-action-menu-bg p-2 flex items-center-justify-center\"\n        >\n          <div className=\"flex flex-col gap-y-2\">\n            {mode === \"multi\" && !!user && (\n              <button\n                onClick={handleOpenAccountModal}\n                className=\"border-none text-white hover:bg-theme-action-menu-item-hover w-full text-left px-4 py-1.5 rounded-md\"\n              >\n                {t(\"profile_settings.account\")}\n              </button>\n            )}\n            <a\n              href={supportEmail}\n              className=\"text-white hover:bg-theme-action-menu-item-hover w-full text-left px-4 py-1.5 rounded-md\"\n            >\n              {t(\"profile_settings.support\")}\n            </a>\n            <button\n              onClick={() => {\n                window.localStorage.removeItem(AUTH_USER);\n                window.localStorage.removeItem(AUTH_TOKEN);\n                window.localStorage.removeItem(AUTH_TIMESTAMP);\n                window.localStorage.removeItem(LAST_VISITED_WORKSPACE);\n                window.localStorage.removeItem(USER_PROMPT_INPUT_MAP);\n                window.location.replace(paths.home());\n              }}\n              type=\"button\"\n              className=\"text-white hover:bg-theme-action-menu-item-hover w-full text-left px-4 py-1.5 rounded-md\"\n            >\n              {t(\"profile_settings.signout\")}\n            </button>\n          </div>\n        </div>\n      )}\n      {user && showAccountSettings && (\n        <AccountModal\n          user={user}\n          hideModal={() => setShowAccountSettings(false)}\n        />\n      )}\n    </div>\n  );\n}\n\nfunction UserDisplay() {\n  const { pfp } = usePfp();\n  const user = userFromStorage();\n\n  if (pfp) {\n    return (\n      <div className=\"w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden transition-all duration-300 bg-gray-100 hover:border-slate-100 hover:border-opacity-50 border-transparent border hover:opacity-60\">\n        <img\n          src={pfp}\n          alt=\"User profile picture\"\n          className=\"w-full h-full object-cover\"\n        />\n      </div>\n    );\n  }\n\n  return user?.username?.slice(0, 2) || \"AA\";\n}\n"
  },
  {
    "path": "frontend/src/components/UserMenu/index.jsx",
    "content": "import UserButton from \"./UserButton\";\n\nexport default function UserMenu({ children }) {\n  return (\n    <div className=\"w-auto h-auto\">\n      <UserButton />\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/VectorDBSelection/AstraDBOptions/index.jsx",
    "content": "export default function AstraDBOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Astra DB Endpoint\n          </label>\n          <input\n            type=\"url\"\n            name=\"AstraDBEndpoint\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Astra DB API endpoint\"\n            defaultValue={settings?.AstraDBEndpoint}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Astra DB Application Token\n          </label>\n          <input\n            type=\"password\"\n            name=\"AstraDBApplicationToken\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"AstraCS:...\"\n            defaultValue={\n              settings?.AstraDBApplicationToken ? \"*\".repeat(20) : \"\"\n            }\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/VectorDBSelection/ChromaCloudOptions/index.jsx",
    "content": "export default function ChromaCloudOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"ChromaCloudApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"ck-your-api-key-here\"\n            defaultValue={settings?.ChromaCloudApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Tenant ID\n          </label>\n          <input\n            name=\"ChromaCloudTenant\"\n            autoComplete=\"off\"\n            type=\"text\"\n            defaultValue={settings?.ChromaCloudTenant}\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"your-tenant-id-here\"\n            required={true}\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Database Name\n          </label>\n          <input\n            name=\"ChromaCloudDatabase\"\n            autoComplete=\"off\"\n            type=\"text\"\n            defaultValue={settings?.ChromaCloudDatabase}\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"your-database-name\"\n            required={true}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/VectorDBSelection/ChromaDBOptions/index.jsx",
    "content": "export default function ChromaDBOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Chroma Endpoint\n          </label>\n          <input\n            type=\"url\"\n            name=\"ChromaEndpoint\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"http://localhost:8000\"\n            defaultValue={settings?.ChromaEndpoint}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Header\n          </label>\n          <input\n            name=\"ChromaApiHeader\"\n            autoComplete=\"off\"\n            type=\"text\"\n            defaultValue={settings?.ChromaApiHeader}\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"X-Api-Key\"\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            name=\"ChromaApiKey\"\n            autoComplete=\"off\"\n            type=\"password\"\n            defaultValue={settings?.ChromaApiKey ? \"*\".repeat(20) : \"\"}\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"sk-myApiKeyToAccessMyChromaInstance\"\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/VectorDBSelection/LanceDBOptions/index.jsx",
    "content": "import { useTranslation } from \"react-i18next\";\nexport default function LanceDBOptions() {\n  const { t } = useTranslation();\n  return (\n    <div className=\"w-full h-10 items-center flex\">\n      <p className=\"text-sm font-base text-white text-opacity-60\">\n        {t(\"vector.provider.description\")}\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/VectorDBSelection/MilvusDBOptions/index.jsx",
    "content": "export default function MilvusDBOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Milvus DB Address\n          </label>\n          <input\n            type=\"text\"\n            name=\"MilvusAddress\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"http://localhost:19530\"\n            defaultValue={settings?.MilvusAddress}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Milvus Username\n          </label>\n          <input\n            type=\"text\"\n            name=\"MilvusUsername\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"username\"\n            defaultValue={settings?.MilvusUsername}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Milvus Password\n          </label>\n          <input\n            type=\"password\"\n            name=\"MilvusPassword\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"password\"\n            defaultValue={settings?.MilvusPassword ? \"*\".repeat(20) : \"\"}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/VectorDBSelection/PGVectorOptions/index.jsx",
    "content": "import { Info } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\n\nexport default function PGVectorOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-96\">\n          <div className=\"flex items-center gap-x-1 mb-3\">\n            <label className=\"text-white text-sm font-semibold block\">\n              Postgres Connection String\n            </label>\n            <Info\n              size={16}\n              className=\"text-theme-text-secondary cursor-pointer\"\n              data-tooltip-id=\"pgvector-connection-string-tooltip\"\n              data-tooltip-place=\"right\"\n            />\n            <Tooltip\n              delayHide={300}\n              id=\"pgvector-connection-string-tooltip\"\n              className=\"max-w-md z-99\"\n              clickable={true}\n            >\n              <p className=\"text-md whitespace-pre-line break-words\">\n                This is the connection string for the Postgres database in the\n                format of <br />\n                <code>postgresql://username:password@host:port/database</code>\n                <br />\n                <br />\n                The user for the database must have the following permissions:\n                <ul className=\"list-disc list-inside\">\n                  <li>Read access to the database</li>\n                  <li>Read access to the database schema</li>\n                  <li>Create access to the database</li>\n                </ul>\n                <br />\n                <b>\n                  You must have the pgvector extension installed on the\n                  database.\n                </b>\n              </p>\n            </Tooltip>\n          </div>\n          <input\n            type=\"text\"\n            name=\"PGVectorConnectionString\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"postgresql://username:password@host:port/database\"\n            defaultValue={\n              settings?.PGVectorConnectionString ? \"*\".repeat(20) : \"\"\n            }\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <div className=\"flex items-center gap-x-1 mb-3\">\n            <label className=\"text-white text-sm font-semibold block\">\n              Vector Table Name\n            </label>\n            <Info\n              size={16}\n              className=\"text-theme-text-secondary cursor-pointer\"\n              data-tooltip-id=\"pgvector-table-name-tooltip\"\n              data-tooltip-place=\"right\"\n            />\n            <Tooltip\n              delayHide={300}\n              id=\"pgvector-table-name-tooltip\"\n              className=\"max-w-md z-99\"\n              clickable={true}\n            >\n              <p className=\"text-md whitespace-pre-line break-words\">\n                This is the name of the table in the Postgres database that will\n                store the vectors.\n                <br />\n                <br />\n                By default, the table name is <code>anythingllm_vectors</code>.\n                <br />\n                <br />\n                <b>\n                  This table must not already exist on the database - it will be\n                  created automatically.\n                </b>\n              </p>\n            </Tooltip>\n          </div>\n          <input\n            type=\"text\"\n            name=\"PGVectorTableName\"\n            autoComplete=\"off\"\n            defaultValue={settings?.PGVectorTableName}\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"vector_table\"\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/VectorDBSelection/PineconeDBOptions/index.jsx",
    "content": "export default function PineconeDBOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Pinecone DB API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"PineConeKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Pinecone API Key\"\n            defaultValue={settings?.PineConeKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Pinecone Index Name\n          </label>\n          <input\n            type=\"text\"\n            name=\"PineConeIndex\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"my-index\"\n            defaultValue={settings?.PineConeIndex}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/VectorDBSelection/QDrantDBOptions/index.jsx",
    "content": "export default function QDrantDBOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            QDrant API Endpoint\n          </label>\n          <input\n            type=\"url\"\n            name=\"QdrantEndpoint\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"http://localhost:6633\"\n            defaultValue={settings?.QdrantEndpoint}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"QdrantApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"wOeqxsYP4....1244sba\"\n            defaultValue={settings?.QdrantApiKey ? \"*\".repeat(20) : \"\"}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/VectorDBSelection/VectorDBItem/index.jsx",
    "content": "export default function VectorDBItem({\n  name,\n  value,\n  image,\n  description,\n  checked,\n  onClick,\n}) {\n  return (\n    <div\n      onClick={() => onClick(value)}\n      className={`w-full p-2 rounded-md hover:cursor-pointer hover:bg-theme-bg-secondary ${\n        checked ? \"bg-theme-bg-secondary\" : \"\"\n      }`}\n    >\n      <input\n        type=\"checkbox\"\n        value={value}\n        className=\"peer hidden\"\n        checked={checked}\n        readOnly={true}\n        formNoValidate={true}\n      />\n      <div className=\"flex gap-x-4 items-center\">\n        <img\n          src={image}\n          alt={`${name} logo`}\n          className=\"w-10 h-10 rounded-md\"\n        />\n        <div className=\"flex flex-col\">\n          <div className=\"text-sm font-semibold text-white\">{name}</div>\n          <div className=\"mt-1 text-xs text-description\">{description}</div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/VectorDBSelection/WeaviateDBOptions/index.jsx",
    "content": "export default function WeaviateDBOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-7\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Weaviate Endpoint\n          </label>\n          <input\n            type=\"url\"\n            name=\"WeaviateEndpoint\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"http://localhost:8080\"\n            defaultValue={settings?.WeaviateEndpoint}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"WeaviateApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"sk-123Abcweaviate\"\n            defaultValue={settings?.WeaviateApiKey ? \"*\".repeat(20) : \"\"}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/VectorDBSelection/ZillizCloudOptions/index.jsx",
    "content": "export default function ZillizCloudOptions({ settings }) {\n  return (\n    <div className=\"w-full flex flex-col gap-y-4\">\n      <div className=\"w-full flex items-center gap-[36px] mt-1.5\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Cluster Endpoint\n          </label>\n          <input\n            type=\"text\"\n            name=\"ZillizEndpoint\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"https://sample.api.gcp-us-west1.zillizcloud.com\"\n            defaultValue={settings?.ZillizEndpoint}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Token\n          </label>\n          <input\n            type=\"password\"\n            name=\"ZillizApiToken\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Zilliz cluster API Token\"\n            defaultValue={settings?.ZillizApiToken ? \"*\".repeat(20) : \"\"}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Chartable/CustomCell.jsx",
    "content": "export default function CustomCell({ ...props }) {\n  const { root, depth, x, y, width, height, index, colors, name } = props;\n  return (\n    <g>\n      <rect\n        x={x}\n        y={y}\n        width={width}\n        height={height}\n        style={{\n          fill:\n            depth < 2\n              ? colors[Math.floor((index / root.children.length) * 6)]\n              : \"#ffffff00\",\n          stroke: \"#fff\",\n          strokeWidth: 2 / (depth + 1e-10),\n          strokeOpacity: 1 / (depth + 1e-10),\n        }}\n      />\n      {depth === 1 ? (\n        <text\n          x={x + width / 2}\n          y={y + height / 2 + 7}\n          textAnchor=\"middle\"\n          fill=\"#fff\"\n          fontSize={14}\n        >\n          {name}\n        </text>\n      ) : null}\n      {depth === 1 ? (\n        <text x={x + 4} y={y + 18} fill=\"#fff\" fontSize={16} fillOpacity={0.9}>\n          {index + 1}\n        </text>\n      ) : null}\n    </g>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Chartable/CustomTooltip.jsx",
    "content": "import { Tooltip as RechartsTooltip } from \"recharts\";\n\n// Given a hex, convert to the opposite highest-contrast color\n// and if `bw` is enabled, force it to be black/white to normalize\n// interface.\nfunction invertColor(hex, bw) {\n  if (hex.indexOf(\"#\") === 0) {\n    hex = hex.slice(1);\n  }\n  // convert 3-digit hex to 6-digits.\n  if (hex.length === 3) {\n    hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];\n  }\n  if (hex.length !== 6) {\n    throw new Error(\"Invalid HEX color.\");\n  }\n  var r = parseInt(hex.slice(0, 2), 16),\n    g = parseInt(hex.slice(2, 4), 16),\n    b = parseInt(hex.slice(4, 6), 16);\n  if (bw) {\n    // https://stackoverflow.com/a/3943023/112731\n    return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? \"#FFFFFF\" : \"#000000\";\n    // : '#FFFFFF';\n  }\n  // invert color components\n  r = (255 - r).toString(16);\n  g = (255 - g).toString(16);\n  b = (255 - b).toString(16);\n  // pad each with zeros and return\n  return \"#\" + padZero(r) + padZero(g) + padZero(b);\n}\n\nfunction padZero(str, len) {\n  len = len || 2;\n  var zeros = new Array(len).join(\"0\");\n  return (zeros + str).slice(-len);\n}\n\nexport default function Tooltip({ legendColor, ...props }) {\n  return (\n    <RechartsTooltip\n      wrapperStyle={{ outline: \"none\" }}\n      isAnimationActive={false}\n      cursor={{ fill: \"#d1d5db\", opacity: \"0.15\" }}\n      position={{ y: 0 }}\n      {...props}\n      content={({ active, payload, label }) => {\n        return active && payload ? (\n          <div className=\"bg-theme-bg-primary text-sm rounded-md border shadow-lg\">\n            <div className=\"border-b py-2 px-4\">\n              <p className=\"text-theme-bg-primary font-medium\">{label}</p>\n            </div>\n            <div className=\"space-y-1 py-2 px-4\">\n              {payload.map(({ value, name }, idx) => (\n                <div\n                  key={`id-${idx}`}\n                  className=\"flex items-center justify-between space-x-8\"\n                >\n                  <div className=\"flex items-center space-x-2\">\n                    <span\n                      className=\"shrink-0 h-3 w-3 border-theme-bg-primary rounded-md rounded-full border-2 shadow-md\"\n                      style={{ backgroundColor: legendColor }}\n                    />\n                    <p\n                      style={{\n                        color: invertColor(legendColor, true),\n                      }}\n                      className=\"font-medium tabular-nums text-right whitespace-nowrap\"\n                    >\n                      {value}\n                    </p>\n                  </div>\n                  <p\n                    style={{\n                      color: invertColor(legendColor, true),\n                    }}\n                    className=\"whitespace-nowrap font-normal\"\n                  >\n                    {name}\n                  </p>\n                </div>\n              ))}\n            </div>\n          </div>\n        ) : null;\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Chartable/chart-utils.js",
    "content": "export const Colors = {\n  blue: \"#3b82f6\",\n  sky: \"#0ea5e9\",\n  cyan: \"#06b6d4\",\n  teal: \"#14b8a6\",\n  emerald: \"#10b981\",\n  green: \"#22c55e\",\n  lime: \"#84cc16\",\n  yellow: \"#eab308\",\n  amber: \"#f59e0b\",\n  orange: \"#f97316\",\n  red: \"#ef4444\",\n  rose: \"#f43f5e\",\n  pink: \"#ec4899\",\n  fuchsia: \"#d946ef\",\n  purple: \"#a855f7\",\n  violet: \"#8b5cf6\",\n  indigo: \"#6366f1\",\n  neutral: \"#737373\",\n  stone: \"#78716c\",\n  gray: \"#6b7280\",\n  slate: \"#64748b\",\n  zinc: \"#71717a\",\n};\n\nexport function getTremorColor(color) {\n  switch (color) {\n    case \"blue\":\n      return Colors.blue;\n    case \"sky\":\n      return Colors.sky;\n    case \"cyan\":\n      return Colors.cyan;\n    case \"teal\":\n      return Colors.teal;\n    case \"emerald\":\n      return Colors.emerald;\n    case \"green\":\n      return Colors.green;\n    case \"lime\":\n      return Colors.lime;\n    case \"yellow\":\n      return Colors.yellow;\n    case \"amber\":\n      return Colors.amber;\n    case \"orange\":\n      return Colors.orange;\n    case \"red\":\n      return Colors.red;\n    case \"rose\":\n      return Colors.rose;\n    case \"pink\":\n      return Colors.pink;\n    case \"fuchsia\":\n      return Colors.fuchsia;\n    case \"purple\":\n      return Colors.purple;\n    case \"violet\":\n      return Colors.violet;\n    case \"indigo\":\n      return Colors.indigo;\n    case \"neutral\":\n      return Colors.neutral;\n    case \"stone\":\n      return Colors.stone;\n    case \"gray\":\n      return Colors.gray;\n    case \"slate\":\n      return Colors.slate;\n    case \"zinc\":\n      return Colors.zinc;\n  }\n}\n\nexport const themeColorRange = [\n  \"slate\",\n  \"gray\",\n  \"zinc\",\n  \"neutral\",\n  \"stone\",\n  \"red\",\n  \"orange\",\n  \"amber\",\n  \"yellow\",\n  \"lime\",\n  \"green\",\n  \"emerald\",\n  \"teal\",\n  \"cyan\",\n  \"sky\",\n  \"blue\",\n  \"indigo\",\n  \"violet\",\n  \"purple\",\n  \"fuchsia\",\n  \"pink\",\n  \"rose\",\n];\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Chartable/index.jsx",
    "content": "import { v4 } from \"uuid\";\nimport {\n  AreaChart,\n  BarChart,\n  DonutChart,\n  Legend,\n  LineChart,\n} from \"@tremor/react\";\nimport {\n  Bar,\n  CartesianGrid,\n  ComposedChart,\n  Funnel,\n  FunnelChart,\n  Line,\n  PolarAngleAxis,\n  PolarGrid,\n  PolarRadiusAxis,\n  Radar,\n  RadarChart,\n  RadialBar,\n  RadialBarChart,\n  Scatter,\n  ScatterChart,\n  Treemap,\n  XAxis,\n  YAxis,\n} from \"recharts\";\nimport { Colors, getTremorColor } from \"./chart-utils.js\";\nimport CustomCell from \"./CustomCell.jsx\";\nimport Tooltip from \"./CustomTooltip.jsx\";\nimport { safeJsonParse } from \"@/utils/request.js\";\nimport renderMarkdown from \"@/utils/chat/markdown.js\";\nimport { memo, useCallback, useState } from \"react\";\nimport { saveAs } from \"file-saver\";\nimport { useGenerateImage } from \"recharts-to-png\";\nimport { CircleNotch, DownloadSimple } from \"@phosphor-icons/react\";\n\nconst dataFormatter = (number) => {\n  return Intl.NumberFormat(\"us\").format(number).toString();\n};\n\nexport function Chartable({ props }) {\n  const [getDivJpeg, { ref }] = useGenerateImage({\n    quality: 1,\n    type: \"image/jpeg\",\n    options: {\n      backgroundColor: \"#393d43\",\n      padding: 20,\n    },\n  });\n  const handleDownload = useCallback(async () => {\n    const jpeg = await getDivJpeg();\n    if (jpeg) saveAs(jpeg, `chart-${v4().split(\"-\")[0]}.jpg`);\n  }, []);\n\n  const color = null;\n  const showLegend = true;\n  const content =\n    typeof props.content === \"string\"\n      ? safeJsonParse(props.content, null)\n      : props.content;\n  if (content === null) return null;\n\n  const chartType = content?.type?.toLowerCase();\n  const data =\n    typeof content.dataset === \"string\"\n      ? safeJsonParse(content.dataset, [])\n      : content.dataset;\n  const value = data.length > 0 ? Object.keys(data[0])[1] : \"value\";\n  const title = content?.title;\n\n  const renderChart = () => {\n    switch (chartType) {\n      case \"area\":\n        return (\n          <div className=\"bg-theme-bg-primary p-8 rounded-xl text-white light:border light:border-theme-border-primary\">\n            <h3 className=\"text-lg text-theme-text-primary font-medium\">\n              {title}\n            </h3>\n            <AreaChart\n              className=\"h-[350px]\"\n              data={data}\n              index=\"name\"\n              categories={[value]}\n              colors={[color || \"blue\", \"cyan\"]}\n              showLegend={showLegend}\n              valueFormatter={dataFormatter}\n            />\n          </div>\n        );\n      case \"bar\":\n        return (\n          <div className=\"bg-theme-bg-primary p-8 rounded-xl text-white light:border light:border-theme-border-primary\">\n            <h3 className=\"text-lg text-theme-text-primary font-medium\">\n              {title}\n            </h3>\n            <BarChart\n              className=\"h-[350px]\"\n              data={data}\n              index=\"name\"\n              categories={[value]}\n              colors={[color || \"blue\"]}\n              showLegend={showLegend}\n              valueFormatter={dataFormatter}\n              layout={\"vertical\"}\n              yAxisWidth={100}\n            />\n          </div>\n        );\n      case \"line\":\n        return (\n          <div className=\"bg-theme-bg-primary p-8 pb-12 rounded-xl text-white h-[500px] w-full light:border light:border-theme-border-primary\">\n            <h3 className=\"text-lg text-theme-text-primary font-medium\">\n              {title}\n            </h3>\n            <LineChart\n              className=\"h-[400px]\"\n              data={data}\n              index=\"name\"\n              categories={[value]}\n              colors={[color || \"blue\"]}\n              showLegend={showLegend}\n              valueFormatter={dataFormatter}\n            />\n          </div>\n        );\n      case \"composed\":\n        return (\n          <div className=\"bg-theme-bg-primary p-8 rounded-xl text-white light:border light:border-theme-border-primary\">\n            <h3 className=\"text-lg text-theme-text-primary font-medium\">\n              {title}\n            </h3>\n            {showLegend && (\n              <Legend\n                categories={[value]}\n                colors={[color || \"blue\", color || \"blue\"]}\n                className=\"mb-5 justify-end\"\n              />\n            )}\n            <ComposedChart width={500} height={260} data={data}>\n              <CartesianGrid\n                strokeDasharray=\"3 3\"\n                horizontal\n                vertical={false}\n              />\n              <XAxis\n                dataKey=\"name\"\n                tickLine={false}\n                axisLine={false}\n                interval=\"preserveStartEnd\"\n                tick={{ transform: \"translate(0, 6)\", fill: \"white\" }}\n                style={{\n                  fontSize: \"12px\",\n                  fontFamily: \"Inter; Helvetica\",\n                }}\n                padding={{ left: 10, right: 10 }}\n              />\n              <YAxis\n                tickLine={false}\n                axisLine={false}\n                type=\"number\"\n                tick={{ transform: \"translate(-3, 0)\", fill: \"white\" }}\n                style={{\n                  fontSize: \"12px\",\n                  fontFamily: \"Inter; Helvetica\",\n                }}\n              />\n              <Tooltip legendColor={getTremorColor(color || \"blue\")} />\n              <Line\n                type=\"linear\"\n                dataKey={value}\n                stroke={getTremorColor(color || \"blue\")}\n                dot={false}\n                strokeWidth={2}\n              />\n              <Bar\n                dataKey=\"value\"\n                name=\"value\"\n                type=\"linear\"\n                fill={getTremorColor(color || \"blue\")}\n              />\n            </ComposedChart>\n          </div>\n        );\n      case \"scatter\":\n        return (\n          <div className=\"bg-theme-bg-primary p-8 rounded-xl text-white light:border light:border-theme-border-primary\">\n            <h3 className=\"text-lg text-theme-text-primary font-medium\">\n              {title}\n            </h3>\n            {showLegend && (\n              <div className=\"flex justify-end\">\n                <Legend\n                  categories={[value]}\n                  colors={[color || \"blue\", color || \"blue\"]}\n                  className=\"mb-5\"\n                />\n              </div>\n            )}\n            <ScatterChart width={500} height={260} data={data}>\n              <CartesianGrid\n                strokeDasharray=\"3 3\"\n                horizontal\n                vertical={false}\n              />\n              <XAxis\n                dataKey=\"name\"\n                tickLine={false}\n                axisLine={false}\n                interval=\"preserveStartEnd\"\n                tick={{ transform: \"translate(0, 6)\", fill: \"white\" }}\n                style={{\n                  fontSize: \"12px\",\n                  fontFamily: \"Inter; Helvetica\",\n                }}\n                padding={{ left: 10, right: 10 }}\n              />\n              <YAxis\n                tickLine={false}\n                axisLine={false}\n                type=\"number\"\n                tick={{ transform: \"translate(-3, 0)\", fill: \"white\" }}\n                style={{\n                  fontSize: \"12px\",\n                  fontFamily: \"Inter; Helvetica\",\n                }}\n              />\n              <Tooltip legendColor={getTremorColor(color || \"blue\")} />\n              <Scatter dataKey={value} fill={getTremorColor(color || \"blue\")} />\n            </ScatterChart>\n          </div>\n        );\n      case \"pie\":\n        return (\n          <div className=\"bg-theme-bg-primary p-8 rounded-xl text-white light:border light:border-theme-border-primary\">\n            <h3 className=\"text-lg text-theme-text-primary font-medium\">\n              {title}\n            </h3>\n            <DonutChart\n              data={data}\n              category={value}\n              index=\"name\"\n              colors={[\n                color || \"cyan\",\n                \"violet\",\n                \"rose\",\n                \"amber\",\n                \"emerald\",\n                \"teal\",\n                \"fuchsia\",\n              ]}\n              // No actual legend for pie chart, but this will toggle the central text\n              showLabel={showLegend}\n              valueFormatter={dataFormatter}\n              customTooltip={customTooltip}\n            />\n          </div>\n        );\n      case \"radar\":\n        return (\n          <div className=\"bg-theme-bg-primary p-8 rounded-xl text-white light:border light:border-theme-border-primary\">\n            <h3 className=\"text-lg text-theme-text-primary font-medium\">\n              {title}\n            </h3>\n            {showLegend && (\n              <div className=\"flex justify-end\">\n                <Legend\n                  categories={[value]}\n                  colors={[color || \"blue\", color || \"blue\"]}\n                  className=\"mb-5\"\n                />\n              </div>\n            )}\n            <RadarChart\n              cx={300}\n              cy={250}\n              outerRadius={150}\n              width={600}\n              height={500}\n              data={data}\n            >\n              <PolarGrid />\n              <PolarAngleAxis dataKey=\"name\" tick={{ fill: \"white\" }} />\n              <PolarRadiusAxis tick={{ fill: \"white\" }} />\n              <Tooltip legendColor={getTremorColor(color || \"blue\")} />\n              <Radar\n                dataKey=\"value\"\n                stroke={getTremorColor(color || \"blue\")}\n                fill={getTremorColor(color || \"blue\")}\n                fillOpacity={0.6}\n              />\n            </RadarChart>\n          </div>\n        );\n      case \"radialbar\":\n        return (\n          <div className=\"bg-theme-bg-primary p-8 rounded-xl text-white light:border light:border-theme-border-primary\">\n            <h3 className=\"text-lg text-theme-text-primary font-medium\">\n              {title}\n            </h3>\n            {showLegend && (\n              <div className=\"flex justify-end\">\n                <Legend\n                  categories={[value]}\n                  colors={[color || \"blue\", color || \"blue\"]}\n                  className=\"mb-5\"\n                />\n              </div>\n            )}\n            <RadialBarChart\n              width={500}\n              height={300}\n              cx={150}\n              cy={150}\n              innerRadius={20}\n              outerRadius={140}\n              barSize={10}\n              data={data}\n            >\n              <RadialBar\n                angleAxisId={15}\n                label={{\n                  position: \"insideStart\",\n                  fill: getTremorColor(color || \"blue\"),\n                }}\n                dataKey=\"value\"\n              />\n              <Tooltip legendColor={getTremorColor(color || \"blue\")} />\n            </RadialBarChart>\n          </div>\n        );\n      case \"treemap\":\n        return (\n          <div className=\"bg-theme-bg-primary p-8 rounded-xl text-white light:border light:border-theme-border-primary\">\n            <h3 className=\"text-lg text-theme-text-primary font-medium\">\n              {title}\n            </h3>\n            {showLegend && (\n              <div className=\"flex justify-end\">\n                <Legend\n                  categories={[value]}\n                  colors={[color || \"blue\", color || \"blue\"]}\n                  className=\"mb-5\"\n                />\n              </div>\n            )}\n            <Treemap\n              width={500}\n              height={260}\n              data={data}\n              dataKey=\"value\"\n              stroke=\"#fff\"\n              fill={getTremorColor(color || \"blue\")}\n              content={<CustomCell colors={Object.values(Colors)} />}\n            >\n              <Tooltip legendColor={getTremorColor(color || \"blue\")} />\n            </Treemap>\n          </div>\n        );\n      case \"funnel\":\n        return (\n          <div className=\"bg-theme-bg-primary p-8 rounded-xl text-white light:border light:border-theme-border-primary\">\n            <h3 className=\"text-lg text-theme-text-primary font-medium\">\n              {title}\n            </h3>\n            {showLegend && (\n              <div className=\"flex justify-end\">\n                <Legend\n                  categories={[value]}\n                  colors={[color || \"blue\", color || \"blue\"]}\n                  className=\"mb-5\"\n                />\n              </div>\n            )}\n            <FunnelChart width={500} height={300} data={data}>\n              <Tooltip legendColor={getTremorColor(color || \"blue\")} />\n              <Funnel dataKey=\"value\" color={getTremorColor(color || \"blue\")} />\n            </FunnelChart>\n          </div>\n        );\n      default:\n        return <p>Unsupported chart type.</p>;\n    }\n  };\n\n  if (!!props.chatId) {\n    return (\n      <div className=\"flex justify-start w-full\">\n        <div className=\"py-2 px-4 w-full flex flex-col md:max-w-[80%]\">\n          <div className=\"relative w-full\">\n            <DownloadGraph onClick={handleDownload} />\n            <div ref={ref}>{renderChart()}</div>\n            <span\n              className=\"flex flex-col gap-y-1 mt-2\"\n              dangerouslySetInnerHTML={{\n                __html: renderMarkdown(content.caption),\n              }}\n            />\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex justify-start w-full\">\n      <div className=\"py-2 px-4 w-full flex flex-col md:max-w-[80%]\">\n        <div className=\"relative w-full\">\n          <DownloadGraph onClick={handleDownload} />\n          <div ref={ref}>{renderChart()}</div>\n        </div>\n        <span\n          className=\"flex flex-col gap-y-1 mt-2\"\n          dangerouslySetInnerHTML={{\n            __html: renderMarkdown(content.caption),\n          }}\n        />\n      </div>\n    </div>\n  );\n}\n\nconst customTooltip = (props) => {\n  const { payload, active } = props;\n  if (!active || !payload) return null;\n  const categoryPayload = payload?.[0];\n  if (!categoryPayload) return null;\n  return (\n    <div className=\"w-56 bg-theme-bg-primary rounded-lg border p-2 text-white\">\n      <div className=\"flex flex-1 space-x-2.5\">\n        <div\n          className={`flex w-1.5 flex-col bg-${categoryPayload?.color}-500 rounded`}\n        />\n        <div className=\"w-full\">\n          <div className=\"flex items-center justify-between space-x-8\">\n            <p className=\"whitespace-nowrap text-right text-tremor-content\">\n              {categoryPayload.name}\n            </p>\n            <p className=\"whitespace-nowrap text-right font-medium text-tremor-content-emphasis\">\n              {categoryPayload.value}\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nfunction DownloadGraph({ onClick }) {\n  const [loading, setLoading] = useState(false);\n  const handleClick = async () => {\n    setLoading(true);\n    await onClick?.();\n    setLoading(false);\n  };\n\n  return (\n    <div className=\"absolute top-3 right-3 z-50 cursor-pointer\">\n      <div className=\"flex flex-col items-center\">\n        <div className=\"p-1 rounded-full border-none\">\n          {loading ? (\n            <CircleNotch\n              className=\"text-theme-text-primary w-5 h-5 animate-spin\"\n              aria-label=\"Downloading image...\"\n            />\n          ) : (\n            <DownloadSimple\n              weight=\"bold\"\n              className=\"text-theme-text-primary w-5 h-5 hover:text-theme-text-primary\"\n              onClick={handleClick}\n              aria-label=\"Download graph image\"\n            />\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default memo(Chartable);\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/Citation/index.jsx",
    "content": "import { Fragment, useState, useEffect } from \"react\";\nimport { decode as HTMLDecode } from \"he\";\nimport truncate from \"truncate\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport {\n  FileText,\n  Info,\n  ArrowSquareOut,\n  GithubLogo,\n  X,\n  YoutubeLogo,\n  LinkSimple,\n  GitlabLogo,\n} from \"@phosphor-icons/react\";\nimport { toPercentString } from \"@/utils/numbers\";\nimport { useTranslation } from \"react-i18next\";\nimport { useSourcesSidebar } from \"../../SourcesSidebar\";\n\nconst CIRCLE_ICONS = {\n  file: FileText,\n  link: LinkSimple,\n  youtube: YoutubeLogo,\n  github: GithubLogo,\n  gitlab: GitlabLogo,\n  confluence: LinkSimple,\n  drupalwiki: FileText,\n  obsidian: FileText,\n  paperlessNgx: FileText,\n};\n\n/**\n * Renders a circle with a source type icon inside, or a favicon if URL is provided.\n * @param {\"file\"|\"link\"|\"youtube\"|\"github\"|\"gitlab\"|\"confluence\"|\"drupalwiki\"|\"obsidian\"|\"paperlessNgx\"} props.type\n * @param {number} [props.size] - Circle diameter in px\n * @param {number} [props.iconSize] - Icon size in px\n * @param {string} [props.url] - Optional URL to fetch favicon from\n */\nexport function SourceTypeCircle({\n  type = \"file\",\n  size = 22,\n  iconSize = 12,\n  url = null,\n}) {\n  const Icon = CIRCLE_ICONS[type] || CIRCLE_ICONS.file;\n  const [imgError, setImgError] = useState(false);\n\n  let faviconUrl = null;\n  if (type === \"link\" && url) {\n    try {\n      const hostname = new URL(url).hostname;\n      faviconUrl = `https://www.google.com/s2/favicons?domain=${hostname}&sz=64`;\n    } catch {\n      faviconUrl = null;\n    }\n  }\n\n  useEffect(() => {\n    setImgError(false);\n  }, [url]);\n\n  return (\n    <div\n      className=\"bg-white light:bg-slate-100 rounded-full flex items-center justify-center overflow-hidden\"\n      style={{ width: size, height: size }}\n    >\n      {faviconUrl && !imgError ? (\n        <img\n          src={faviconUrl}\n          alt=\"favicon\"\n          style={{ width: size, height: size }}\n          className=\"object-cover\"\n          onError={() => setImgError(true)}\n        />\n      ) : (\n        <Icon size={iconSize} weight=\"bold\" className=\"text-black\" />\n      )}\n    </div>\n  );\n}\n\nexport function combineLikeSources(sources) {\n  const combined = {};\n  sources.forEach((source) => {\n    const { id, title, text, chunkSource = \"\", score = null } = source;\n    if (combined.hasOwnProperty(title)) {\n      combined[title].chunks.push({ id, text, chunkSource, score });\n      combined[title].references += 1;\n    } else {\n      combined[title] = {\n        title,\n        chunks: [{ id, text, chunkSource, score }],\n        references: 1,\n      };\n    }\n  });\n  return Object.values(combined);\n}\n\nexport default function Citations({ sources = [] }) {\n  const {\n    sidebarOpen,\n    openSidebar,\n    closeSidebar,\n    sources: currentSources,\n  } = useSourcesSidebar();\n  const { t } = useTranslation();\n  if (sources.length === 0) return null;\n\n  const combined = combineLikeSources(sources);\n  const visibleSources = combined.slice(0, 3);\n  const remainingCount = Math.max(0, combined.length - 3);\n\n  function handleOpenSourcesSidebar() {\n    if (sidebarOpen && sources === currentSources) {\n      closeSidebar();\n    } else {\n      openSidebar(sources);\n    }\n  }\n\n  return (\n    <button\n      onClick={handleOpenSourcesSidebar}\n      className=\"w-fit flex items-center gap-[5px] px-[10px] py-[4px] rounded-full hover:bg-white/5 light:hover:bg-black/5 transition-colors\"\n      type=\"button\"\n    >\n      <span className=\"text-xs text-white light:text-slate-800\">\n        {t(\"chat_window.sources\")}\n      </span>\n      <div\n        className=\"relative h-[22px]\"\n        style={{ width: `${visibleSources.length * 17 + 5}px` }}\n      >\n        {visibleSources.map((source, idx) => {\n          const info = parseChunkSource(source);\n          return (\n            <div\n              key={source.title || idx}\n              className=\"absolute top-0 size-[22px] rounded-full border-2 border-zinc-800 light:border-white\"\n              style={{ left: `${idx * 17}px`, zIndex: 3 - idx }}\n            >\n              <SourceTypeCircle\n                type={info.icon}\n                size={18}\n                iconSize={10}\n                url={info.href}\n              />\n            </div>\n          );\n        })}\n      </div>\n      {remainingCount > 0 && (\n        <span className=\"text-xs text-white light:text-slate-800\">\n          + {remainingCount}\n        </span>\n      )}\n    </button>\n  );\n}\n\nexport function omitChunkHeader(text) {\n  if (!text.includes(\"<document_metadata>\")) return text;\n  return text.split(\"</document_metadata>\")[1].trim();\n}\n\nexport function CitationDetailModal({ source, onClose }) {\n  const { references, title, chunks } = source;\n  const { isUrl, text: webpageUrl, href: linkTo } = parseChunkSource(source);\n  const { t } = useTranslation();\n\n  return (\n    <ModalWrapper isOpen={!!source}>\n      <div className=\"w-full max-w-2xl bg-zinc-900 light:bg-white rounded-lg shadow border-2 border-zinc-700 light:border-slate-300 overflow-hidden\">\n        <div className=\"relative p-6 border-b rounded-t border-zinc-700 light:border-slate-300\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            {isUrl ? (\n              <a\n                href={linkTo}\n                target=\"_blank\"\n                rel=\"noreferrer\"\n                className=\"text-xl w-[90%] font-semibold text-white light:text-slate-900 whitespace-nowrap hover:underline hover:text-blue-300 light:hover:text-blue-600 flex items-center gap-x-1\"\n              >\n                <div className=\"flex items-center gap-x-1 max-w-full overflow-hidden\">\n                  <h3 className=\"truncate text-ellipsis whitespace-nowrap overflow-hidden w-full\">\n                    {webpageUrl}\n                  </h3>\n                  <ArrowSquareOut className=\"flex-shrink-0\" />\n                </div>\n              </a>\n            ) : (\n              <h3 className=\"text-xl font-semibold text-white light:text-slate-900 overflow-hidden overflow-ellipsis whitespace-nowrap\">\n                {truncate(title, 45)}\n              </h3>\n            )}\n          </div>\n          {references > 1 && (\n            <p className=\"text-xs text-zinc-400 light:text-slate-500 mt-2\">\n              Referenced {references} times.\n            </p>\n          )}\n          <button\n            onClick={onClose}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-zinc-700 light:hover:bg-slate-200 border-transparent border\"\n          >\n            <X\n              size={24}\n              weight=\"bold\"\n              className=\"text-white light:text-slate-900\"\n            />\n          </button>\n        </div>\n        <div\n          className=\"h-full w-full overflow-y-auto\"\n          style={{ maxHeight: \"calc(100vh - 200px)\" }}\n        >\n          <div className=\"py-7 px-9 space-y-2 flex-col\">\n            {chunks.map(({ text, score }, idx) => (\n              <Fragment key={idx}>\n                <div className=\"pt-6 text-white light:text-slate-900\">\n                  <div className=\"flex flex-col w-full justify-start pb-6 gap-y-1\">\n                    <p className=\"text-white light:text-slate-900 whitespace-pre-line\">\n                      {HTMLDecode(omitChunkHeader(text))}\n                    </p>\n\n                    {!!score && (\n                      <div className=\"w-full flex items-center text-xs text-white/60 light:text-slate-500 gap-x-2 cursor-default\">\n                        <div\n                          data-tooltip-id=\"similarity-score\"\n                          data-tooltip-content={`This is the semantic similarity score of this chunk of text compared to your query calculated by the vector database.`}\n                          className=\"flex items-center gap-x-1\"\n                        >\n                          <Info size={14} />\n                          <p>\n                            {toPercentString(score)}{\" \"}\n                            {t(\"chat_window.similarity_match\")}\n                          </p>\n                        </div>\n                      </div>\n                    )}\n                  </div>\n                </div>\n                {idx !== chunks.length - 1 && (\n                  <hr className=\"border-zinc-700 light:border-slate-300\" />\n                )}\n              </Fragment>\n            ))}\n            <div className=\"mb-6\"></div>\n          </div>\n        </div>\n      </div>\n    </ModalWrapper>\n  );\n}\n\nconst supportedSources = [\n  \"link://\",\n  \"confluence://\",\n  \"github://\",\n  \"gitlab://\",\n  \"drupalwiki://\",\n  \"youtube://\",\n  \"obsidian://\",\n  \"paperless-ngx://\",\n];\n\n/**\n * Parses the chunk source to get the correct title and/or display text for citations\n * which contain valid outbound links that can be clicked by the\n * user when viewing a citation. Optionally allows various icons\n * to show distinct types of sources.\n * @param {{title: string, chunks: {text: string, chunkSource: string}[]}} options\n * @returns {{isUrl: boolean, text: string, href: string, icon: string}}\n */\nexport function parseChunkSource({ title = \"\", chunks = [] }) {\n  const nullResponse = {\n    isUrl: false,\n    text: null,\n    href: null,\n    icon: \"file\",\n  };\n\n  if (\n    !chunks.length ||\n    !supportedSources.some((source) =>\n      chunks[0].chunkSource?.startsWith(source)\n    )\n  )\n    return nullResponse;\n\n  try {\n    const sourceID = supportedSources.find((source) =>\n      chunks[0].chunkSource?.startsWith(source)\n    );\n    let url, text, icon;\n\n    // Try to parse the URL from the chunk source\n    // If it fails, we'll use the title as the text and the link icon\n    // but the document will not be linkable\n    try {\n      url = new URL(chunks[0].chunkSource.split(sourceID)[1]);\n    } catch {}\n\n    switch (sourceID) {\n      case \"link://\":\n        text = url.host + url.pathname;\n        icon = \"link\";\n        break;\n\n      case \"youtube://\":\n        text = title;\n        icon = \"youtube\";\n        break;\n\n      case \"github://\":\n        text = title;\n        icon = \"github\";\n        break;\n\n      case \"gitlab://\":\n        text = title;\n        icon = \"gitlab\";\n        break;\n\n      case \"confluence://\":\n        text = title;\n        icon = \"confluence\";\n        break;\n\n      case \"drupalwiki://\":\n        text = title;\n        icon = \"drupalwiki\";\n        break;\n\n      case \"obsidian://\":\n        text = title;\n        icon = \"obsidian\";\n        break;\n\n      case \"paperless-ngx://\":\n        text = title;\n        icon = \"paperlessNgx\";\n        break;\n\n      default:\n        text = url.host + url.pathname;\n        icon = \"link\";\n        break;\n    }\n\n    return {\n      isUrl: !!url,\n      href: url?.toString() ?? \"#\",\n      text,\n      icon,\n    };\n  } catch (err) {\n    console.warn(`Unsupported source identifier ${chunks[0].chunkSource}`, err);\n  }\n  return nullResponse;\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/ActionMenu/index.jsx",
    "content": "import React, { useState, useEffect, useRef } from \"react\";\nimport { Trash, DotsThreeVertical, TreeView } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\n\nfunction ActionMenu({ chatId, forkThread, isEditing, role }) {\n  const { t } = useTranslation();\n  const [open, setOpen] = useState(false);\n  const menuRef = useRef(null);\n\n  const toggleMenu = () => setOpen(!open);\n\n  const handleFork = () => {\n    forkThread(chatId);\n    setOpen(false);\n  };\n\n  const handleDelete = () => {\n    window.dispatchEvent(\n      new CustomEvent(\"delete-message\", { detail: { chatId } })\n    );\n    setOpen(false);\n  };\n\n  useEffect(() => {\n    const handleClickOutside = (event) => {\n      if (menuRef.current && !menuRef.current.contains(event.target)) {\n        setOpen(false);\n      }\n    };\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, []);\n\n  if (!chatId || isEditing || role === \"user\") return null;\n\n  return (\n    <div className=\"mt-2 -ml-0.5 relative\" ref={menuRef}>\n      <button\n        onClick={toggleMenu}\n        className=\"border-none text-zinc-300 light:text-slate-500 transition-colors duration-200\"\n        data-tooltip-id=\"action-menu\"\n        data-tooltip-content={t(\"chat_window.more_actions\")}\n        aria-label={t(\"chat_window.more_actions\")}\n      >\n        <DotsThreeVertical size={24} weight=\"bold\" />\n      </button>\n      {open && (\n        <div className=\"absolute -top-1 left-7 mt-1 border-[1.5px] border-white/40 rounded-lg bg-theme-action-menu-bg flex flex-col shadow-[0_4px_14px_rgba(0,0,0,0.25)] text-white z-99 md:z-10\">\n          <button\n            onClick={handleFork}\n            className=\"border-none rounded-t-lg flex items-center text-white gap-x-2 hover:bg-theme-action-menu-item-hover py-1.5 px-2 transition-colors duration-200 w-full text-left\"\n          >\n            <TreeView size={18} />\n            <span className=\"text-sm\">{t(\"chat_window.fork\")}</span>\n          </button>\n          <button\n            onClick={handleDelete}\n            className=\"border-none flex rounded-b-lg items-center text-white gap-x-2 hover:bg-theme-action-menu-item-hover py-1.5 px-2 transition-colors duration-200 w-full text-left\"\n          >\n            <Trash size={18} />\n            <span className=\"text-sm\">{t(\"chat_window.delete\")}</span>\n          </button>\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default ActionMenu;\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/DeleteMessage/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { Trash } from \"@phosphor-icons/react\";\nimport Workspace from \"@/models/workspace\";\n\nconst DELETE_EVENT = \"delete-message\";\n\nexport function useWatchDeleteMessage({ chatId = null, role = \"user\" }) {\n  const [isDeleted, setIsDeleted] = useState(false);\n  const [completeDelete, setCompleteDelete] = useState(false);\n\n  useEffect(() => {\n    function listenForEvent() {\n      if (!chatId) return;\n      window.addEventListener(DELETE_EVENT, onDeleteEvent);\n    }\n    listenForEvent();\n    return () => {\n      window.removeEventListener(DELETE_EVENT, onDeleteEvent);\n    };\n  }, [chatId]);\n\n  function onEndAnimation() {\n    if (!isDeleted) return;\n    setCompleteDelete(true);\n  }\n\n  async function onDeleteEvent(e) {\n    if (e.detail.chatId === chatId) {\n      setIsDeleted(true);\n      // Do this to prevent double-emission of the PUT/DELETE api call\n      // because then there will be a race condition and it will make an error log for nothing\n      // as one call will complete and the other will fail.\n      if (role === \"assistant\") await Workspace.deleteChat(chatId);\n      return false;\n    }\n  }\n\n  return { isDeleted, completeDelete, onEndAnimation };\n}\n\nexport function DeleteMessage({ chatId, isEditing, role }) {\n  if (!chatId || isEditing || role === \"user\") return null;\n\n  function emitDeleteEvent() {\n    window.dispatchEvent(new CustomEvent(DELETE_EVENT, { detail: { chatId } }));\n  }\n\n  return (\n    <button\n      onClick={emitDeleteEvent}\n      className=\"border-none flex items-center gap-x-1 w-full\"\n      role=\"menuitem\"\n    >\n      <Trash size={21} weight=\"fill\" />\n      <p>Delete</p>\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx",
    "content": "import { Info, Pencil } from \"@phosphor-icons/react\";\nimport { useState, useEffect, useRef } from \"react\";\nimport Appearance from \"@/models/appearance\";\nimport { useTranslation } from \"react-i18next\";\n\nconst EDIT_EVENT = \"toggle-message-edit\";\n\nexport function useEditMessage({ chatId, role }) {\n  const [isEditing, setIsEditing] = useState(false);\n\n  function onEditEvent(e) {\n    if (e.detail.chatId !== chatId || e.detail.role !== role) {\n      setIsEditing(false);\n      return false;\n    }\n    setIsEditing((prev) => !prev);\n  }\n\n  useEffect(() => {\n    function listenForEdits() {\n      if (!chatId || !role) return;\n      window.addEventListener(EDIT_EVENT, onEditEvent);\n    }\n    listenForEdits();\n    return () => {\n      window.removeEventListener(EDIT_EVENT, onEditEvent);\n    };\n  }, [chatId, role]);\n\n  return { isEditing, setIsEditing };\n}\n\nexport function EditMessageAction({ chatId = null, role, isEditing }) {\n  const { t } = useTranslation();\n  function handleEditClick() {\n    window.dispatchEvent(\n      new CustomEvent(EDIT_EVENT, { detail: { chatId, role } })\n    );\n  }\n\n  if (!chatId || isEditing) return null;\n  return (\n    <div\n      className={`mt-3 relative ${\n        role === \"user\" && !isEditing ? \"\" : \"!opacity-100\"\n      }`}\n    >\n      <button\n        onClick={handleEditClick}\n        data-tooltip-id=\"edit-input-text\"\n        data-tooltip-content={`${\n          role === \"user\"\n            ? t(\"chat_window.edit_prompt\")\n            : t(\"chat_window.edit_response\")\n        } `}\n        className=\"border-none text-zinc-300 light:text-slate-500\"\n        aria-label={`Edit ${role === \"user\" ? t(\"chat_window.edit_prompt\") : t(\"chat_window.edit_response\")}`}\n      >\n        <Pencil size={21} className=\"mb-1\" />\n      </button>\n    </div>\n  );\n}\n\nexport function EditMessageForm({\n  role,\n  chatId,\n  message,\n  attachments = [],\n  adjustTextArea,\n  saveChanges,\n}) {\n  const formRef = useRef(null);\n\n  function handleSubmit(e) {\n    e.preventDefault();\n    const editedMessage = formRef.current.value;\n    saveChanges({ editedMessage, chatId, role, attachments });\n    window.dispatchEvent(\n      new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } })\n    );\n  }\n\n  function handleSave() {\n    const editedMessage = formRef.current.value;\n    saveChanges({\n      editedMessage,\n      chatId,\n      role,\n      attachments,\n      saveOnly: true,\n    });\n    window.dispatchEvent(\n      new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } })\n    );\n  }\n\n  function cancelEdits() {\n    window.dispatchEvent(\n      new CustomEvent(EDIT_EVENT, { detail: { chatId, role, attachments } })\n    );\n    return false;\n  }\n\n  useEffect(() => {\n    if (!formRef?.current) return;\n    formRef.current.focus();\n    adjustTextArea({ target: formRef.current });\n  }, []);\n\n  if (role === \"user\") {\n    return (\n      <form\n        onSubmit={handleSubmit}\n        className=\"flex flex-col w-full max-w-[650px]\"\n      >\n        <textarea\n          ref={formRef}\n          name=\"editedMessage\"\n          spellCheck={Appearance.get(\"enableSpellCheck\")}\n          className=\"text-white light:text-slate-900 w-full rounded-2xl bg-zinc-800 light:bg-slate-100 border border-sky-300 focus:border-sky-300 active:outline-none focus:outline-none focus:ring-0 px-4 py-3 resize-none overflow-hidden\"\n          defaultValue={message}\n          onChange={adjustTextArea}\n        />\n        <EditActionBar\n          onCancel={cancelEdits}\n          onSave={handleSave}\n          isUserMessage\n        />\n      </form>\n    );\n  }\n\n  return (\n    <form\n      onSubmit={handleSubmit}\n      className=\"flex flex-col w-full max-w-[650px]\"\n    >\n      <textarea\n        ref={formRef}\n        name=\"editedMessage\"\n        spellCheck={Appearance.get(\"enableSpellCheck\")}\n        className=\"text-white light:text-slate-900 w-full rounded-2xl bg-zinc-800 light:bg-slate-100 border border-sky-300 focus:border-sky-300 active:outline-none focus:outline-none focus:ring-0 px-4 py-3 resize-none overflow-hidden\"\n        defaultValue={message}\n        onChange={adjustTextArea}\n      />\n      <EditActionBar onCancel={cancelEdits} />\n    </form>\n  );\n}\n\nfunction EditActionBar({ onCancel, onSave, isUserMessage = false }) {\n  const { t } = useTranslation();\n  return (\n    <div className=\"mt-2 flex flex-col md:flex-row md:items-center justify-between gap-2 bg-zinc-800 light:bg-slate-200 rounded-lg p-2\">\n      <div className=\"flex items-start gap-2\">\n        <Info\n          size={12}\n          className=\"shrink-0 mt-0.5 text-zinc-200 light:text-slate-800\"\n        />\n        <span className=\"text-zinc-200 light:text-slate-800 text-xs leading-4\">\n          {isUserMessage\n            ? t(\"chat_window.edit_info_user\")\n            : t(\"chat_window.edit_info_assistant\")}\n        </span>\n      </div>\n      <div className=\"flex items-center gap-2 self-end shrink-0\">\n        <button\n          type=\"button\"\n          onClick={onCancel}\n          className=\"border-none text-white light:text-slate-900 text-sm font-medium w-[70px] h-9 rounded-lg hover:bg-white/5 light:hover:bg-slate-300\"\n        >\n          {t(\"chat_window.cancel\")}\n        </button>\n        {isUserMessage && (\n          <button\n            type=\"button\"\n            onClick={onSave}\n            className=\"border border-zinc-600 light:border-slate-600 text-white light:text-slate-900 text-sm font-medium w-[70px] h-9 rounded-lg hover:bg-white/5 light:hover:bg-slate-300\"\n          >\n            {t(\"chat_window.save\")}\n          </button>\n        )}\n        <button\n          type=\"submit\"\n          className=\"border-none bg-zinc-50 light:bg-slate-800 text-zinc-800 light:text-white text-sm font-medium w-[70px] h-9 rounded-lg hover:bg-zinc-200 light:hover:bg-slate-800\"\n        >\n          {isUserMessage ? t(\"chat_window.submit\") : t(\"chat_window.save\")}\n        </button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/RenderMetrics/index.jsx",
    "content": "import { formatDateTimeAsMoment } from \"@/utils/directories\";\nimport { numberWithCommas } from \"@/utils/numbers\";\nimport React, { useEffect, useState, useContext } from \"react\";\nimport { isMobile } from \"react-device-detect\";\nconst MetricsContext = React.createContext();\nconst SHOW_METRICS_KEY = \"anythingllm_show_chat_metrics\";\nconst SHOW_METRICS_EVENT = \"anythingllm_show_metrics_change\";\n\n/**\n * @param {number} duration - duration in milliseconds\n * @returns {string}\n */\nfunction formatDuration(duration) {\n  try {\n    return duration < 1\n      ? `${(duration * 1000).toFixed(0)}ms`\n      : `${duration.toFixed(3)}s`;\n  } catch {\n    return \"\";\n  }\n}\n\n/**\n * Format the output TPS to a string\n * @param {number} outputTps - output TPS\n * @returns {string}\n */\nfunction formatTps(outputTps) {\n  try {\n    return outputTps < 1000\n      ? outputTps.toFixed(2)\n      : numberWithCommas(outputTps.toFixed(0));\n  } catch {\n    return \"\";\n  }\n}\n\n/**\n * Get the show metrics setting from localStorage `anythingllm_show_chat_metrics` key\n * @returns {boolean}\n */\nfunction getAutoShowMetrics() {\n  return window?.localStorage?.getItem(SHOW_METRICS_KEY) === \"true\";\n}\n\n/**\n * Build the metrics string for a given metrics object\n * - Model name\n * - Duration and output TPS\n * - Timestamp\n * @param {metrics: {duration:number, outputTps: number, model?: string, timestamp?: number}} metrics\n * @returns {string}\n */\nfunction buildMetricsString(metrics = {}) {\n  return [\n    metrics?.model ? metrics.model : \"\",\n    `${formatDuration(metrics.duration)} (${formatTps(metrics.outputTps)} tok/s)`,\n    metrics?.timestamp\n      ? formatDateTimeAsMoment(metrics.timestamp, \"MMM D, h:mm A\")\n      : \"\",\n  ]\n    .filter(Boolean)\n    .join(\" · \");\n}\n\n/**\n * Toggle the show metrics setting in localStorage `anythingllm_show_chat_metrics` key\n * @returns {void}\n */\nfunction toggleAutoShowMetrics() {\n  const currentValue = getAutoShowMetrics() || false;\n  window?.localStorage?.setItem(SHOW_METRICS_KEY, !currentValue);\n  window.dispatchEvent(\n    new CustomEvent(SHOW_METRICS_EVENT, {\n      detail: { showMetricsAutomatically: !currentValue },\n    })\n  );\n  return !currentValue;\n}\n\n/**\n * Provider for the metrics context that controls the visibility of the metrics\n * per-chat based on the user's preference.\n * @param {React.ReactNode} children\n * @returns {React.ReactNode}\n */\nexport function MetricsProvider({ children }) {\n  const [showMetricsAutomatically, setShowMetricsAutomatically] =\n    useState(getAutoShowMetrics());\n\n  useEffect(() => {\n    function handleShowingMetricsEvent(e) {\n      if (!e?.detail?.hasOwnProperty(\"showMetricsAutomatically\")) return;\n      setShowMetricsAutomatically(e.detail.showMetricsAutomatically);\n    }\n    console.log(\"Adding event listener for metrics visibility\");\n    window.addEventListener(SHOW_METRICS_EVENT, handleShowingMetricsEvent);\n    return () =>\n      window.removeEventListener(SHOW_METRICS_EVENT, handleShowingMetricsEvent);\n  }, []);\n\n  return (\n    <MetricsContext.Provider\n      value={{ showMetricsAutomatically, setShowMetricsAutomatically }}\n    >\n      {children}\n    </MetricsContext.Provider>\n  );\n}\n\n/**\n * Render the metrics for a given chat, if available\n * @param {metrics: {duration:number, outputTps: number, model: string, timestamp: number}} props\n * @returns\n */\nexport default function RenderMetrics({ metrics = {} }) {\n  // Inherit the showMetricsAutomatically state from the MetricsProvider so the state is shared across all chats\n  const { showMetricsAutomatically, setShowMetricsAutomatically } =\n    useContext(MetricsContext);\n  if (!metrics?.duration || !metrics?.outputTps || isMobile) return null;\n\n  return (\n    <button\n      type=\"button\"\n      onClick={() => setShowMetricsAutomatically(toggleAutoShowMetrics())}\n      data-tooltip-id=\"metrics-visibility\"\n      data-tooltip-content={\n        showMetricsAutomatically\n          ? \"Click to only show metrics when hovering\"\n          : \"Click to show metrics as soon as they are available\"\n      }\n      className={`border-none flex md:justify-end items-center gap-x-[8px] -ml-7 ${showMetricsAutomatically ? \"opacity-100\" : \"opacity-0\"} md:group-hover:opacity-100 transition-all duration-300`}\n    >\n      <p className=\"cursor-pointer text-xs font-mono text-zinc-400 light:text-slate-500\">\n        {buildMetricsString(metrics)}\n      </p>\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/asyncTts.jsx",
    "content": "import { useEffect, useState, useRef } from \"react\";\nimport { SpeakerHigh, PauseCircle, CircleNotch } from \"@phosphor-icons/react\";\nimport Workspace from \"@/models/workspace\";\nimport showToast from \"@/utils/toast\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function AsyncTTSMessage({ slug, chatId }) {\n  const playerRef = useRef(null);\n  const [speaking, setSpeaking] = useState(false);\n  const [loading, setLoading] = useState(false);\n  const [audioSrc, setAudioSrc] = useState(null);\n  const { t } = useTranslation();\n\n  function speakMessage() {\n    if (speaking) {\n      playerRef?.current?.pause();\n      return;\n    }\n\n    try {\n      if (!audioSrc) {\n        setLoading(true);\n        Workspace.ttsMessage(slug, chatId)\n          .then((audioBlob) => {\n            if (!audioBlob)\n              throw new Error(\"Failed to load or play TTS message response.\");\n            setAudioSrc(audioBlob);\n          })\n          .catch((e) => showToast(e.message, \"error\", { clear: true }))\n          .finally(() => setLoading(false));\n      } else {\n        playerRef.current.play();\n      }\n    } catch (e) {\n      console.error(e);\n      setLoading(false);\n      setSpeaking(false);\n    }\n  }\n\n  useEffect(() => {\n    function setupPlayer() {\n      if (!playerRef?.current) return;\n      playerRef.current.addEventListener(\"play\", () => {\n        setSpeaking(true);\n      });\n\n      playerRef.current.addEventListener(\"pause\", () => {\n        playerRef.current.currentTime = 0;\n        setSpeaking(false);\n      });\n    }\n    setupPlayer();\n  }, []);\n\n  if (!chatId) return null;\n  return (\n    <div className=\"mt-3 relative\">\n      <button\n        onClick={speakMessage}\n        data-auto-play-chat-id={chatId}\n        data-tooltip-id=\"message-to-speech\"\n        data-tooltip-content={\n          speaking\n            ? t(\"pause_tts_speech_message\")\n            : t(\"chat_window.tts_speak_message\")\n        }\n        className=\"border-none text-zinc-300 light:text-slate-500\"\n        aria-label={speaking ? \"Pause speech\" : \"Speak message\"}\n      >\n        {speaking ? (\n          <PauseCircle size={18} className=\"mb-1\" />\n        ) : (\n          <>\n            {loading ? (\n              <CircleNotch size={18} className=\"mb-1 animate-spin\" />\n            ) : (\n              <SpeakerHigh size={18} className=\"mb-1\" />\n            )}\n          </>\n        )}\n        <audio\n          ref={playerRef}\n          hidden={true}\n          src={audioSrc}\n          autoPlay={true}\n          controls={false}\n        />\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/index.jsx",
    "content": "import { useTTSProvider } from \"@/components/contexts/TTSProvider\";\nimport NativeTTSMessage from \"./native\";\nimport AsyncTTSMessage from \"./asyncTts\";\nimport PiperTTSMessage from \"./piperTTS\";\n\nfunction WrapTTS({ children }) {\n  return <div className=\"mx-2\">{children}</div>;\n}\n\nexport default function TTSMessage({ slug, chatId, message }) {\n  const { settings, provider, loading } = useTTSProvider();\n  if (!chatId || loading) return null;\n\n  switch (provider) {\n    case \"openai\":\n    case \"generic-openai\":\n    case \"elevenlabs\":\n      return (\n        <WrapTTS>\n          <AsyncTTSMessage chatId={chatId} slug={slug} />\n        </WrapTTS>\n      );\n    case \"piper_local\":\n      return (\n        <WrapTTS>\n          <PiperTTSMessage\n            chatId={chatId}\n            voiceId={settings?.TTSPiperTTSVoiceModel}\n            message={message}\n          />\n        </WrapTTS>\n      );\n    default:\n      return (\n        <WrapTTS>\n          <NativeTTSMessage chatId={chatId} message={message} />\n        </WrapTTS>\n      );\n  }\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/native.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { SpeakerHigh, PauseCircle } from \"@phosphor-icons/react\";\n\nexport default function NativeTTSMessage({ chatId, message }) {\n  const [speaking, setSpeaking] = useState(false);\n  const [supported, setSupported] = useState(false);\n  useEffect(() => {\n    setSupported(\"speechSynthesis\" in window);\n  }, []);\n\n  function endSpeechUtterance() {\n    window.speechSynthesis?.cancel();\n    setSpeaking(false);\n    return;\n  }\n\n  function speakMessage() {\n    // if the user is pausing this particular message\n    // while the synth is speaking we can end it.\n    // If they are clicking another message's TTS\n    // we need to ignore that until they pause the one that is playing.\n    if (window.speechSynthesis.speaking && speaking) {\n      endSpeechUtterance();\n      return;\n    }\n\n    if (window.speechSynthesis.speaking && !speaking) return;\n    const utterance = new SpeechSynthesisUtterance(message);\n    utterance.addEventListener(\"end\", endSpeechUtterance);\n    window.speechSynthesis.speak(utterance);\n    setSpeaking(true);\n  }\n\n  if (!supported) return null;\n  return (\n    <div className=\"mt-3 relative\">\n      <button\n        onClick={speakMessage}\n        data-auto-play-chat-id={chatId}\n        data-tooltip-id=\"message-to-speech\"\n        data-tooltip-content={\n          speaking ? \"Pause TTS speech of message\" : \"TTS Speak message\"\n        }\n        className=\"border-none text-zinc-300 light:text-slate-500\"\n        aria-label={speaking ? \"Pause speech\" : \"Speak message\"}\n      >\n        {speaking ? (\n          <PauseCircle size={18} className=\"mb-1\" />\n        ) : (\n          <SpeakerHigh size={18} className=\"mb-1\" />\n        )}\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/TTSButton/piperTTS.jsx",
    "content": "import { useEffect, useState, useRef } from \"react\";\nimport { SpeakerHigh, PauseCircle, CircleNotch } from \"@phosphor-icons/react\";\nimport PiperTTSClient from \"@/utils/piperTTS\";\n\nexport default function PiperTTS({ chatId, voiceId = null, message }) {\n  const playerRef = useRef(null);\n  const [speaking, setSpeaking] = useState(false);\n  const [loading, setLoading] = useState(false);\n  const [audioSrc, setAudioSrc] = useState(null);\n\n  async function speakMessage(e) {\n    e.preventDefault();\n    if (speaking) {\n      playerRef?.current?.pause();\n      return;\n    }\n\n    try {\n      if (!audioSrc) {\n        setLoading(true);\n        const client = new PiperTTSClient({ voiceId });\n        const blobUrl = await client.getAudioBlobForText(message);\n        setAudioSrc(blobUrl);\n        setLoading(false);\n      } else {\n        playerRef.current.play();\n      }\n    } catch (e) {\n      console.error(e);\n      setLoading(false);\n      setSpeaking(false);\n    }\n  }\n\n  useEffect(() => {\n    function setupPlayer() {\n      if (!playerRef?.current) return;\n      playerRef.current.addEventListener(\"play\", () => {\n        setSpeaking(true);\n      });\n\n      playerRef.current.addEventListener(\"pause\", () => {\n        playerRef.current.currentTime = 0;\n        setSpeaking(false);\n      });\n    }\n    setupPlayer();\n  }, []);\n\n  return (\n    <div className=\"mt-3 relative\">\n      <button\n        type=\"button\"\n        onClick={speakMessage}\n        disabled={loading}\n        data-auto-play-chat-id={chatId}\n        data-tooltip-id=\"message-to-speech\"\n        data-tooltip-content={\n          speaking ? \"Pause TTS speech of message\" : \"TTS Speak message\"\n        }\n        className=\"border-none text-[var(--theme-sidebar-footer-icon-fill)]\"\n        aria-label={speaking ? \"Pause speech\" : \"Speak message\"}\n      >\n        {speaking ? (\n          <PauseCircle size={18} className=\"mb-1\" />\n        ) : (\n          <>\n            {loading ? (\n              <CircleNotch size={18} className=\"mb-1 animate-spin\" />\n            ) : (\n              <SpeakerHigh size={18} className=\"mb-1\" />\n            )}\n          </>\n        )}\n        <audio\n          ref={playerRef}\n          hidden={true}\n          src={audioSrc}\n          autoPlay={true}\n          controls={false}\n        />\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx",
    "content": "import React, { memo, useState } from \"react\";\nimport useCopyText from \"@/hooks/useCopyText\";\nimport { Check, ThumbsUp, ArrowsClockwise, Copy } from \"@phosphor-icons/react\";\nimport Workspace from \"@/models/workspace\";\nimport { EditMessageAction } from \"./EditMessage\";\nimport RenderMetrics from \"./RenderMetrics\";\nimport ActionMenu from \"./ActionMenu\";\nimport { useTranslation } from \"react-i18next\";\n\nconst Actions = ({\n  message,\n  feedbackScore,\n  chatId,\n  slug,\n  isLastMessage,\n  regenerateMessage,\n  forkThread,\n  isEditing,\n  role,\n  metrics = {},\n}) => {\n  const { t } = useTranslation();\n  const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore);\n  const handleFeedback = async (newFeedback) => {\n    const updatedFeedback =\n      selectedFeedback === newFeedback ? null : newFeedback;\n    await Workspace.updateChatFeedback(chatId, slug, updatedFeedback);\n    setSelectedFeedback(updatedFeedback);\n  };\n\n  return (\n    <div\n      className={`flex w-full flex-wrap items-center gap-y-1 ${role === \"user\" ? \"justify-end\" : \"justify-between\"}`}\n    >\n      <div className=\"flex justify-start items-center gap-x-[8px]\">\n        <div className=\"md:group-hover:opacity-100 transition-all duration-300 md:opacity-0 flex justify-start items-center gap-x-[8px]\">\n          <div\n            className={`flex justify-start items-center gap-x-[8px] ${role === \"user\" ? \"flex-row-reverse\" : \"\"}`}\n          >\n            <CopyMessage message={message} />\n            <EditMessageAction\n              chatId={chatId}\n              role={role}\n              isEditing={isEditing}\n            />\n          </div>\n          {isLastMessage && !isEditing && (\n            <RegenerateMessage\n              regenerateMessage={regenerateMessage}\n              slug={slug}\n              chatId={chatId}\n            />\n          )}\n          {chatId && role !== \"user\" && !isEditing && (\n            <FeedbackButton\n              isSelected={selectedFeedback === true}\n              handleFeedback={() => handleFeedback(true)}\n              tooltipId=\"feedback-button\"\n              tooltipContent={t(\"chat_window.good_response\")}\n              IconComponent={ThumbsUp}\n            />\n          )}\n          <ActionMenu\n            chatId={chatId}\n            forkThread={forkThread}\n            isEditing={isEditing}\n            role={role}\n          />\n        </div>\n      </div>\n      <RenderMetrics metrics={metrics} />\n    </div>\n  );\n};\n\nfunction FeedbackButton({\n  isSelected,\n  handleFeedback,\n  tooltipContent,\n  IconComponent,\n}) {\n  return (\n    <div className=\"mt-3 relative\">\n      <button\n        onClick={handleFeedback}\n        data-tooltip-id=\"feedback-button\"\n        data-tooltip-content={tooltipContent}\n        className=\"text-zinc-300 light:text-slate-500\"\n        aria-label={tooltipContent}\n      >\n        <IconComponent\n          size={20}\n          className=\"mb-1\"\n          weight={isSelected ? \"fill\" : \"regular\"}\n        />\n      </button>\n    </div>\n  );\n}\n\nfunction CopyMessage({ message }) {\n  const { copied, copyText } = useCopyText();\n  const { t } = useTranslation();\n\n  return (\n    <>\n      <div className=\"mt-3 relative\">\n        <button\n          onClick={() => copyText(message)}\n          data-tooltip-id=\"copy-assistant-text\"\n          data-tooltip-content={t(\"chat_window.copy\")}\n          className=\"text-zinc-300 light:text-slate-500\"\n          aria-label={t(\"chat_window.copy\")}\n        >\n          {copied ? (\n            <Check size={20} className=\"mb-1\" />\n          ) : (\n            <Copy size={20} className=\"mb-1\" />\n          )}\n        </button>\n      </div>\n    </>\n  );\n}\n\nfunction RegenerateMessage({ regenerateMessage, chatId }) {\n  const { t } = useTranslation();\n  if (!chatId) return null;\n  return (\n    <div className=\"mt-3 relative\">\n      <button\n        onClick={() => regenerateMessage(chatId)}\n        data-tooltip-id=\"regenerate-assistant-text\"\n        data-tooltip-content={t(\"chat_window.regenerate_response\")}\n        className=\"border-none text-zinc-300 light:text-slate-500\"\n        aria-label={t(\"chat_window.regenerate\")}\n      >\n        <ArrowsClockwise size={20} className=\"mb-1\" weight=\"fill\" />\n      </button>\n    </div>\n  );\n}\n\nexport default memo(Actions);\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx",
    "content": "import React, { memo, useEffect, useRef, useState } from \"react\";\nimport { Info, Warning } from \"@phosphor-icons/react\";\nimport Actions from \"./Actions\";\nimport renderMarkdown from \"@/utils/chat/markdown\";\nimport Citations from \"../Citation\";\nimport { v4 } from \"uuid\";\nimport DOMPurify from \"@/utils/chat/purify\";\nimport { EditMessageForm, useEditMessage } from \"./Actions/EditMessage\";\nimport { useWatchDeleteMessage } from \"./Actions/DeleteMessage\";\nimport TTSMessage from \"./Actions/TTSButton\";\nimport {\n  THOUGHT_REGEX_CLOSE,\n  THOUGHT_REGEX_COMPLETE,\n  THOUGHT_REGEX_OPEN,\n  ThoughtChainComponent,\n} from \"../ThoughtContainer\";\nimport paths from \"@/utils/paths\";\nimport { useTranslation } from \"react-i18next\";\nimport { Link } from \"react-router-dom\";\nimport { chatQueryRefusalResponse } from \"@/utils/chat\";\n\nconst HistoricalMessage = ({\n  uuid = v4(),\n  message,\n  role,\n  workspace,\n  sources = [],\n  attachments = [],\n  error = false,\n  feedbackScore = null,\n  chatId = null,\n  isLastMessage = false,\n  regenerateMessage,\n  saveEditedMessage,\n  forkThread,\n  metrics = {},\n}) => {\n  const { t } = useTranslation();\n  const { isEditing } = useEditMessage({ chatId, role });\n  const { isDeleted, completeDelete, onEndAnimation } = useWatchDeleteMessage({\n    chatId,\n    role,\n  });\n  const adjustTextArea = (event) => {\n    const element = event.target;\n    element.style.height = \"auto\";\n    element.style.height = element.scrollHeight + \"px\";\n  };\n\n  const isRefusalMessage =\n    role === \"assistant\" && message === chatQueryRefusalResponse(workspace);\n\n  if (completeDelete) return null;\n\n  if (!!error) {\n    return (\n      <div key={uuid} className=\"flex justify-start w-full\">\n        <div className=\"py-4 pl-0 pr-4 flex flex-col md:max-w-[80%]\">\n          <div className=\"p-2 rounded-lg bg-red-50 text-red-500\">\n            <span className=\"inline-block\">\n              <Warning className=\"h-4 w-4 mb-1 inline-block\" /> Could not\n              respond to message.\n            </span>\n            <p className=\"text-xs font-mono mt-2 border-l-2 border-red-300 pl-2 bg-red-200 p-2 rounded-sm\">\n              {error}\n            </p>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  if (role === \"user\") {\n    if (isEditing) {\n      return (\n        <div key={uuid} className=\"flex justify-end w-full py-4 px-4\">\n          <EditMessageForm\n            role={role}\n            chatId={chatId}\n            message={message}\n            attachments={attachments}\n            adjustTextArea={adjustTextArea}\n            saveChanges={saveEditedMessage}\n          />\n        </div>\n      );\n    }\n\n    return (\n      <div\n        key={uuid}\n        onAnimationEnd={onEndAnimation}\n        className={`${isDeleted ? \"animate-remove\" : \"\"} flex justify-end w-full group`}\n      >\n        <div className=\"py-4 px-4 flex flex-col items-end\">\n          <div className=\"bg-zinc-800 light:bg-slate-100 rounded-[20px] rounded-br-none px-4 py-3.5 max-w-[600px] [&_p]:m-0\">\n            <TruncatableContent>\n              <RenderChatContent\n                role={role}\n                message={message}\n                messageId={uuid}\n              />\n              <ChatAttachments attachments={attachments} />\n            </TruncatableContent>\n          </div>\n          <Actions\n            message={message}\n            feedbackScore={feedbackScore}\n            chatId={chatId}\n            slug={workspace?.slug}\n            isLastMessage={isLastMessage}\n            regenerateMessage={regenerateMessage}\n            isEditing={isEditing}\n            role={role}\n            forkThread={forkThread}\n            metrics={metrics}\n          />\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      key={uuid}\n      onAnimationEnd={onEndAnimation}\n      className={`${isDeleted ? \"animate-remove\" : \"\"} flex justify-start w-full group`}\n    >\n      <div className=\"py-4 px-4 md:pl-0 flex flex-col w-full\">\n        {isEditing ? (\n          <EditMessageForm\n            role={role}\n            chatId={chatId}\n            message={message}\n            attachments={attachments}\n            adjustTextArea={adjustTextArea}\n            saveChanges={saveEditedMessage}\n          />\n        ) : (\n          <div className=\"break-words\">\n            <RenderChatContent role={role} message={message} messageId={uuid} />\n            {isRefusalMessage && (\n              <Link\n                data-tooltip-id=\"query-refusal-info\"\n                data-tooltip-content={`${t(\"chat.refusal.tooltip-description\")}`}\n                className=\"!no-underline group !flex w-fit\"\n                to={paths.chatModes()}\n                target=\"_blank\"\n              >\n                <div className=\"flex flex-row items-center gap-x-1 group-hover:opacity-100 opacity-60 w-fit\">\n                  <Info className=\"text-theme-text-secondary\" />\n                  <p className=\"!m-0 !p-0 text-theme-text-secondary !no-underline text-xs cursor-pointer\">\n                    {t(\"chat.refusal.tooltip-title\")}\n                  </p>\n                </div>\n              </Link>\n            )}\n            <ChatAttachments attachments={attachments} />\n          </div>\n        )}\n        <div className=\"flex items-start md:items-center gap-x-1\">\n          <TTSMessage\n            slug={workspace?.slug}\n            chatId={chatId}\n            message={message}\n          />\n          <Actions\n            message={message}\n            feedbackScore={feedbackScore}\n            chatId={chatId}\n            slug={workspace?.slug}\n            isLastMessage={isLastMessage}\n            regenerateMessage={regenerateMessage}\n            isEditing={isEditing}\n            role={role}\n            forkThread={forkThread}\n            metrics={metrics}\n          />\n        </div>\n        {role === \"assistant\" && <Citations sources={sources} />}\n      </div>\n    </div>\n  );\n};\n\nexport default memo(\n  HistoricalMessage,\n  // Skip re-render the historical message:\n  // - if the content is the exact same\n  // - AND (not streaming)\n  // - the lastMessage status is the same (regen icon)\n  // - the chatID matches between renders. (feedback icons)\n  // - the metrics are the same (metrics are updated in real time)\n  (prevProps, nextProps) => {\n    return (\n      prevProps.message === nextProps.message &&\n      prevProps.isLastMessage === nextProps.isLastMessage &&\n      prevProps.chatId === nextProps.chatId &&\n      JSON.stringify(prevProps.metrics) === JSON.stringify(nextProps.metrics) &&\n      JSON.stringify(prevProps.sources) === JSON.stringify(nextProps.sources)\n    );\n  }\n);\n\nfunction ChatAttachments({ attachments = [] }) {\n  if (!attachments.length) return null;\n  return (\n    <div className=\"flex flex-wrap gap-4 mt-4\">\n      {attachments.map((item) => (\n        <img\n          alt={`Attachment: ${item.name}`}\n          key={item.name}\n          src={item.contentString}\n          className=\"w-[120px] h-[120px] object-cover rounded-lg\"\n        />\n      ))}\n    </div>\n  );\n}\n\nfunction TruncatableContent({ children }) {\n  const contentRef = useRef(null);\n  const [isExpanded, setIsExpanded] = useState(false);\n  const [isOverflowing, setIsOverflowing] = useState(false);\n  const { t } = useTranslation();\n\n  useEffect(() => {\n    if (contentRef.current) {\n      setIsOverflowing(contentRef.current.scrollHeight > 250);\n    }\n  }, []);\n\n  const showTruncation = !isExpanded && isOverflowing;\n\n  return (\n    <>\n      <div className=\"relative\">\n        <div\n          ref={contentRef}\n          className={showTruncation ? \"max-h-[250px] overflow-hidden\" : \"\"}\n        >\n          {children}\n        </div>\n        {showTruncation && (\n          <>\n            <div\n              className=\"absolute bottom-0 left-0 right-0 h-[36px] light:hidden pointer-events-none\"\n              style={{\n                background:\n                  \"linear-gradient(180deg, rgba(39, 39, 42, 0.00) 0%, rgba(39, 39, 42, 0.65) 50%, #27272A 100%)\",\n              }}\n            />\n            <div\n              className=\"absolute bottom-0 left-0 right-0 h-[36px] hidden light:block pointer-events-none\"\n              style={{\n                background:\n                  \"linear-gradient(180deg, rgba(241, 245, 249, 0.00) 0%, rgba(241, 245, 249, 0.65) 50%, #F1F5F9 100%)\",\n              }}\n            />\n          </>\n        )}\n      </div>\n      {isOverflowing && (\n        <button\n          onClick={() => setIsExpanded(!isExpanded)}\n          className=\"text-zinc-300 light:text-slate-700 hover:text-white light:hover:text-slate-900 text-xs font-medium leading-4 mt-2\"\n        >\n          {isExpanded ? t(\"chat_window.see_less\") : t(\"chat_window.see_more\")}\n        </button>\n      )}\n    </>\n  );\n}\n\nconst RenderChatContent = memo(\n  ({ role, message, messageId }) => {\n    // If the message is not from the assistant, we can render it directly\n    // as normal since the user cannot think (lol)\n    if (role !== \"assistant\")\n      return (\n        <span\n          className=\"flex flex-col gap-y-1 text-white light:text-slate-900\"\n          dangerouslySetInnerHTML={{\n            __html: DOMPurify.sanitize(renderMarkdown(message)),\n          }}\n        />\n      );\n    let thoughtChain = null;\n    let msgToRender = message;\n    if (!message) return null;\n\n    // If the message is a perfect thought chain, we can render it directly\n    // Complete == open and close tags match perfectly.\n    if (message.match(THOUGHT_REGEX_COMPLETE)) {\n      thoughtChain = message.match(THOUGHT_REGEX_COMPLETE)?.[0];\n      msgToRender = message.replace(THOUGHT_REGEX_COMPLETE, \"\");\n    }\n\n    // If the message is a thought chain but not a complete thought chain (matching opening tags but not closing tags),\n    // we can render it as a thought chain if we can at least find a closing tag\n    // This can occur when the assistant starts with <thinking> and then <response>'s later.\n    if (\n      message.match(THOUGHT_REGEX_OPEN) &&\n      !message.match(THOUGHT_REGEX_CLOSE)\n    ) {\n      thoughtChain = message;\n      msgToRender = \"\";\n    }\n\n    return (\n      <>\n        {thoughtChain && (\n          <ThoughtChainComponent content={thoughtChain} messageId={messageId} />\n        )}\n        <span\n          className=\"flex flex-col gap-y-1 text-white light:text-slate-900\"\n          dangerouslySetInnerHTML={{\n            __html: DOMPurify.sanitize(renderMarkdown(msgToRender)),\n          }}\n        />\n      </>\n    );\n  },\n  (prevProps, nextProps) => {\n    return (\n      prevProps.role === nextProps.role &&\n      prevProps.message === nextProps.message &&\n      prevProps.messageId === nextProps.messageId\n    );\n  }\n);\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx",
    "content": "/* eslint-disable react-hooks/refs */\nimport { memo, useRef, useEffect } from \"react\";\nimport { Warning } from \"@phosphor-icons/react\";\nimport renderMarkdown from \"@/utils/chat/markdown\";\nimport DOMPurify from \"@/utils/chat/purify\";\nimport Citations from \"../Citation\";\nimport {\n  THOUGHT_REGEX_CLOSE,\n  THOUGHT_REGEX_COMPLETE,\n  THOUGHT_REGEX_OPEN,\n  ThoughtChainComponent,\n} from \"../ThoughtContainer\";\n\nconst PromptReply = ({ uuid, reply, pending, error, sources = [] }) => {\n  if (!reply && sources.length === 0 && !pending && !error) return null;\n\n  if (pending) {\n    return (\n      <div className=\"flex justify-start w-full\">\n        <div className=\"py-4 pl-0 pr-4 flex flex-col md:max-w-[80%]\">\n          <div className=\"mt-3 ml-1 dot-falling light:invert\"></div>\n        </div>\n      </div>\n    );\n  }\n\n  if (error) {\n    return (\n      <div className=\"flex justify-start w-full\">\n        <div className=\"py-4 pl-0 pr-4 flex flex-col md:max-w-[80%]\">\n          <span className=\"inline-block p-2 rounded-lg bg-red-50 text-red-500\">\n            <Warning className=\"h-4 w-4 mb-1 inline-block\" /> Could not respond\n            to message.\n            <span className=\"text-xs\">Reason: {error || \"unknown\"}</span>\n          </span>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div key={uuid} className=\"flex justify-start w-full\">\n      <div className=\"py-4 pl-0 pr-4 flex flex-col w-full\">\n        <RenderAssistantChatContent\n          key={`${uuid}-prompt-reply-content`}\n          message={reply}\n          messageId={uuid}\n        />\n        <Citations sources={sources} />\n      </div>\n    </div>\n  );\n};\n\nfunction RenderAssistantChatContent({ message, messageId }) {\n  const contentRef = useRef(\"\");\n  const thoughtChainRef = useRef(null);\n\n  useEffect(() => {\n    const thinking =\n      message.match(THOUGHT_REGEX_OPEN) && !message.match(THOUGHT_REGEX_CLOSE);\n\n    if (thinking && thoughtChainRef.current) {\n      thoughtChainRef.current.updateContent(message);\n      return;\n    }\n\n    const completeThoughtChain = message.match(THOUGHT_REGEX_COMPLETE)?.[0];\n    const msgToRender = message.replace(THOUGHT_REGEX_COMPLETE, \"\");\n\n    if (completeThoughtChain && thoughtChainRef.current) {\n      thoughtChainRef.current.updateContent(completeThoughtChain);\n    }\n\n    contentRef.current = msgToRender;\n  }, [message]);\n\n  const thinking =\n    message.match(THOUGHT_REGEX_OPEN) && !message.match(THOUGHT_REGEX_CLOSE);\n  if (thinking)\n    return (\n      <ThoughtChainComponent\n        ref={thoughtChainRef}\n        content=\"\"\n        messageId={messageId}\n      />\n    );\n\n  return (\n    <div className=\"flex flex-col gap-y-1\">\n      {message.match(THOUGHT_REGEX_COMPLETE) && (\n        <ThoughtChainComponent\n          ref={thoughtChainRef}\n          content=\"\"\n          messageId={messageId}\n        />\n      )}\n      <span\n        className=\"break-words\"\n        dangerouslySetInnerHTML={{\n          __html: DOMPurify.sanitize(renderMarkdown(contentRef.current)),\n        }}\n      />\n    </div>\n  );\n}\n\nexport default memo(PromptReply);\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/StatusResponse/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport { CaretDown } from \"@phosphor-icons/react\";\n\nimport AgentAnimation from \"@/media/animations/agent-animation.webm\";\nimport AgentStatic from \"@/media/animations/agent-static.png\";\n\nexport default function StatusResponse({ messages = [], isThinking = false }) {\n  const [isExpanded, setIsExpanded] = useState(false);\n  const currentThought = messages[messages.length - 1];\n  const previousThoughts = messages.slice(0, -1);\n\n  function handleExpandClick() {\n    if (!previousThoughts.length > 0) return;\n    setIsExpanded(!isExpanded);\n  }\n\n  return (\n    <div className=\"flex justify-center w-full pr-4\">\n      <div className=\"w-full flex flex-col\">\n        <div className=\"w-full\">\n          <div\n            onClick={handleExpandClick}\n            style={{\n              transition: \"all 0.1s ease-in-out\",\n              borderRadius: \"16px\",\n            }}\n            className=\"relative bg-zinc-800 light:bg-slate-100 p-4\"\n          >\n            <div className=\"absolute top-4 left-4 w-[18px] h-[18px]\">\n              {isThinking ? (\n                <video\n                  autoPlay\n                  loop\n                  muted\n                  playsInline\n                  className=\"w-[18px] h-[18px] scale-[165%] transition-opacity duration-200 light:invert light:opacity-50\"\n                  data-tooltip-id=\"agent-thinking\"\n                  data-tooltip-content=\"Agent is thinking...\"\n                  aria-label=\"Agent is thinking...\"\n                >\n                  <source src={AgentAnimation} type=\"video/webm\" />\n                </video>\n              ) : (\n                <img\n                  src={AgentStatic}\n                  alt=\"Agent complete\"\n                  className=\"w-[18px] h-[18px] transition-opacity duration-200 light:invert light:opacity-50\"\n                  data-tooltip-id=\"agent-thinking\"\n                  data-tooltip-content=\"Agent has finished thinking\"\n                  aria-label=\"Agent has finished thinking\"\n                />\n              )}\n            </div>\n            {previousThoughts?.length > 0 && (\n              <button\n                onClick={handleExpandClick}\n                className=\"absolute top-4 right-4 border-none text-zinc-200 light:text-slate-800 transition-colors\"\n                data-tooltip-id=\"expand-cot\"\n                data-tooltip-content={\n                  isExpanded ? \"Hide thought chain\" : \"Show thought chain\"\n                }\n                aria-label={\n                  isExpanded ? \"Hide thought chain\" : \"Show thought chain\"\n                }\n              >\n                <CaretDown\n                  className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? \"rotate-180\" : \"\"}`}\n                />\n              </button>\n            )}\n            <div\n              className={`ml-[28px] mr-[26px] transition-[max-height] duration-300 ease-in-out origin-top ${isExpanded ? \"\" : \"overflow-hidden max-h-[18px]\"}`}\n            >\n              <div className=\"text-zinc-200 light:text-slate-800 font-mono text-sm leading-[18px]\">\n                {!isExpanded ? (\n                  <span className=\"block w-full truncate\">\n                    {currentThought.content}\n                  </span>\n                ) : (\n                  <>\n                    {previousThoughts.map((thought, index) => (\n                      <div\n                        key={`cot-${thought.uuid || index}`}\n                        className=\"mb-2\"\n                      >\n                        {thought.content}\n                      </div>\n                    ))}\n                    <div>{currentThought.content}</div>\n                  </>\n                )}\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/ThoughtContainer/index.jsx",
    "content": "import {\n  useState,\n  useEffect,\n  forwardRef,\n  useImperativeHandle,\n  createContext,\n  useContext,\n  useCallback,\n} from \"react\";\nimport renderMarkdown from \"@/utils/chat/markdown\";\nimport { CaretDown } from \"@phosphor-icons/react\";\nimport DOMPurify from \"dompurify\";\nimport { isMobile } from \"react-device-detect\";\nimport ThinkingAnimation from \"@/media/animations/thinking-animation.webm\";\nimport ThinkingStatic from \"@/media/animations/thinking-static.png\";\n\n/**\n * Context to persist thought expansion state across component transitions\n * (e.g., from PromptReply to HistoricalMessage)\n */\nconst ThoughtExpansionContext = createContext(null);\n\nexport function ThoughtExpansionProvider({ children }) {\n  const [expansionStates, setExpansionStates] = useState({});\n\n  const getExpanded = useCallback(\n    (messageId) => {\n      if (!messageId) return false;\n      return expansionStates[messageId] ?? false;\n    },\n    [expansionStates]\n  );\n\n  const setExpanded = useCallback((messageId, expanded) => {\n    if (!messageId) return;\n    setExpansionStates((prev) => ({\n      ...prev,\n      [messageId]: expanded,\n    }));\n  }, []);\n\n  return (\n    <ThoughtExpansionContext.Provider value={{ getExpanded, setExpanded }}>\n      {children}\n    </ThoughtExpansionContext.Provider>\n  );\n}\n\nexport function useThoughtExpansion(messageId) {\n  const context = useContext(ThoughtExpansionContext);\n  if (!context) {\n    // Fallback when used outside provider - use local state only\n    return { expanded: false, setExpanded: () => {} };\n  }\n  return {\n    expanded: context.getExpanded(messageId),\n    setExpanded: (value) => context.setExpanded(messageId, value),\n  };\n}\n\nconst THOUGHT_KEYWORDS = [\"thought\", \"thinking\", \"think\", \"thought_chain\"];\nconst CLOSING_TAGS = [...THOUGHT_KEYWORDS, \"response\", \"answer\"];\nexport const THOUGHT_REGEX_OPEN = new RegExp(\n  THOUGHT_KEYWORDS.map((keyword) => `<${keyword}\\\\s*(?:[^>]*?)?\\\\s*>`).join(\"|\")\n);\nexport const THOUGHT_REGEX_CLOSE = new RegExp(\n  CLOSING_TAGS.map((keyword) => `</${keyword}\\\\s*(?:[^>]*?)?>`).join(\"|\")\n);\nexport const THOUGHT_REGEX_COMPLETE = new RegExp(\n  THOUGHT_KEYWORDS.map(\n    (keyword) =>\n      `<${keyword}\\\\s*(?:[^>]*?)?\\\\s*>[\\\\s\\\\S]*?<\\\\/${keyword}\\\\s*(?:[^>]*?)?>`\n  ).join(\"|\")\n);\nconst THOUGHT_PREVIEW_LENGTH = isMobile ? 25 : 50;\n\n/**\n * Checks if the content has readable content.\n * @param {string} content - The content to check.\n * @returns {boolean} - Whether the content has readable content.\n */\nfunction contentIsNotEmpty(content = \"\") {\n  return (\n    content\n      ?.trim()\n      ?.replace(THOUGHT_REGEX_OPEN, \"\")\n      ?.replace(THOUGHT_REGEX_CLOSE, \"\")\n      ?.replace(/[\\n\\s]/g, \"\")?.length > 0\n  );\n}\n\n/**\n * Component to render a thought chain.\n * @param {string} content - The content of the thought chain.\n * @param {string} messageId - The unique ID for this message (used to persist expansion state).\n * @returns {JSX.Element}\n */\nexport const ThoughtChainComponent = forwardRef(\n  ({ content: initialContent, messageId }, ref) => {\n    const [content, setContent] = useState(initialContent);\n    const [hasReadableContent, setHasReadableContent] = useState(\n      contentIsNotEmpty(initialContent)\n    );\n    const { expanded: persistedExpanded, setExpanded: setPersistedExpanded } =\n      useThoughtExpansion(messageId);\n    const [localExpanded, setLocalExpanded] = useState(false);\n\n    // Use persisted state if messageId is provided, otherwise use local state\n    const isExpanded = messageId ? persistedExpanded : localExpanded;\n    const setIsExpanded = messageId ? setPersistedExpanded : setLocalExpanded;\n\n    // Sync content state with prop changes (for streaming through HistoricalMessage)\n    useEffect(() => {\n      if (initialContent !== content) {\n        setContent(initialContent);\n        setHasReadableContent(contentIsNotEmpty(initialContent));\n      }\n    }, [initialContent]);\n\n    useImperativeHandle(ref, () => ({\n      updateContent: (newContent) => {\n        setContent(newContent);\n        setHasReadableContent(contentIsNotEmpty(newContent));\n      },\n    }));\n\n    const isThinking =\n      content.match(THOUGHT_REGEX_OPEN) && !content.match(THOUGHT_REGEX_CLOSE);\n    const isComplete =\n      content.match(THOUGHT_REGEX_COMPLETE) ||\n      content.match(THOUGHT_REGEX_CLOSE);\n    const tagStrippedContent = content\n      .replace(THOUGHT_REGEX_OPEN, \"\")\n      .replace(THOUGHT_REGEX_CLOSE, \"\");\n    const canExpand = tagStrippedContent.length > THOUGHT_PREVIEW_LENGTH;\n    if (!content || !content.length || !hasReadableContent) return null;\n\n    function handleExpandClick() {\n      if (!canExpand) return;\n      setIsExpanded(!isExpanded);\n    }\n\n    return (\n      <div className=\"flex justify-center w-full\">\n        <div className=\"w-full flex flex-col\">\n          <div className=\"w-full\">\n            <div\n              style={{\n                transition: \"all 0.1s ease-in-out\",\n                borderRadius: \"16px\",\n              }}\n              className=\"relative bg-zinc-800 light:bg-slate-100 p-4\"\n            >\n              <div className=\"absolute top-4 left-4 w-[18px] h-[18px]\">\n                {isThinking || isComplete ? (\n                  <>\n                    <video\n                      autoPlay\n                      loop\n                      muted\n                      playsInline\n                      className={`w-[18px] h-[18px] scale-[115%] transition-opacity duration-200 light:invert light:opacity-50 ${isThinking ? \"opacity-100\" : \"opacity-0 hidden\"}`}\n                      data-tooltip-id=\"cot-thinking\"\n                      data-tooltip-content=\"Model is thinking...\"\n                      aria-label=\"Model is thinking...\"\n                    >\n                      <source src={ThinkingAnimation} type=\"video/webm\" />\n                    </video>\n                    <img\n                      src={ThinkingStatic}\n                      alt=\"Thinking complete\"\n                      className={`w-[18px] h-[18px] transition-opacity duration-200 light:invert light:opacity-50 ${!isThinking && isComplete ? \"opacity-100\" : \"opacity-0 hidden\"}`}\n                      data-tooltip-id=\"cot-thinking\"\n                      data-tooltip-content=\"Model has finished thinking\"\n                      aria-label=\"Model has finished thinking\"\n                    />\n                  </>\n                ) : null}\n              </div>\n              {canExpand && (\n                <button\n                  onClick={handleExpandClick}\n                  className=\"absolute top-4 right-4 border-none text-zinc-200 light:text-slate-800 transition-colors\"\n                  data-tooltip-id=\"expand-cot\"\n                  data-tooltip-content={\n                    isExpanded ? \"Hide thought chain\" : \"Show thought chain\"\n                  }\n                  aria-label={\n                    isExpanded ? \"Hide thought chain\" : \"Show thought chain\"\n                  }\n                >\n                  <CaretDown\n                    className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? \"rotate-180\" : \"\"}`}\n                  />\n                </button>\n              )}\n              <div\n                className={`ml-[28px] mr-[26px] transition-[max-height] duration-300 ease-in-out origin-top ${isExpanded ? \"\" : \"overflow-hidden max-h-[18px]\"}`}\n              >\n                <div className=\"text-zinc-200 light:text-slate-800 font-mono text-sm leading-[18px] [&_p]:m-0\">\n                  <span\n                    className={`block w-full ${!isExpanded ? \"truncate\" : \"\"}`}\n                    dangerouslySetInnerHTML={{\n                      __html: DOMPurify.sanitize(\n                        isExpanded\n                          ? renderMarkdown(tagStrippedContent)\n                          : tagStrippedContent\n                      ),\n                    }}\n                  />\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    );\n  }\n);\nThoughtChainComponent.displayName = \"ThoughtChainComponent\";\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx",
    "content": "import {\n  useEffect,\n  useRef,\n  useState,\n  useMemo,\n  useCallback,\n  forwardRef,\n} from \"react\";\nimport HistoricalMessage from \"./HistoricalMessage\";\nimport PromptReply from \"./PromptReply\";\nimport StatusResponse from \"./StatusResponse\";\nimport { useManageWorkspaceModal } from \"../../../Modals/ManageWorkspace\";\nimport ManageWorkspace from \"../../../Modals/ManageWorkspace\";\nimport { ArrowDown } from \"@phosphor-icons/react\";\nimport debounce from \"lodash.debounce\";\nimport Chartable from \"./Chartable\";\nimport Workspace from \"@/models/workspace\";\nimport { useParams } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\nimport Appearance from \"@/models/appearance\";\nimport useTextSize from \"@/hooks/useTextSize\";\nimport useChatHistoryScrollHandle from \"@/hooks/useChatHistoryScrollHandle\";\nimport { ThoughtExpansionProvider } from \"./ThoughtContainer\";\n\nexport default forwardRef(function (\n  {\n    history = [],\n    workspace,\n    sendCommand,\n    updateHistory,\n    regenerateAssistantMessage,\n  },\n  ref\n) {\n  const lastScrollTopRef = useRef(0);\n  const chatHistoryRef = useRef(null);\n  const { threadSlug = null } = useParams();\n  const { showing, hideModal } = useManageWorkspaceModal();\n  const [isAtBottom, setIsAtBottom] = useState(true);\n  const [isUserScrolling, setIsUserScrolling] = useState(false);\n  const isStreaming = history[history.length - 1]?.animate;\n  const { showScrollbar } = Appearance.getSettings();\n  const { textSizeClass } = useTextSize();\n\n  useEffect(() => {\n    if (!isUserScrolling && (isAtBottom || isStreaming)) {\n      scrollToBottom(false); // Use instant scroll for auto-scrolling\n    }\n  }, [history, isAtBottom, isStreaming, isUserScrolling]);\n\n  const handleScroll = (e) => {\n    const { scrollTop, scrollHeight, clientHeight } = e.target;\n    const isBottom = scrollHeight - scrollTop - clientHeight < 2;\n\n    // Detect if this is a user-initiated scroll\n    if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) {\n      setIsUserScrolling(!isBottom);\n    }\n\n    setIsAtBottom(isBottom);\n    lastScrollTopRef.current = scrollTop;\n  };\n\n  const debouncedScroll = debounce(handleScroll, 100);\n\n  useEffect(() => {\n    const chatHistoryElement = chatHistoryRef.current;\n    if (chatHistoryElement) {\n      chatHistoryElement.addEventListener(\"scroll\", debouncedScroll);\n      return () =>\n        chatHistoryElement.removeEventListener(\"scroll\", debouncedScroll);\n    }\n  }, []);\n\n  const scrollToBottom = (smooth = false) => {\n    if (chatHistoryRef.current) {\n      chatHistoryRef.current.scrollTo({\n        top: chatHistoryRef.current.scrollHeight,\n\n        // Smooth is on when user clicks the button but disabled during auto scroll\n        // We must disable this during auto scroll because it causes issues with\n        // detecting when we are at the bottom of the chat.\n        ...(smooth ? { behavior: \"smooth\" } : {}),\n      });\n    }\n  };\n\n  useChatHistoryScrollHandle(ref, chatHistoryRef, {\n    setIsUserScrolling,\n    isStreaming,\n    scrollToBottom,\n  });\n\n  const saveEditedMessage = async ({\n    editedMessage,\n    chatId,\n    role,\n    attachments = [],\n    saveOnly = false,\n  }) => {\n    if (!editedMessage) return; // Don't save empty edits.\n\n    // \"Save\" on a user message: update the prompt text without regenerating\n    if (role === \"user\" && saveOnly) {\n      const updatedHistory = [...history];\n      const targetIdx = history.findIndex((msg) => msg.chatId === chatId);\n      if (targetIdx < 0) return;\n      updatedHistory[targetIdx].content = editedMessage;\n      updateHistory(updatedHistory);\n      await Workspace.updateChat(\n        workspace.slug,\n        threadSlug,\n        chatId,\n        editedMessage,\n        \"user\"\n      );\n      return;\n    }\n\n    // \"Submit\" on a user message: auto-regenerate the response and delete all\n    // messages post modified message\n    if (role === \"user\") {\n      // remove all messages after the edited message\n      // technically there are two chatIds per-message pair, this will split the first.\n      const updatedHistory = history.slice(\n        0,\n        history.findIndex((msg) => msg.chatId === chatId) + 1\n      );\n\n      // update last message in history to edited message\n      updatedHistory[updatedHistory.length - 1].content = editedMessage;\n      // remove all edited messages after the edited message in backend\n      await Workspace.deleteEditedChats(workspace.slug, threadSlug, chatId);\n      sendCommand({\n        text: editedMessage,\n        autoSubmit: true,\n        history: updatedHistory,\n        attachments,\n      });\n      return;\n    }\n\n    // If role is an assistant we simply want to update the comment and save on the backend as an edit.\n    if (role === \"assistant\") {\n      const updatedHistory = [...history];\n      const targetIdx = history.findIndex(\n        (msg) => msg.chatId === chatId && msg.role === role\n      );\n      if (targetIdx < 0) return;\n      updatedHistory[targetIdx].content = editedMessage;\n      updateHistory(updatedHistory);\n      await Workspace.updateChat(\n        workspace.slug,\n        threadSlug,\n        chatId,\n        editedMessage\n      );\n      return;\n    }\n  };\n\n  const forkThread = async (chatId) => {\n    const newThreadSlug = await Workspace.forkThread(\n      workspace.slug,\n      threadSlug,\n      chatId\n    );\n    window.location.href = paths.workspace.thread(\n      workspace.slug,\n      newThreadSlug\n    );\n  };\n\n  const compiledHistory = useMemo(\n    () =>\n      buildMessages({\n        workspace,\n        history,\n        regenerateAssistantMessage,\n        saveEditedMessage,\n        forkThread,\n      }),\n    [\n      workspace,\n      history,\n      regenerateAssistantMessage,\n      saveEditedMessage,\n      forkThread,\n    ]\n  );\n  const lastMessageInfo = useMemo(() => getLastMessageInfo(history), [history]);\n  const renderStatusResponse = useCallback(\n    (item, index) => {\n      const hasSubsequentMessages = index < compiledHistory.length - 1;\n      return (\n        <StatusResponse\n          key={`status-group-${index}`}\n          messages={item}\n          isThinking={!hasSubsequentMessages && lastMessageInfo.isAnimating}\n        />\n      );\n    },\n    [compiledHistory.length, lastMessageInfo]\n  );\n\n  return (\n    <ThoughtExpansionProvider>\n      <div\n        className={`markdown text-white/80 light:text-theme-text-primary font-light ${textSizeClass} h-full md:h-[83%] pb-[100px] pt-6 md:pt-0 md:pb-20 md:mx-0 overflow-y-scroll flex flex-col items-center justify-start ${showScrollbar ? \"show-scrollbar\" : \"no-scroll\"}`}\n        id=\"chat-history\"\n        ref={chatHistoryRef}\n        onScroll={handleScroll}\n      >\n        <div className=\"w-full max-w-[750px]\">\n          {compiledHistory.map((item, index) =>\n            Array.isArray(item) ? renderStatusResponse(item, index) : item\n          )}\n        </div>\n        {showing && (\n          <ManageWorkspace\n            hideModal={hideModal}\n            providedSlug={workspace.slug}\n          />\n        )}\n      </div>\n      {!isAtBottom && (\n        <div className=\"absolute bottom-40 right-10 z-50 cursor-pointer animate-pulse\">\n          <div className=\"flex flex-col items-center\">\n            <div\n              className=\"p-1 rounded-full border border-white/10 bg-white/10 hover:bg-white/20 hover:text-white\"\n              onClick={() => {\n                scrollToBottom(isStreaming ? false : true);\n                setIsUserScrolling(false);\n              }}\n            >\n              <ArrowDown weight=\"bold\" className=\"text-white/60 w-5 h-5\" />\n            </div>\n          </div>\n        </div>\n      )}\n    </ThoughtExpansionProvider>\n  );\n});\n\nconst getLastMessageInfo = (history) => {\n  const lastMessage = history?.[history.length - 1] || {};\n  return {\n    isAnimating: lastMessage?.animate,\n    isStatusResponse: lastMessage?.type === \"statusResponse\",\n  };\n};\n\n/**\n * Builds the history of messages for the chat.\n * This is mostly useful for rendering the history in a way that is easy to understand.\n * as well as compensating for agent thinking and other messages that are not part of the history, but\n * are still part of the chat.\n *\n * @param {Object} param0 - The parameters for building the messages.\n * @param {Array} param0.history - The history of messages.\n * @param {Object} param0.workspace - The workspace object.\n * @param {Function} param0.regenerateAssistantMessage - The function to regenerate the assistant message.\n * @param {Function} param0.saveEditedMessage - The function to save the edited message.\n * @param {Function} param0.forkThread - The function to fork the thread.\n * @returns {Array} The compiled history of messages.\n */\nfunction buildMessages({\n  history,\n  workspace,\n  regenerateAssistantMessage,\n  saveEditedMessage,\n  forkThread,\n}) {\n  return history.reduce((acc, props, index) => {\n    const isLastBotReply =\n      index === history.length - 1 && props.role === \"assistant\";\n\n    if (props?.type === \"statusResponse\" && !!props.content) {\n      if (acc.length > 0 && Array.isArray(acc[acc.length - 1])) {\n        acc[acc.length - 1].push(props);\n      } else {\n        acc.push([props]);\n      }\n      return acc;\n    }\n\n    if (props.type === \"rechartVisualize\" && !!props.content) {\n      acc.push(<Chartable key={props.uuid} props={props} />);\n    } else if (isLastBotReply && props.animate) {\n      acc.push(\n        <PromptReply\n          key={`prompt-reply-${props.uuid || index}`}\n          uuid={props.uuid}\n          reply={props.content}\n          pending={props.pending}\n          sources={props.sources}\n          error={props.error}\n          closed={props.closed}\n        />\n      );\n    } else {\n      acc.push(\n        <HistoricalMessage\n          key={index}\n          uuid={props.uuid}\n          message={props.content}\n          role={props.role}\n          workspace={workspace}\n          sources={props.sources}\n          feedbackScore={props.feedbackScore}\n          chatId={props.chatId}\n          error={props.error}\n          attachments={props.attachments}\n          regenerateMessage={regenerateAssistantMessage}\n          isLastMessage={isLastBotReply}\n          saveEditedMessage={saveEditedMessage}\n          forkThread={forkThread}\n          metrics={props.metrics}\n        />\n      );\n    }\n    return acc;\n  }, []);\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/ChatTooltips/index.jsx",
    "content": "import { Tooltip } from \"react-tooltip\";\nimport { createPortal } from \"react-dom\";\nimport { useTranslation } from \"react-i18next\";\n\n/**\n * Set the tooltips for the chat container in bulk.\n * Why do this?\n *\n * React-tooltip rendering on _each_ chat will attach an event listener to the body.\n * This will add up if we have many chats open resulting in the browser crashing\n * so we batch them together in a single component that renders at the top most level with\n * a static id the content can change, but this prevents the React-tooltip library from adding\n * hundreds of event listeners to the DOM.\n *\n * In general, anywhere we have iterative rendering the Tooltip should be rendered at the highest level to prevent\n * hundreds of event listeners from being added to the DOM in the worst case scenario.\n * @returns\n */\nexport function ChatTooltips() {\n  const { t } = useTranslation();\n\n  return (\n    <>\n      <Tooltip\n        id=\"message-to-speech\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip !text-xs\"\n      />\n      <Tooltip\n        id=\"regenerate-assistant-text\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip !text-xs\"\n      />\n      <Tooltip\n        id=\"copy-assistant-text\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip !text-xs\"\n      />\n      <Tooltip\n        id=\"feedback-button\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip !text-xs\"\n      />\n      <Tooltip\n        id=\"action-menu\"\n        place=\"top\"\n        delayShow={300}\n        className=\"tooltip !text-xs\"\n      />\n      <Tooltip\n        id=\"edit-input-text\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip !text-xs\"\n      />\n      <Tooltip\n        id=\"metrics-visibility\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip !text-xs\"\n      />\n      <Tooltip\n        id=\"expand-cot\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip !text-xs\"\n      />\n      <Tooltip\n        id=\"cot-thinking\"\n        place=\"bottom\"\n        delayShow={500}\n        className=\"tooltip !text-xs\"\n      />\n      <Tooltip\n        id=\"query-refusal-info\"\n        place=\"top\"\n        delayShow={500}\n        className=\"tooltip !text-xs max-w-[350px]\"\n      />\n      <Tooltip\n        id=\"context-window-limit-exceeded\"\n        place=\"top\"\n        delayShow={500}\n        className=\"tooltip !text-xs max-w-[350px]\"\n      />\n      <Tooltip\n        id=\"attachment-status-tooltip\"\n        place=\"top\"\n        delayShow={300}\n        className=\"tooltip !text-xs\"\n      />\n      <Tooltip\n        id=\"attach-item-btn\"\n        place=\"top\"\n        delayShow={300}\n        className=\"tooltip !text-xs\"\n      />\n      <Tooltip\n        id=\"agent-skill-disabled-tooltip\"\n        place=\"top\"\n        delayShow={300}\n        className=\"tooltip !text-xs z-99\"\n        content={t(\"chat_window.agent_skills_disabled_in_session\")}\n      />\n      <DocumentLevelTooltip />\n    </>\n  );\n}\n\n/**\n * This is a document level tooltip that is rendered at the top most level of the document\n * to ensure it is rendered above the chat history and other elements. Anytime we have tooltips\n * in modals the z-indexing can be recalculated and we need to ensure it is rendered at the top most level\n * so it positions correctly.\n */\nfunction DocumentLevelTooltip() {\n  return createPortal(\n    <>\n      <Tooltip\n        id=\"similarity-score\"\n        place=\"top\"\n        delayShow={100}\n        // z-[100] to ensure it renders above the chat history\n        // as the citation modal is z-indexed above the chat history\n        className=\"tooltip !text-xs z-[100]\"\n      />\n    </>,\n    document.body\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/FileUploadWarningModal/index.jsx",
    "content": "import { CircleNotch } from \"@phosphor-icons/react\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport pluralize from \"pluralize\";\nimport { numberWithCommas } from \"@/utils/numbers\";\nimport useUser from \"@/hooks/useUser\";\nimport { Link } from \"react-router-dom\";\nimport Paths from \"@/utils/paths\";\nimport Workspace from \"@/models/workspace\";\n\nexport default function FileUploadWarningModal({\n  show,\n  onClose,\n  onContinue,\n  onEmbed,\n  tokenCount,\n  maxTokens,\n  fileCount = 1,\n  isEmbedding = false,\n  embedProgress = 0,\n}) {\n  const { user } = useUser();\n  const canEmbed = !user || user.role !== \"default\";\n  if (!show) return null;\n\n  if (isEmbedding) {\n    return (\n      <ModalWrapper isOpen={show}>\n        <div className=\"relative max-w-[600px] bg-theme-bg-primary rounded-lg shadow border border-theme-modal-border\">\n          <div className=\"p-6 flex flex-col items-center justify-center\">\n            <p className=\"text-white text-lg font-semibold mb-4\">\n              Embedding {embedProgress + 1} of {fileCount}{\" \"}\n              {pluralize(\"file\", fileCount)}\n            </p>\n            <CircleNotch size={32} className=\"animate-spin text-white\" />\n            <p className=\"text-white/60 text-sm mt-2\">\n              Please wait while we embed your files...\n            </p>\n          </div>\n        </div>\n      </ModalWrapper>\n    );\n  }\n\n  return (\n    <ModalWrapper isOpen={show}>\n      <div className=\"relative max-w-[600px] bg-theme-bg-primary rounded-lg shadow border border-theme-modal-border\">\n        <div className=\"relative p-6 border-b border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Context Window Warning\n            </h3>\n          </div>\n        </div>\n\n        <div className=\"py-7 px-9 space-y-4\">\n          <p className=\"text-theme-text-primary text-sm\">\n            Your workspace is using {numberWithCommas(tokenCount)} of{\" \"}\n            {numberWithCommas(maxTokens)} available tokens. We recommend keeping\n            usage below {(Workspace.maxContextWindowLimit * 100).toFixed(0)}% to\n            ensure the best chat experience. Adding {fileCount} more{\" \"}\n            {pluralize(\"file\", fileCount)} would exceed this limit.{\" \"}\n            <Link\n              target=\"_blank\"\n              to={Paths.documentation.contextWindows()}\n              className=\"text-theme-text-secondary text-sm underline\"\n            >\n              Learn more about context windows &rarr;\n            </Link>\n          </p>\n          <p className=\"text-theme-text-primary text-sm\">\n            Choose how you would like to proceed with these uploads.\n          </p>\n        </div>\n\n        <div className=\"flex w-full justify-between items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b\">\n          <button\n            onClick={onClose}\n            type=\"button\"\n            className=\"border-none transition-all duration-300 bg-theme-modal-border text-white hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n          >\n            Cancel\n          </button>\n          <div className=\"flex w-full justify-end items-center space-x-2\">\n            <button\n              onClick={onContinue}\n              type=\"button\"\n              className=\"border-none transition-all duration-300 bg-theme-modal-border text-white hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n            >\n              Continue Anyway\n            </button>\n            {canEmbed && (\n              <button\n                onClick={onEmbed}\n                disabled={isEmbedding || !canEmbed}\n                type=\"button\"\n                className=\"border-none transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                Embed {pluralize(\"File\", fileCount)}\n              </button>\n            )}\n          </div>\n        </div>\n      </div>\n    </ModalWrapper>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/DnDWrapper/index.jsx",
    "content": "import { useState, useEffect, createContext, useContext } from \"react\";\nimport { v4 } from \"uuid\";\nimport System from \"@/models/system\";\nimport { useDropzone } from \"react-dropzone\";\nimport DndIcon from \"./dnd-icon.png\";\nimport Workspace from \"@/models/workspace\";\nimport showToast from \"@/utils/toast\";\nimport FileUploadWarningModal from \"./FileUploadWarningModal\";\nimport pluralize from \"pluralize\";\n\nexport const DndUploaderContext = createContext();\nexport const REMOVE_ATTACHMENT_EVENT = \"ATTACHMENT_REMOVE\";\nexport const CLEAR_ATTACHMENTS_EVENT = \"ATTACHMENT_CLEAR\";\nexport const PASTE_ATTACHMENT_EVENT = \"ATTACHMENT_PASTED\";\nexport const ATTACHMENTS_PROCESSING_EVENT = \"ATTACHMENTS_PROCESSING\";\nexport const ATTACHMENTS_PROCESSED_EVENT = \"ATTACHMENTS_PROCESSED\";\nexport const PARSED_FILE_ATTACHMENT_REMOVED_EVENT =\n  \"PARSED_FILE_ATTACHMENT_REMOVED\";\n\n/**\n * File Attachment for automatic upload on the chat container page.\n * @typedef Attachment\n * @property {string} uid - unique file id.\n * @property {File} file - native File object\n * @property {string|null} contentString - base64 encoded string of file\n * @property {('in_progress'|'failed'|'embedded'|'added_context')} status - the automatic upload status.\n * @property {string|null} error - Error message\n * @property {{id:string, location:string}|null} document - uploaded document details\n * @property {('attachment'|'upload')} type - The type of upload. Attachments are chat-specific, uploads go to the workspace.\n */\n\n/**\n * @typedef {Object} ParsedFile\n * @property {number} id - The id of the parsed file.\n * @property {string} filename - The name of the parsed file.\n * @property {number} workspaceId - The id of the workspace the parsed file belongs to.\n * @property {string|null} userId - The id of the user the parsed file belongs to.\n * @property {string|null} threadId - The id of the thread the parsed file belongs to.\n * @property {string} metadata - The metadata of the parsed file.\n * @property {number} tokenCountEstimate - The estimated token count of the parsed file.\n */\n\nexport function DnDFileUploaderProvider({\n  workspace,\n  threadSlug = null,\n  children,\n}) {\n  const [files, setFiles] = useState([]);\n  const [ready, setReady] = useState(false);\n  const [dragging, setDragging] = useState(false);\n  const [showWarningModal, setShowWarningModal] = useState(false);\n  const [isEmbedding, setIsEmbedding] = useState(false);\n  const [embedProgress, setEmbedProgress] = useState(0);\n  const [pendingFiles, setPendingFiles] = useState([]);\n  const [tokenCount, setTokenCount] = useState(0);\n  const [maxTokens, setMaxTokens] = useState(Number.POSITIVE_INFINITY);\n\n  useEffect(() => {\n    System.checkDocumentProcessorOnline().then((status) => setReady(status));\n  }, []);\n\n  useEffect(() => {\n    window.addEventListener(REMOVE_ATTACHMENT_EVENT, handleRemove);\n    window.addEventListener(CLEAR_ATTACHMENTS_EVENT, resetAttachments);\n    window.addEventListener(PASTE_ATTACHMENT_EVENT, handlePastedAttachment);\n    window.addEventListener(\n      PARSED_FILE_ATTACHMENT_REMOVED_EVENT,\n      handleRemoveParsedFile\n    );\n\n    return () => {\n      window.removeEventListener(REMOVE_ATTACHMENT_EVENT, handleRemove);\n      window.removeEventListener(CLEAR_ATTACHMENTS_EVENT, resetAttachments);\n      window.removeEventListener(\n        PARSED_FILE_ATTACHMENT_REMOVED_EVENT,\n        handleRemoveParsedFile\n      );\n      window.removeEventListener(\n        PASTE_ATTACHMENT_EVENT,\n        handlePastedAttachment\n      );\n    };\n  }, []);\n\n  /**\n   * Handles the removal of a parsed file attachment from the uploader queue.\n   * Only uses the document id to remove the file from the queue\n   * @param {CustomEvent<{document: ParsedFile}>} event\n   */\n  async function handleRemoveParsedFile(event) {\n    const { document } = event.detail;\n    setFiles((prev) =>\n      prev.filter((prevFile) => prevFile.document.id !== document.id)\n    );\n  }\n\n  /**\n   * Remove file from uploader queue.\n   * @param {CustomEvent<{uid: string}>} event\n   */\n  async function handleRemove(event) {\n    /** @type {{uid: Attachment['uid'], document: Attachment['document']}} */\n    const { uid, document } = event.detail;\n    setFiles((prev) => prev.filter((prevFile) => prevFile.uid !== uid));\n    if (!document?.location) return;\n    await Workspace.deleteAndUnembedFile(workspace.slug, document.location);\n  }\n\n  /**\n   * Clear queue of attached files currently in prompt box\n   */\n  function resetAttachments() {\n    setFiles([]);\n  }\n\n  /**\n   * Turns files into attachments we can send as body request to backend\n   * for a chat.\n   * @returns {{name:string,mime:string,contentString:string}[]}\n   */\n  function parseAttachments() {\n    return (\n      files\n        ?.filter((file) => file.type === \"attachment\")\n        ?.map(\n          (\n            /** @type {Attachment} */\n            attachment\n          ) => {\n            return {\n              name: attachment.file.name,\n              mime: attachment.file.type,\n              contentString: attachment.contentString,\n            };\n          }\n        ) || []\n    );\n  }\n\n  /**\n   * Handle pasted attachments.\n   * @param {CustomEvent<{files: File[]}>} event\n   */\n  async function handlePastedAttachment(event) {\n    const { files = [] } = event.detail;\n    if (!files.length) return;\n    const newAccepted = [];\n    for (const file of files) {\n      if (file.type.startsWith(\"image/\")) {\n        newAccepted.push({\n          uid: v4(),\n          file,\n          contentString: await toBase64(file),\n          status: \"success\",\n          error: null,\n          type: \"attachment\",\n        });\n      } else {\n        newAccepted.push({\n          uid: v4(),\n          file,\n          contentString: null,\n          status: \"in_progress\",\n          error: null,\n          type: \"upload\",\n        });\n      }\n    }\n    setFiles((prev) => [...prev, ...newAccepted]);\n    embedEligibleAttachments(newAccepted);\n  }\n\n  /**\n   * Handle dropped files.\n   * @param {Attachment[]} acceptedFiles\n   * @param {any[]} _rejections\n   */\n  async function onDrop(acceptedFiles, _rejections) {\n    setDragging(false);\n\n    /** @type {Attachment[]} */\n    const newAccepted = [];\n    for (const file of acceptedFiles) {\n      if (file.type.startsWith(\"image/\")) {\n        newAccepted.push({\n          uid: v4(),\n          file,\n          contentString: await toBase64(file),\n          status: \"success\",\n          error: null,\n          type: \"attachment\",\n        });\n      } else {\n        newAccepted.push({\n          uid: v4(),\n          file,\n          contentString: null,\n          status: \"in_progress\",\n          error: null,\n          type: \"upload\",\n        });\n      }\n    }\n\n    setFiles((prev) => [...prev, ...newAccepted]);\n    embedEligibleAttachments(newAccepted);\n  }\n\n  /**\n   * Embeds attachments that are eligible for embedding - basically files that are not images.\n   * @param {Attachment[]} newAttachments\n   */\n  async function embedEligibleAttachments(newAttachments = []) {\n    window.dispatchEvent(new CustomEvent(ATTACHMENTS_PROCESSING_EVENT));\n    const promises = [];\n\n    const { currentContextTokenCount, contextWindow } =\n      await Workspace.getParsedFiles(workspace.slug, threadSlug);\n    const workspaceContextWindow = contextWindow\n      ? Math.floor(contextWindow * Workspace.maxContextWindowLimit)\n      : Number.POSITIVE_INFINITY;\n    setMaxTokens(workspaceContextWindow);\n\n    let totalTokenCount = currentContextTokenCount;\n    let batchPendingFiles = [];\n\n    for (const attachment of newAttachments) {\n      // Images/attachments are chat specific.\n      if (attachment.type === \"attachment\") continue;\n\n      const formData = new FormData();\n      formData.append(\"file\", attachment.file, attachment.file.name);\n      formData.append(\"threadSlug\", threadSlug || null);\n      promises.push(\n        Workspace.parseFile(workspace.slug, formData).then(\n          async ({ response, data }) => {\n            if (!response.ok) {\n              const updates = {\n                status: \"failed\",\n                error: data?.error ?? null,\n              };\n              setFiles((prev) =>\n                prev.map(\n                  (\n                    /** @type {Attachment} */\n                    prevFile\n                  ) =>\n                    prevFile.uid !== attachment.uid\n                      ? prevFile\n                      : { ...prevFile, ...updates }\n                )\n              );\n              return;\n            }\n            // Will always be one file in the array\n            /** @type {ParsedFile} */\n            const file = data.files[0];\n\n            // Add token count for this file\n            // and add it to the batch pending files\n            totalTokenCount += file.tokenCountEstimate;\n            batchPendingFiles.push({\n              attachment,\n              parsedFileId: file.id,\n              tokenCount: file.tokenCountEstimate,\n            });\n\n            if (totalTokenCount > workspaceContextWindow) {\n              setTokenCount(totalTokenCount);\n              setPendingFiles(batchPendingFiles);\n              setShowWarningModal(true);\n              return;\n            }\n\n            // File is within limits, keep in parsed files\n            const result = { success: true, document: file };\n            const updates = {\n              status: result.success ? \"added_context\" : \"failed\",\n              error: result.error ?? null,\n              document: result.document,\n            };\n\n            setFiles((prev) =>\n              prev.map(\n                (\n                  /** @type {Attachment} */\n                  prevFile\n                ) =>\n                  prevFile.uid !== attachment.uid\n                    ? prevFile\n                    : { ...prevFile, ...updates }\n              )\n            );\n          }\n        )\n      );\n    }\n\n    // Wait for all promises to resolve in some way before dispatching the event to unlock the send button\n    Promise.all(promises).finally(() =>\n      window.dispatchEvent(new CustomEvent(ATTACHMENTS_PROCESSED_EVENT))\n    );\n  }\n\n  // Handle modal actions\n  const handleCloseModal = async () => {\n    if (!pendingFiles.length) return;\n\n    // Delete all files from this batch\n    await Workspace.deleteParsedFiles(\n      workspace.slug,\n      pendingFiles.map((file) => file.parsedFileId)\n    );\n\n    // Remove all files from this batch from the UI\n    setFiles((prev) =>\n      prev.filter(\n        (prevFile) =>\n          !pendingFiles.some((file) => file.attachment.uid === prevFile.uid)\n      )\n    );\n    setShowWarningModal(false);\n    setPendingFiles([]);\n    setTokenCount(0);\n    window.dispatchEvent(new CustomEvent(ATTACHMENTS_PROCESSED_EVENT));\n  };\n\n  const handleContinueAnyway = async () => {\n    if (!pendingFiles.length) return;\n    const results = pendingFiles.map((file) => ({\n      success: true,\n      document: { id: file.parsedFileId },\n    }));\n\n    const fileUpdates = pendingFiles.map((file, i) => ({\n      uid: file.attachment.uid,\n      updates: {\n        status: results[i].success ? \"success\" : \"failed\",\n        error: results[i].error ?? null,\n        document: results[i].document,\n      },\n    }));\n\n    setFiles((prev) =>\n      prev.map((prevFile) => {\n        const update = fileUpdates.find((f) => f.uid === prevFile.uid);\n        return update ? { ...prevFile, ...update.updates } : prevFile;\n      })\n    );\n    setShowWarningModal(false);\n    setPendingFiles([]);\n    setTokenCount(0);\n  };\n\n  const handleEmbed = async () => {\n    if (!pendingFiles.length) return;\n    setIsEmbedding(true);\n    setEmbedProgress(0);\n\n    // Embed all pending files\n    let completed = 0;\n    const results = await Promise.all(\n      pendingFiles.map((file) =>\n        Workspace.embedParsedFile(workspace.slug, file.parsedFileId).then(\n          (result) => {\n            completed++;\n            setEmbedProgress(completed);\n            return result;\n          }\n        )\n      )\n    );\n\n    // Update status for all files\n    const fileUpdates = pendingFiles.map((file, i) => ({\n      uid: file.attachment.uid,\n      updates: {\n        status: results[i].response.ok ? \"embedded\" : \"failed\",\n        error: results[i].data?.error ?? null,\n        document: results[i].data?.document,\n      },\n    }));\n\n    setFiles((prev) =>\n      prev.map((prevFile) => {\n        const update = fileUpdates.find((f) => f.uid === prevFile.uid);\n        return update ? { ...prevFile, ...update.updates } : prevFile;\n      })\n    );\n    setShowWarningModal(false);\n    setPendingFiles([]);\n    setTokenCount(0);\n    setIsEmbedding(false);\n    window.dispatchEvent(new CustomEvent(ATTACHMENTS_PROCESSED_EVENT));\n    showToast(\n      `${pendingFiles.length} ${pluralize(\"file\", pendingFiles.length)} embedded successfully`,\n      \"success\"\n    );\n  };\n\n  return (\n    <DndUploaderContext.Provider\n      value={{ files, ready, dragging, setDragging, onDrop, parseAttachments }}\n    >\n      <FileUploadWarningModal\n        show={showWarningModal}\n        onClose={handleCloseModal}\n        onContinue={handleContinueAnyway}\n        onEmbed={handleEmbed}\n        tokenCount={tokenCount}\n        maxTokens={maxTokens}\n        fileCount={pendingFiles.length}\n        isEmbedding={isEmbedding}\n        embedProgress={embedProgress}\n      />\n      {children}\n    </DndUploaderContext.Provider>\n  );\n}\n\nexport default function DnDFileUploaderWrapper({ children }) {\n  const { onDrop, ready, dragging, setDragging } =\n    useContext(DndUploaderContext);\n  const { getRootProps, getInputProps } = useDropzone({\n    onDrop,\n    disabled: !ready,\n    noClick: true,\n    noKeyboard: true,\n    onDragEnter: () => setDragging(true),\n    onDragLeave: () => setDragging(false),\n  });\n\n  return (\n    <div\n      className={`relative flex flex-col h-full w-full md:mt-0 mt-[40px] p-[1px]`}\n      {...getRootProps()}\n    >\n      <div\n        hidden={!dragging}\n        className=\"absolute top-0 w-full h-full bg-dark-text/90 light:bg-[#C2E7FE]/90 rounded-2xl border-[4px] border-white z-[9999]\"\n      >\n        <div className=\"w-full h-full flex justify-center items-center rounded-xl\">\n          <div className=\"flex flex-col gap-y-[14px] justify-center items-center\">\n            <img\n              src={DndIcon}\n              width={69}\n              height={69}\n              alt=\"Drag and drop icon\"\n            />\n            <p className=\"text-white text-[24px] font-semibold\">Add anything</p>\n            <p className=\"text-white text-[16px] text-center\">\n              Drop a file or image here to attach it to your <br />\n              workspace auto-magically.\n            </p>\n          </div>\n        </div>\n      </div>\n      <input id=\"dnd-chat-file-uploader\" {...getInputProps()} />\n      {children}\n    </div>\n  );\n}\n\n/**\n * Convert image types into Base64 strings for requests.\n * @param {File} file\n * @returns {Promise<string>}\n */\nasync function toBase64(file) {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => {\n      const base64String = reader.result.split(\",\")[1];\n      resolve(`data:${file.type};base64,${base64String}`);\n    };\n    reader.onerror = (error) => reject(error);\n    reader.readAsDataURL(file);\n  });\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AgentMenu/index.jsx",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport { Tooltip } from \"react-tooltip\";\nimport { At } from \"@phosphor-icons/react\";\nimport { useIsAgentSessionActive } from \"@/utils/chat/agent\";\nimport { useTranslation } from \"react-i18next\";\nimport { useSearchParams } from \"react-router-dom\";\n\nexport default function AvailableAgentsButton({ showing, setShowAgents }) {\n  const { t } = useTranslation();\n  const agentSessionActive = useIsAgentSessionActive();\n  if (agentSessionActive) return null;\n  return (\n    <div\n      id=\"agent-list-btn\"\n      data-tooltip-id=\"tooltip-agent-list-btn\"\n      data-tooltip-content={t(\"chat_window.agents\")}\n      aria-label={t(\"chat_window.agents\")}\n      onClick={() => setShowAgents(!showing)}\n      className={`flex justify-center items-center cursor-pointer opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 ${\n        showing ? \"!opacity-100\" : \"\"\n      }`}\n    >\n      <At\n        color=\"var(--theme-sidebar-footer-icon-fill)\"\n        className=\"w-[20px] h-[20px] pointer-events-none text-theme-text-primary\"\n      />\n      <Tooltip\n        id=\"tooltip-agent-list-btn\"\n        place=\"top\"\n        delayShow={300}\n        className=\"tooltip !text-xs z-99\"\n      />\n    </div>\n  );\n}\n\nexport function AvailableAgents({\n  showing,\n  setShowing,\n  sendCommand,\n  promptRef,\n  centered = false,\n}) {\n  const formRef = useRef(null);\n  const agentSessionActive = useIsAgentSessionActive();\n  const [searchParams] = useSearchParams();\n  const { t } = useTranslation();\n\n  /*\n   * @checklist-item\n   * If the URL has the agent param, open the agent menu for the user\n   * automatically when the component mounts.\n   */\n  useEffect(() => {\n    if (searchParams.get(\"action\") === \"set-agent-chat\" && !showing)\n      handleAgentClick();\n  }, [promptRef.current]);\n\n  useEffect(() => {\n    function listenForOutsideClick() {\n      if (!showing || !formRef.current) return false;\n      document.addEventListener(\"click\", closeIfOutside);\n    }\n    listenForOutsideClick();\n  }, [showing, formRef.current]);\n\n  const closeIfOutside = ({ target }) => {\n    if (target.id === \"agent-list-btn\") return;\n    const isOutside = !formRef?.current?.contains(target);\n    if (!isOutside) return;\n    setShowing(false);\n  };\n\n  const handleAgentClick = () => {\n    setShowing(false);\n    sendCommand({ text: \"@agent \" });\n    promptRef?.current?.focus();\n  };\n\n  if (agentSessionActive) return null;\n  return (\n    <>\n      <div hidden={!showing}>\n        <div\n          className={\n            centered\n              ? \"w-full flex justify-center md:justify-start absolute top-full mt-2 left-0 z-10 px-4 md:px-0 md:pl-[57px]\"\n              : \"flex justify-center md:justify-start absolute bottom-[130px] md:bottom-[150px] left-0 right-0 z-10 max-w-[750px] mx-auto px-4 md:px-0 md:pl-[57px]\"\n          }\n        >\n          <div\n            ref={formRef}\n            className=\"w-[600px] p-2 bg-theme-action-menu-bg rounded-2xl shadow flex-col justify-center items-start gap-2.5 inline-flex overflow-y-auto max-h-[200px] no-scroll\"\n          >\n            <button\n              onClick={handleAgentClick}\n              className=\"border-none w-full hover:cursor-pointer hover:bg-theme-action-menu-item-hover px-2 py-2 rounded-xl flex flex-col justify-start group\"\n            >\n              <div className=\"w-full flex-col text-left flex pointer-events-none\">\n                <div className=\"text-theme-text-primary text-sm\">\n                  <b>{t(\"chat_window.at_agent\")}</b>\n                  {t(\"chat_window.default_agent_description\")}\n                </div>\n              </div>\n            </button>\n          </div>\n        </div>\n      </div>\n    </>\n  );\n}\n\nexport function useAvailableAgents() {\n  const [showAgents, setShowAgents] = useState(false);\n  return { showAgents, setShowAgents };\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AttachItem/ParsedFilesMenu/index.jsx",
    "content": "import { useState } from \"react\";\nimport { X, CircleNotch, Warning } from \"@phosphor-icons/react\";\nimport Workspace from \"@/models/workspace\";\nimport { nFormatter } from \"@/utils/numbers\";\nimport showToast from \"@/utils/toast\";\nimport pluralize from \"pluralize\";\nimport { PARSED_FILE_ATTACHMENT_REMOVED_EVENT } from \"../../../DnDWrapper\";\nimport useUser from \"@/hooks/useUser\";\n\nexport default function ParsedFilesMenu({\n  onEmbeddingChange,\n  tooltipRef,\n  files,\n  setFiles,\n  currentTokens,\n  setCurrentTokens,\n  contextWindow,\n  isLoading,\n  workspaceSlug,\n  threadSlug = null,\n}) {\n  const { user } = useUser();\n  const canEmbed = !user || user.role !== \"default\";\n  const initialContextWindowLimitExceeded =\n    contextWindow &&\n    currentTokens >= contextWindow * Workspace.maxContextWindowLimit;\n  const [isEmbedding, setIsEmbedding] = useState(false);\n  const [embedProgress, setEmbedProgress] = useState(1);\n  const [contextWindowLimitExceeded, setContextWindowLimitExceeded] = useState(\n    initialContextWindowLimitExceeded\n  );\n\n  async function handleRemove(e, file) {\n    e.preventDefault();\n    e.stopPropagation();\n    if (!file?.id) return;\n\n    const success = await Workspace.deleteParsedFiles(workspaceSlug, [file.id]);\n    if (!success) return;\n\n    // Update the local files list and current tokens\n    setFiles((prev) => prev.filter((f) => f.id !== file.id));\n\n    // Dispatch an event to the DnDFileUploaderWrapper to update the files list in attachment manager if it exists\n    window.dispatchEvent(\n      new CustomEvent(PARSED_FILE_ATTACHMENT_REMOVED_EVENT, {\n        detail: { document: file },\n      })\n    );\n    const { currentContextTokenCount } = await Workspace.getParsedFiles(\n      workspaceSlug,\n      threadSlug\n    );\n    const newContextWindowLimitExceeded =\n      contextWindow &&\n      currentContextTokenCount >=\n        contextWindow * Workspace.maxContextWindowLimit;\n    setCurrentTokens(currentContextTokenCount);\n    setContextWindowLimitExceeded(newContextWindowLimitExceeded);\n  }\n\n  /**\n   * Handles the embedding of the files when the user exceeds the context window limit\n   * and opts to embed the files into the workspace instead.\n   * @returns {Promise<void>}\n   */\n  async function handleEmbed() {\n    if (!files.length) return;\n    setIsEmbedding(true);\n    onEmbeddingChange?.(true);\n    setEmbedProgress(1);\n    try {\n      let completed = 0;\n      await Promise.all(\n        files.map((file) =>\n          Workspace.embedParsedFile(workspaceSlug, file.id).then(() => {\n            completed++;\n            setEmbedProgress(completed + 1);\n          })\n        )\n      );\n      setFiles([]);\n      const { currentContextTokenCount } = await Workspace.getParsedFiles(\n        workspaceSlug,\n        threadSlug\n      );\n      setCurrentTokens(currentContextTokenCount);\n      setContextWindowLimitExceeded(\n        currentContextTokenCount >=\n          contextWindow * Workspace.maxContextWindowLimit\n      );\n      showToast(\n        `${files.length} ${pluralize(\"file\", files.length)} embedded successfully`,\n        \"success\"\n      );\n      tooltipRef?.current?.close();\n    } catch (error) {\n      console.error(\"Failed to embed files:\", error);\n      showToast(\"Failed to embed files\", \"error\");\n    }\n    setIsEmbedding(false);\n    onEmbeddingChange?.(false);\n    setEmbedProgress(1);\n  }\n\n  return (\n    <div className=\"flex flex-col gap-2 p-2\">\n      <div className=\"flex items-center justify-between\">\n        <div className=\"text-sm font-medium text-theme-text-primary\">\n          Current Context ({files.length} files)\n        </div>\n        <div\n          // If the user cannot see the embed CTA, show a tooltip\n          {...(contextWindowLimitExceeded &&\n            !canEmbed && {\n              \"data-tooltip-id\": \"context-window-limit-exceeded\",\n              \"data-tooltip-content\":\n                \"You have exceeded the context window limit. Some files may be truncated or excluded from chat responses. Responses may hallucinate or lack relevant information.\",\n            })}\n          className={`flex items-center gap-x-1 ${contextWindowLimitExceeded && !canEmbed ? \"cursor-pointer\" : \"\"}`}\n        >\n          {contextWindowLimitExceeded && (\n            <Warning size={14} className=\"text-orange-600\" />\n          )}\n          <div\n            className={`text-xs ${contextWindowLimitExceeded ? \"text-orange-600\" : \"text-theme-text-secondary\"}`}\n          >\n            {nFormatter(currentTokens)} /{\" \"}\n            {contextWindow ? nFormatter(contextWindow) : \"--\"} tokens\n          </div>\n        </div>\n      </div>\n      {contextWindowLimitExceeded && canEmbed && (\n        <div className=\"flex flex-col gap-2 p-2 bg-theme-bg-secondary light:bg-theme-bg-primary rounded\">\n          <div className=\"flex items-start gap-2\">\n            <Warning\n              className=\"flex-shrink-0 mt-1 text-yellow-500 light:text-yellow-600\"\n              size={16}\n            />\n            <div className=\"text-xs text-theme-text-primary\">\n              Your context window is getting full. Some files may be truncated\n              or excluded from chat responses. We recommend embedding these\n              files directly into your workspace for better results.\n            </div>\n          </div>\n          <button\n            onClick={handleEmbed}\n            disabled={isEmbedding}\n            className=\"border-none disabled:opacity-50 flex items-center justify-center gap-2 px-3 py-2 text-xs bg-primary-button hover:bg-theme-button-primary-hover text-white font-medium rounded transition-colors shadow-sm\"\n          >\n            {isEmbedding ? (\n              <>\n                <CircleNotch size={14} className=\"animate-spin\" />\n                Embedding {embedProgress} of {files.length} files...\n              </>\n            ) : (\n              \"Embed Files into Workspace\"\n            )}\n          </button>\n        </div>\n      )}\n      <div className=\"flex flex-col gap-1 max-h-[300px] overflow-y-auto\">\n        {files.length > 0 &&\n          files.map((file, i) => (\n            <div\n              key={i}\n              className={\n                \"flex items-center justify-between gap-2 p-2 text-xs bg-theme-bg-secondary rounded\"\n              }\n            >\n              <div className=\"truncate flex-1 text-theme-text-primary\">\n                {file.title}\n              </div>\n              <button\n                onClick={(e) => handleRemove(e, file)}\n                className=\"border-none text-theme-text-secondary hover:text-theme-text-primary\"\n                disabled={isEmbedding}\n              >\n                <X size={16} />\n              </button>\n            </div>\n          ))}\n        {isLoading && (\n          <div className=\"flex items-center justify-center gap-2 text-xs text-theme-text-secondary text-center py-2\">\n            <CircleNotch size={16} className=\"animate-spin\" />\n            Loading...\n          </div>\n        )}\n        {!isLoading && files.length === 0 && (\n          <div className=\"text-xs text-theme-text-secondary text-center py-2\">\n            No files found\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/AttachItem/index.jsx",
    "content": "import { Plus } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\nimport { useTranslation } from \"react-i18next\";\nimport { useRef, useState, useEffect } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport Workspace from \"@/models/workspace\";\nimport {\n  ATTACHMENTS_PROCESSED_EVENT,\n  REMOVE_ATTACHMENT_EVENT,\n} from \"../../DnDWrapper\";\nimport { useTheme } from \"@/hooks/useTheme\";\nimport ParsedFilesMenu from \"./ParsedFilesMenu\";\n\n/**\n * This is a simple proxy component that clicks on the DnD file uploader for the user.\n * @returns\n */\nexport default function AttachItem({\n  workspaceSlug = null,\n  workspaceThreadSlug = null,\n}) {\n  const { t } = useTranslation();\n  const { theme } = useTheme();\n  const params = useParams();\n  const slug = workspaceSlug || params.slug;\n  const threadSlug = workspaceThreadSlug ?? params.threadSlug ?? null;\n  const tooltipRef = useRef(null);\n  const [isEmbedding, setIsEmbedding] = useState(false);\n  const [files, setFiles] = useState([]);\n  const [currentTokens, setCurrentTokens] = useState(0);\n  const [contextWindow, setContextWindow] = useState(Infinity);\n  const [showTooltip, setShowTooltip] = useState(false);\n  const [isLoading, setIsLoading] = useState(true);\n\n  const fetchFiles = () => {\n    if (!slug) return;\n    if (isEmbedding) return;\n    setIsLoading(true);\n    Workspace.getParsedFiles(slug, threadSlug)\n      .then(({ files, contextWindow, currentContextTokenCount }) => {\n        setFiles(files);\n        setShowTooltip(files.length > 0);\n        setContextWindow(contextWindow);\n        setCurrentTokens(currentContextTokenCount);\n      })\n      .finally(() => {\n        setIsLoading(false);\n      });\n  };\n\n  /**\n   * Handles the removal of an attachment from the parsed files\n   * and triggers a re-fetch of the parsed files.\n   * This function handles when the user clicks the X on an Attachment via the AttachmentManager\n   * so we need to sync the state in the ParsedFilesMenu picker here.\n   */\n  async function handleRemoveAttachment(e) {\n    const { document } = e.detail;\n    await Workspace.deleteParsedFiles(slug, [document.id]);\n    fetchFiles();\n  }\n\n  /**\n   * Handles the click event for the attach item button.\n   * @param {MouseEvent} e - The click event.\n   * @returns {void}\n   */\n  function handleClick(e) {\n    e?.target?.blur();\n    document?.getElementById(\"dnd-chat-file-uploader\")?.click();\n    return;\n  }\n\n  useEffect(() => {\n    fetchFiles();\n    window.addEventListener(ATTACHMENTS_PROCESSED_EVENT, fetchFiles);\n    window.addEventListener(REMOVE_ATTACHMENT_EVENT, handleRemoveAttachment);\n    return () => {\n      window.removeEventListener(ATTACHMENTS_PROCESSED_EVENT, fetchFiles);\n      window.removeEventListener(\n        REMOVE_ATTACHMENT_EVENT,\n        handleRemoveAttachment\n      );\n    };\n  }, [slug, threadSlug]);\n\n  return (\n    <>\n      <button\n        id=\"attach-item-btn\"\n        data-tooltip-id={\n          showTooltip ? \"tooltip-attach-item-btn\" : \"attach-item-btn\"\n        }\n        data-tooltip-content={\n          !showTooltip ? t(\"chat_window.attach_file\") : undefined\n        }\n        aria-label={t(\"chat_window.attach_file\")}\n        type=\"button\"\n        onClick={handleClick}\n        onPointerEnter={fetchFiles}\n        className=\"group border-none relative flex justify-center items-center cursor-pointer w-6 h-6 rounded-full hover:bg-zinc-700 light:hover:bg-slate-200\"\n      >\n        <div className=\"relative\">\n          <Plus\n            size={18}\n            className=\"pointer-events-none text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-600 shrink-0\"\n            weight=\"bold\"\n          />\n          {files.length > 0 && (\n            <div className=\"absolute -top-2.5 -right-2 bg-white text-black light:invert text-[8px] rounded-full px-1 flex items-center justify-center\">\n              {files.length}\n            </div>\n          )}\n        </div>\n      </button>\n      {showTooltip && (\n        <Tooltip\n          ref={tooltipRef}\n          id=\"tooltip-attach-item-btn\"\n          place=\"top\"\n          opacity={1}\n          clickable={!isEmbedding}\n          delayShow={300}\n          delayHide={isEmbedding ? 999999 : 800} // Prevent tooltip from hiding during embedding\n          arrowColor={\n            theme === \"light\"\n              ? \"var(--theme-modal-border)\"\n              : \"var(--theme-bg-primary)\"\n          }\n          className=\"z-99 !w-[400px] !bg-theme-bg-primary !px-[5px] !rounded-lg !pointer-events-auto light:border-2 light:border-theme-modal-border\"\n        >\n          <ParsedFilesMenu\n            onEmbeddingChange={setIsEmbedding}\n            tooltipRef={tooltipRef}\n            isLoading={isLoading}\n            files={files}\n            setFiles={setFiles}\n            currentTokens={currentTokens}\n            setCurrentTokens={setCurrentTokens}\n            contextWindow={contextWindow}\n            workspaceSlug={slug}\n            threadSlug={threadSlug}\n          />\n        </Tooltip>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/Attachments/index.jsx",
    "content": "import {\n  CircleNotch,\n  FileCode,\n  FileCsv,\n  FileDoc,\n  FileHtml,\n  FileText,\n  FileImage,\n  FilePdf,\n  WarningOctagon,\n  X,\n} from \"@phosphor-icons/react\";\nimport { REMOVE_ATTACHMENT_EVENT } from \"../../DnDWrapper\";\n\n/**\n * @param {{attachments: import(\"../../DnDWrapper\").Attachment[]}}\n * @returns\n */\nexport default function AttachmentManager({ attachments }) {\n  if (attachments.length === 0) return null;\n  return (\n    <div className=\"flex flex-wrap gap-2 mt-2 mb-4\">\n      {attachments.map((attachment) => (\n        <AttachmentItem key={attachment.uid} attachment={attachment} />\n      ))}\n    </div>\n  );\n}\n\n/**\n * @param {{attachment: import(\"../../DnDWrapper\").Attachment}}\n */\nfunction AttachmentItem({ attachment }) {\n  const { uid, file, status, error, document, type, contentString } =\n    attachment;\n  const { iconBgColor, Icon } = displayFromFile(file);\n\n  function removeFileFromQueue() {\n    window.dispatchEvent(\n      new CustomEvent(REMOVE_ATTACHMENT_EVENT, { detail: { uid, document } })\n    );\n  }\n\n  if (status === \"in_progress\") {\n    return (\n      <div className=\"relative flex items-center gap-x-1 rounded-lg bg-theme-attachment-bg border-none w-[180px] group\">\n        <div\n          className={`bg-theme-attachment-icon-spinner-bg rounded-md flex items-center justify-center flex-shrink-0 h-[32px] w-[32px] m-1`}\n        >\n          <CircleNotch\n            size={18}\n            weight=\"bold\"\n            className=\"text-theme-attachment-icon-spinner animate-spin\"\n          />\n        </div>\n        <div className=\"flex flex-col w-[125px]\">\n          <p className=\"text-theme-attachment-text text-xs font-semibold truncate\">\n            {file.name}\n          </p>\n          <p className=\"text-theme-attachment-text-secondary text-[10px] leading-[14px] font-medium\">\n            Uploading...\n          </p>\n        </div>\n      </div>\n    );\n  }\n\n  if (status === \"failed\") {\n    return (\n      <div\n        data-tooltip-id=\"attachment-status-tooltip\"\n        data-tooltip-content={error}\n        className={`relative flex items-center gap-x-1 rounded-lg bg-theme-attachment-error-bg border-none w-[180px] group`}\n      >\n        <div className=\"invisible group-hover:visible absolute -top-[5px] -right-[5px] w-fit h-fit z-[10]\">\n          <button\n            onClick={removeFileFromQueue}\n            type=\"button\"\n            className=\"bg-white hover:bg-error hover:text-theme-attachment-text rounded-full p-1 flex items-center justify-center hover:border-transparent border border-theme-attachment-bg\"\n          >\n            <X size={10} className=\"flex-shrink-0\" />\n          </button>\n        </div>\n        <div\n          className={`bg-error rounded-md flex items-center justify-center flex-shrink-0 h-[32px] w-[32px] m-1`}\n        >\n          <WarningOctagon size={24} className=\"text-theme-attachment-icon\" />\n        </div>\n        <div className=\"flex flex-col w-[125px]\">\n          <p className=\"text-theme-attachment-text text-xs font-semibold truncate\">\n            {file.name}\n          </p>\n          <p className=\"text-theme-attachment-text-secondary text-[10px] leading-[14px] font-medium truncate\">\n            {error ?? \"File not embedded!\"}\n          </p>\n        </div>\n      </div>\n    );\n  }\n\n  if (type === \"attachment\") {\n    if (contentString) {\n      return (\n        <div\n          data-tooltip-id=\"attachment-status-tooltip\"\n          data-tooltip-content={`${file.name} will be attached to this prompt. It will not be embedded into the workspace permanently.`}\n          className={`relative flex items-center gap-x-1 rounded-lg border-none group`}\n        >\n          <div className=\"invisible group-hover:visible absolute -top-[5px] -right-[5px] w-fit h-fit z-[10]\">\n            <button\n              onClick={removeFileFromQueue}\n              type=\"button\"\n              className=\"bg-white hover:bg-error hover:text-theme-attachment-text rounded-full p-1 flex items-center justify-center hover:border-transparent border border-theme-attachment-bg\"\n            >\n              <X size={10} className=\"flex-shrink-0\" />\n            </button>\n          </div>\n          <img\n            alt={`Preview of ${file.name}`}\n            src={contentString}\n            style={{ objectFit: \"cover\", objectPosition: \"center\" }}\n            className={`${iconBgColor} w-[40px] h-[40px] rounded-lg flex items-center justify-center`}\n          />\n        </div>\n      );\n    }\n\n    return (\n      <div\n        data-tooltip-id=\"attachment-status-tooltip\"\n        data-tooltip-content={`${file.name} will be attached to this prompt. It will not be embedded into the workspace permanently.`}\n        className={`relative flex items-center gap-x-1 rounded-lg bg-theme-attachment-success-bg border-none w-[180px] group`}\n      >\n        <div className=\"invisible group-hover:visible absolute -top-[5px] -right-[5px] w-fit h-fit z-[10]\">\n          <button\n            onClick={removeFileFromQueue}\n            type=\"button\"\n            className=\"bg-white hover:bg-error hover:text-theme-attachment-text rounded-full p-1 flex items-center justify-center hover:border-transparent border border-theme-attachment-bg\"\n          >\n            <X size={10} className=\"flex-shrink-0\" />\n          </button>\n        </div>\n        <div\n          className={`${iconBgColor} rounded-md flex items-center justify-center flex-shrink-0 h-[32px] w-[32px] m-1`}\n        >\n          <Icon size={24} className=\"text-theme-attachment-icon\" />\n        </div>\n        <div className=\"flex flex-col w-[125px]\">\n          <p className=\"text-theme-attachment-text text-xs font-semibold truncate\">\n            {file.name}\n          </p>\n          <p className=\"text-theme-attachment-text-secondary text-[10px] leading-[14px] font-medium\">\n            Image attached!\n          </p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      data-tooltip-id=\"attachment-status-tooltip\"\n      data-tooltip-content={\n        status === \"embedded\"\n          ? `${file.name} was uploaded and embedded into this workspace. It will be available for RAG chat now.`\n          : `${file.name} will be used as context for this chat only.`\n      }\n      className={`relative flex items-center gap-x-1 rounded-lg bg-theme-attachment-bg border-none w-[180px] group`}\n    >\n      <div className=\"invisible group-hover:visible absolute -top-[5px] -right-[5px] w-fit h-fit z-[10]\">\n        <button\n          onClick={removeFileFromQueue}\n          type=\"button\"\n          className=\"bg-white hover:bg-error hover:text-theme-attachment-text rounded-full p-1 flex items-center justify-center hover:border-transparent border border-theme-attachment-bg\"\n        >\n          <X size={10} className=\"flex-shrink-0\" />\n        </button>\n      </div>\n      <div\n        className={`${iconBgColor} rounded-md flex items-center justify-center flex-shrink-0 h-[32px] w-[32px] m-1`}\n      >\n        <Icon size={24} weight=\"light\" className=\"text-theme-attachment-icon\" />\n      </div>\n      <div className=\"flex flex-col w-[125px]\">\n        <p className=\"text-white text-xs font-semibold truncate\">{file.name}</p>\n        <p className=\"text-theme-attachment-text-secondary text-[10px] leading-[14px] font-medium\">\n          {status === \"embedded\" ? \"File embedded!\" : \"Added as context!\"}\n        </p>\n      </div>\n    </div>\n  );\n}\n\n/**\n * @param {File} file\n * @returns {{iconBgColor:string, Icon: React.Component}}\n */\nfunction displayFromFile(file) {\n  const extension = file?.name?.split(\".\")?.pop()?.toLowerCase() ?? \"txt\";\n  switch (extension) {\n    case \"pdf\":\n      return { iconBgColor: \"bg-magenta\", Icon: FilePdf };\n    case \"doc\":\n    case \"docx\":\n      return { iconBgColor: \"bg-royalblue\", Icon: FileDoc };\n    case \"html\":\n      return { iconBgColor: \"bg-purple\", Icon: FileHtml };\n    case \"csv\":\n    case \"xlsx\":\n      return { iconBgColor: \"bg-success\", Icon: FileCsv };\n    case \"json\":\n    case \"sql\":\n    case \"js\":\n    case \"jsx\":\n    case \"cpp\":\n    case \"c\":\n      return { iconBgColor: \"bg-warn\", Icon: FileCode };\n    case \"png\":\n    case \"jpg\":\n    case \"jpeg\":\n      return { iconBgColor: \"bg-royalblue\", Icon: FileImage };\n    default:\n      return { iconBgColor: \"bg-royalblue\", Icon: FileText };\n  }\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/ChatModelSelection/index.jsx",
    "content": "import useGetProviderModels, {\n  DISABLED_PROVIDERS,\n} from \"@/hooks/useGetProvidersModels\";\n\nexport default function ChatModelSelection({\n  provider,\n  setHasChanges,\n  selectedLLMModel,\n  setSelectedLLMModel,\n}) {\n  const { defaultModels, customModels, loading } =\n    useGetProviderModels(provider);\n  if (DISABLED_PROVIDERS.includes(provider)) return null;\n\n  if (loading) {\n    return (\n      <select\n        required={true}\n        disabled={true}\n        className=\"bg-zinc-900 light:bg-white text-white light:text-slate-900 text-sm rounded-lg h-8 w-full px-2.5 outline-none border border-zinc-900 light:border-slate-400 cursor-not-allowed\"\n      >\n        <option disabled={true} selected={true}>\n          -- waiting for models --\n        </option>\n      </select>\n    );\n  }\n\n  return (\n    <select\n      id=\"workspace-llm-model-select\"\n      required={true}\n      value={selectedLLMModel}\n      onChange={(e) => {\n        setHasChanges(true);\n        setSelectedLLMModel(e.target.value);\n      }}\n      className=\"bg-zinc-900 light:bg-white text-white light:text-slate-900 text-sm rounded-lg h-8 w-full px-2.5 outline-none border border-zinc-900 light:border-slate-400 cursor-pointer\"\n    >\n      {defaultModels.length > 0 && (\n        <optgroup label=\"General models\">\n          {defaultModels.map((model) => {\n            return (\n              <option\n                key={model}\n                value={model}\n                selected={selectedLLMModel === model}\n              >\n                {model}\n              </option>\n            );\n          })}\n        </optgroup>\n      )}\n      {Array.isArray(customModels) && customModels.length > 0 && (\n        <optgroup label=\"Discovered models\">\n          {customModels.map((model) => {\n            return (\n              <option\n                key={model.id}\n                value={model.id}\n                selected={selectedLLMModel === model.id}\n              >\n                {model.id}\n              </option>\n            );\n          })}\n        </optgroup>\n      )}\n      {/* For providers like TogetherAi where we partition model by creator entity. */}\n      {!Array.isArray(customModels) && Object.keys(customModels).length > 0 && (\n        <>\n          {Object.entries(customModels).map(([organization, models]) => (\n            <optgroup key={organization} label={organization}>\n              {models.map((model) => (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={selectedLLMModel === model.id}\n                >\n                  {model.name}\n                </option>\n              ))}\n            </optgroup>\n          ))}\n        </>\n      )}\n    </select>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/LLMSelector/index.jsx",
    "content": "import { MagnifyingGlass } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function LLMSelectorSidePanel({\n  availableProviders,\n  selectedLLMProvider,\n  onSearchChange,\n  onProviderClick,\n}) {\n  const { t } = useTranslation();\n\n  return (\n    <div className=\"w-[40%] h-full flex flex-col gap-4 p-2 border-r border-zinc-700 light:border-slate-300\">\n      <div className=\"relative shrink-0 mx-2\">\n        <MagnifyingGlass\n          size={14}\n          className=\"absolute left-2.5 top-1/2 -translate-y-1/2 text-zinc-400 light:text-slate-400\"\n          weight=\"bold\"\n        />\n        <input\n          id=\"llm-search-input\"\n          type=\"search\"\n          placeholder={t(\"chat_window.workspace_llm_manager.search\")}\n          onChange={onSearchChange}\n          className=\"bg-zinc-900 light:bg-white text-white light:text-slate-900 placeholder:text-zinc-500 light:placeholder:text-slate-400 text-sm rounded-lg pl-8 pr-2.5 h-8 w-full outline-none border border-zinc-900 light:border-slate-400\"\n        />\n      </div>\n      <div className=\"flex flex-col gap-0 overflow-y-auto min-h-0 flex-1\">\n        {availableProviders.map((llm) => (\n          <button\n            key={llm.value}\n            type=\"button\"\n            data-llm-value={llm.value}\n            className={`border-none cursor-pointer flex gap-2 items-center px-2.5 py-1.5 rounded-md transition-colors ${\n              selectedLLMProvider === llm.value\n                ? \"bg-zinc-700 light:bg-slate-200\"\n                : \"hover:bg-zinc-700/50 light:hover:bg-slate-100 bg-transparent\"\n            }`}\n            onClick={() => onProviderClick(llm.value)}\n          >\n            <img\n              src={llm.logo}\n              alt={`${llm.name} logo`}\n              className=\"w-6 h-6 rounded\"\n            />\n            <span className=\"text-sm text-white light:text-slate-900\">\n              {llm.name}\n            </span>\n          </button>\n        ))}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/SetupProvider/index.jsx",
    "content": "import { createPortal } from \"react-dom\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { X, WarningCircle } from \"@phosphor-icons/react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function SetupProvider({\n  isOpen,\n  closeModal,\n  postSubmit,\n  settings,\n  llmProvider,\n}) {\n  if (!isOpen) return null;\n\n  async function handleUpdate(e) {\n    e.preventDefault();\n    e.stopPropagation();\n    const data = {};\n    const form = new FormData(e.target);\n    for (var [key, value] of form.entries()) data[key] = value;\n    const { error } = await System.updateSystem(data);\n    if (error) {\n      showToast(\n        `Failed to save ${llmProvider.name} settings: ${error}`,\n        \"error\"\n      );\n      return;\n    }\n\n    closeModal();\n    postSubmit();\n    return false;\n  }\n\n  return createPortal(\n    <ModalWrapper isOpen={isOpen}>\n      <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n        <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n          <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n            <div className=\"w-full flex gap-x-2 items-center\">\n              <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n                {llmProvider.name} Settings\n              </h3>\n            </div>\n            <button\n              onClick={closeModal}\n              type=\"button\"\n              className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n            >\n              <X size={24} weight=\"bold\" className=\"text-white\" />\n            </button>\n          </div>\n          <form id=\"provider-form\" onSubmit={handleUpdate}>\n            <div className=\"px-7 py-6\">\n              <div className=\"space-y-6 max-h-[60vh] overflow-y-auto p-1\">\n                <p className=\"text-sm text-white/60\">\n                  To use {llmProvider.name} as this workspace's LLM you need to\n                  set it up first.\n                </p>\n                <div>\n                  {llmProvider.options(settings, { credentialsOnly: true })}\n                </div>\n              </div>\n            </div>\n            <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border px-7 pb-6\">\n              <button\n                type=\"button\"\n                onClick={closeModal}\n                className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n              >\n                Cancel\n              </button>\n              <button\n                type=\"submit\"\n                form=\"provider-form\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                Save settings\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </ModalWrapper>,\n    document.body\n  );\n}\n\nexport function NoSetupWarning({ showing, onSetupClick }) {\n  const { t } = useTranslation();\n  if (!showing) return null;\n\n  return (\n    <div className=\"flex items-start gap-1.5\">\n      <WarningCircle\n        size={16}\n        className=\"text-white light:text-slate-800 shrink-0 mt-0.5\"\n      />\n      <p className=\"text-[13px] text-white light:text-slate-800 leading-5\">\n        {t(\"chat_window.workspace_llm_manager.missing_credentials\")}{\" \"}\n        <span\n          onClick={onSetupClick}\n          className=\"text-sky-400 font-semibold cursor-pointer hover:underline\"\n          role=\"button\"\n        >\n          {t(\n            \"chat_window.workspace_llm_manager.missing_credentials_description\"\n          )}\n        </span>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/action.jsx",
    "content": "import { Tooltip } from \"react-tooltip\";\nimport { Brain, CheckCircle } from \"@phosphor-icons/react\";\nimport LLMSelectorModal from \"./index\";\nimport { useTheme } from \"@/hooks/useTheme\";\nimport { useRef, useEffect, useState } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport useUser from \"@/hooks/useUser\";\nimport { useModal } from \"@/hooks/useModal\";\nimport SetupProvider from \"./SetupProvider\";\n\nexport const TOGGLE_LLM_SELECTOR_EVENT = \"toggle_llm_selector\";\nexport const SAVE_LLM_SELECTOR_EVENT = \"save_llm_selector\";\nexport const PROVIDER_SETUP_EVENT = \"provider_setup_requested\";\n\nexport default function LLMSelectorAction({ workspaceSlug = null }) {\n  const { slug: urlSlug } = useParams();\n  const slug = urlSlug ?? workspaceSlug;\n  const tooltipRef = useRef(null);\n  const { theme } = useTheme();\n  const { user } = useUser();\n  const [saved, setSaved] = useState(false);\n  const {\n    isOpen: isSetupProviderOpen,\n    openModal: openSetupProviderModal,\n    closeModal: closeSetupProviderModal,\n  } = useModal();\n  const [config, setConfig] = useState({\n    settings: {},\n    provider: null,\n  });\n\n  function toggleLLMSelectorTooltip() {\n    if (!tooltipRef.current) return;\n    tooltipRef.current.isOpen\n      ? tooltipRef.current.close()\n      : tooltipRef.current.open();\n  }\n\n  function handleSaveLLMSelector() {\n    if (!tooltipRef.current) return;\n    tooltipRef.current.close();\n    setSaved(true);\n  }\n\n  useEffect(() => {\n    window.addEventListener(\n      TOGGLE_LLM_SELECTOR_EVENT,\n      toggleLLMSelectorTooltip\n    );\n    window.addEventListener(SAVE_LLM_SELECTOR_EVENT, handleSaveLLMSelector);\n    return () => {\n      window.removeEventListener(\n        TOGGLE_LLM_SELECTOR_EVENT,\n        toggleLLMSelectorTooltip\n      );\n      window.removeEventListener(\n        SAVE_LLM_SELECTOR_EVENT,\n        handleSaveLLMSelector\n      );\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!saved) return;\n    setTimeout(() => {\n      setSaved(false);\n    }, 1500);\n  }, [saved]);\n\n  useEffect(() => {\n    function handleProviderSetupEvent(e) {\n      const { provider, settings } = e.detail;\n      setConfig({\n        settings,\n        provider,\n      });\n      setTimeout(() => {\n        openSetupProviderModal();\n      }, 300);\n    }\n\n    window.addEventListener(PROVIDER_SETUP_EVENT, handleProviderSetupEvent);\n    return () =>\n      window.removeEventListener(\n        PROVIDER_SETUP_EVENT,\n        handleProviderSetupEvent\n      );\n  }, []);\n\n  // This feature is disabled for multi-user instances where the user is not an admin\n  // This is because of the limitations of model selection currently and other nuances in controls.\n  if (!!user && user.role !== \"admin\") return null;\n  if (!slug) return null;\n\n  return (\n    <>\n      <div\n        id=\"llm-selector-btn\"\n        data-tooltip-id=\"tooltip-llm-selector-btn\"\n        aria-label=\"LLM Selector\"\n        className={`border-none relative flex justify-center items-center opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 cursor-pointer`}\n      >\n        {saved ? (\n          <CheckCircle className=\"w-[20px] h-[20px] pointer-events-none text-green-400\" />\n        ) : (\n          <Brain className=\"w-[20px] h-[20px] pointer-events-none text-[var(--theme-sidebar-footer-icon-fill)]\" />\n        )}\n      </div>\n      <Tooltip\n        ref={tooltipRef}\n        id=\"tooltip-llm-selector-btn\"\n        place=\"top\"\n        opacity={1}\n        clickable={true}\n        delayShow={300} // dont trigger tooltip instantly to not spam the UI\n        delayHide={800} // Prevent the travel time from icon to window hiding tooltip\n        arrowColor={\n          theme === \"light\"\n            ? \"var(--theme-modal-border)\"\n            : \"var(--theme-bg-primary)\"\n        }\n        className=\"z-99 !w-[500px] !bg-theme-bg-primary !px-[5px] !rounded-lg !pointer-events-auto light:border-2 light:border-theme-modal-border\"\n      >\n        <LLMSelectorModal tooltipRef={tooltipRef} workspaceSlug={slug} />\n      </Tooltip>\n      <SetupProvider\n        isOpen={isSetupProviderOpen}\n        closeModal={closeSetupProviderModal}\n        postSubmit={() => closeSetupProviderModal()}\n        settings={config.settings}\n        llmProvider={config.provider}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport PreLoader from \"@/components/Preloader\";\nimport ChatModelSelection from \"./ChatModelSelection\";\nimport { useTranslation } from \"react-i18next\";\nimport { PROVIDER_SETUP_EVENT, SAVE_LLM_SELECTOR_EVENT } from \"./action\";\nimport {\n  WORKSPACE_LLM_PROVIDERS,\n  autoScrollToSelectedLLMProvider,\n  hasMissingCredentials,\n  validatedModelSelection,\n} from \"./utils\";\nimport LLMSelectorSidePanel from \"./LLMSelector\";\nimport { NoSetupWarning } from \"./SetupProvider\";\nimport showToast from \"@/utils/toast\";\nimport Workspace from \"@/models/workspace\";\nimport System from \"@/models/system\";\n\nexport default function LLMSelectorModal({\n  workspaceSlug = null,\n  initialProvider = null,\n}) {\n  const { slug: urlSlug } = useParams();\n  const slug = urlSlug ?? workspaceSlug;\n  const { t } = useTranslation();\n  const [loading, setLoading] = useState(false);\n  const [settings, setSettings] = useState(null);\n  const [selectedLLMProvider, setSelectedLLMProvider] = useState(null);\n  const [selectedLLMModel, setSelectedLLMModel] = useState(\"\");\n  const [availableProviders, setAvailableProviders] = useState(\n    WORKSPACE_LLM_PROVIDERS\n  );\n  const [hasChanges, setHasChanges] = useState(false);\n  const [saving, setSaving] = useState(false);\n  const [missingCredentials, setMissingCredentials] = useState(false);\n\n  useEffect(() => {\n    if (!slug) return;\n    setLoading(true);\n    Promise.all([Workspace.bySlug(slug), System.keys()])\n      .then(([workspace, systemSettings]) => {\n        const savedProvider =\n          workspace.chatProvider ?? systemSettings.LLMProvider;\n        const savedModel = workspace.chatModel ?? systemSettings.LLMModel;\n        const providerToSelect = initialProvider ?? savedProvider;\n\n        setSettings(systemSettings);\n        setSelectedLLMProvider(providerToSelect);\n        autoScrollToSelectedLLMProvider(providerToSelect);\n        setSelectedLLMModel(savedModel);\n\n        if (initialProvider && initialProvider !== savedProvider) {\n          setHasChanges(true);\n          setMissingCredentials(\n            hasMissingCredentials(systemSettings, initialProvider)\n          );\n        }\n      })\n      .finally(() => setLoading(false));\n  }, [slug]);\n\n  function handleSearch(e) {\n    const searchTerm = e.target.value.toLowerCase();\n    const filteredProviders = WORKSPACE_LLM_PROVIDERS.filter((provider) =>\n      provider.name.toLowerCase().includes(searchTerm)\n    );\n    setAvailableProviders(filteredProviders);\n  }\n\n  function handleProviderSelection(provider) {\n    setSelectedLLMProvider(provider);\n    setAvailableProviders(WORKSPACE_LLM_PROVIDERS);\n    autoScrollToSelectedLLMProvider(provider, 50);\n    document.getElementById(\"llm-search-input\").value = \"\";\n    setHasChanges(true);\n    setMissingCredentials(hasMissingCredentials(settings, provider));\n  }\n\n  async function handleSave() {\n    setSaving(true);\n    try {\n      setHasChanges(false);\n      const validatedModel = validatedModelSelection(selectedLLMModel);\n      if (!validatedModel) throw new Error(\"Invalid model selection\");\n\n      const { message } = await Workspace.update(slug, {\n        chatProvider: selectedLLMProvider,\n        chatModel: validatedModel,\n      });\n\n      if (!!message) throw new Error(message);\n      window.dispatchEvent(new Event(SAVE_LLM_SELECTOR_EVENT));\n    } catch (error) {\n      console.error(error);\n      showToast(error.message, \"error\", { clear: true });\n    } finally {\n      setSaving(false);\n    }\n  }\n\n  const providerName =\n    WORKSPACE_LLM_PROVIDERS.find((p) => p.value === selectedLLMProvider)\n      ?.name || selectedLLMProvider;\n\n  if (loading) {\n    return (\n      <div\n        id=\"llm-selector-modal\"\n        className=\"w-full h-[388px] flex flex-col items-center justify-center gap-2\"\n      >\n        <PreLoader size={12} />\n        <p className=\"text-zinc-400 light:text-slate-500 text-sm\">\n          {t(\"chat_window.workspace_llm_manager.loading_workspace_settings\")}\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <div id=\"llm-selector-modal\" className=\"w-full h-[388px] flex\">\n      <LLMSelectorSidePanel\n        availableProviders={availableProviders}\n        selectedLLMProvider={selectedLLMProvider}\n        onSearchChange={handleSearch}\n        onProviderClick={handleProviderSelection}\n      />\n      <div className=\"w-[60%] h-full p-[18px] flex flex-col gap-2.5\">\n        <div className=\"flex flex-col gap-[15px]\">\n          <div className=\"flex flex-col gap-1.5\">\n            <p className=\"text-sm font-medium text-white light:text-slate-800\">\n              {t(\"chat_window.workspace_llm_manager.available_models\", {\n                provider: providerName,\n              })}\n            </p>\n            <p className=\"text-xs font-medium text-zinc-400 light:text-slate-500\">\n              {t(\n                \"chat_window.workspace_llm_manager.available_models_description\"\n              )}\n            </p>\n          </div>\n          {!missingCredentials && (\n            <ChatModelSelection\n              provider={selectedLLMProvider}\n              setHasChanges={setHasChanges}\n              selectedLLMModel={selectedLLMModel}\n              setSelectedLLMModel={setSelectedLLMModel}\n            />\n          )}\n        </div>\n        <NoSetupWarning\n          showing={missingCredentials}\n          onSetupClick={() => {\n            window.dispatchEvent(\n              new CustomEvent(PROVIDER_SETUP_EVENT, {\n                detail: {\n                  provider: WORKSPACE_LLM_PROVIDERS.find(\n                    (p) => p.value === selectedLLMProvider\n                  ),\n                  settings,\n                },\n              })\n            );\n          }}\n        />\n        {hasChanges && !missingCredentials && (\n          <button\n            type=\"button\"\n            disabled={saving}\n            onClick={handleSave}\n            className=\"border-none text-xs px-4 py-1.5 font-semibold rounded-lg bg-white text-zinc-900 hover:bg-zinc-200 light:bg-slate-800 light:text-white light:hover:bg-slate-700 h-8 w-full cursor-pointer transition-colors mt-auto\"\n          >\n            {saving\n              ? t(\"chat_window.workspace_llm_manager.saving\")\n              : t(\"chat_window.workspace_llm_manager.save\")}\n          </button>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/utils.js",
    "content": "import { AVAILABLE_LLM_PROVIDERS } from \"@/pages/GeneralSettings/LLMPreference\";\nimport { DISABLED_PROVIDERS } from \"@/hooks/useGetProvidersModels\";\n\nexport function autoScrollToSelectedLLMProvider(\n  selectedLLMProvider,\n  timeout = 500\n) {\n  setTimeout(() => {\n    const selectedButton = document.querySelector(\n      `[data-llm-value=\"${selectedLLMProvider}\"]`\n    );\n    if (!selectedButton) return;\n    selectedButton.scrollIntoView({ behavior: \"smooth\", block: \"nearest\" });\n  }, timeout);\n}\n\n/**\n * Validates the model selection by checking if the model is in the select option in the available models\n * dropdown. If the model is not in the dropdown, it will return the first model in the dropdown.\n *\n * This exists when the user swaps providers, but did not select a model in the new provider's dropdown\n * and assumed the first model in the picker was OK. This prevents invalid provider<>model selection issues\n * @param {string} model - The model to validate\n * @returns {string} - The validated model\n */\nexport function validatedModelSelection(model) {\n  try {\n    // If the entire select element is not found, return the model as is and cross our fingers\n    const selectOption = document.getElementById(`workspace-llm-model-select`);\n    if (!selectOption) return model;\n\n    // If the model is not in the dropdown, return the first model in the dropdown\n    // to prevent invalid provider<>model selection issues\n    const selectedOption = selectOption.querySelector(\n      `option[value=\"${model}\"]`\n    );\n    if (!selectedOption) return selectOption.querySelector(`option`).value;\n\n    // If the model is in the dropdown, return the model as is\n    return model;\n  } catch {\n    return null; // If the dropdown was empty or something else went wrong, return null to abort the save\n  }\n}\n\nexport function hasMissingCredentials(settings, provider) {\n  if (!settings) return false;\n  const providerEntry = AVAILABLE_LLM_PROVIDERS.find(\n    (p) => p.value === provider\n  );\n  if (!providerEntry) return false;\n\n  for (const requiredKey of providerEntry.requiredConfig) {\n    if (!settings.hasOwnProperty(requiredKey)) return true;\n    if (!settings[requiredKey]) return true;\n  }\n  return false;\n}\n\nexport const WORKSPACE_LLM_PROVIDERS = AVAILABLE_LLM_PROVIDERS.filter(\n  (provider) => !DISABLED_PROVIDERS.includes(provider.value)\n);\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/SpeechToText/index.jsx",
    "content": "import { useEffect, useCallback, useRef } from \"react\";\nimport { Microphone } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\nimport \"regenerator-runtime\"; //required polyfill for speech recognition;\nimport SpeechRecognition, {\n  useSpeechRecognition,\n} from \"react-speech-recognition\";\nimport { PROMPT_INPUT_EVENT } from \"../../PromptInput\";\nimport { useTranslation } from \"react-i18next\";\nimport Appearance from \"@/models/appearance\";\n\nlet timeout;\nconst SILENCE_INTERVAL = 3_200; // wait in seconds of silence before closing.\n\n/**\n * Speech-to-text input component for the chat window.\n * @param {Object} props - The component props\n * @param {(textToAppend: string, autoSubmit: boolean) => void} props.sendCommand - The function to send the command\n * @returns {React.ReactElement} The SpeechToText component\n */\nexport default function SpeechToText({ sendCommand }) {\n  const previousTranscriptRef = useRef(\"\");\n  const {\n    transcript,\n    listening,\n    resetTranscript,\n    browserSupportsSpeechRecognition,\n    browserSupportsContinuousListening,\n    isMicrophoneAvailable,\n  } = useSpeechRecognition({\n    clearTranscriptOnListen: true,\n  });\n  const { t } = useTranslation();\n  function startSTTSession() {\n    if (!isMicrophoneAvailable) {\n      alert(\n        \"AnythingLLM does not have access to microphone. Please enable for this site to use this feature.\"\n      );\n      return;\n    }\n\n    resetTranscript();\n    previousTranscriptRef.current = \"\";\n    SpeechRecognition.startListening({\n      continuous: browserSupportsContinuousListening,\n      language: window?.navigator?.language ?? \"en-US\",\n    });\n  }\n\n  function endSTTSession() {\n    SpeechRecognition.stopListening();\n\n    // If auto submit is enabled, send an empty string to the chat window to submit the current transcript\n    // since every chunk of text should have been streamed to the chat window by now.\n    if (Appearance.get(\"autoSubmitSttInput\")) {\n      sendCommand({\n        text: \"\",\n        autoSubmit: true,\n        writeMode: \"append\",\n      });\n    }\n\n    resetTranscript();\n    previousTranscriptRef.current = \"\";\n    clearTimeout(timeout);\n  }\n\n  const handleKeyPress = useCallback(\n    (event) => {\n      // CTRL + m on Mac and Windows to toggle STT listening\n      if (event.ctrlKey && event.keyCode === 77) {\n        if (listening) {\n          endSTTSession();\n        } else {\n          startSTTSession();\n        }\n      }\n    },\n    [listening, endSTTSession, startSTTSession]\n  );\n\n  function handlePromptUpdate(e) {\n    if (!e?.detail && timeout) {\n      endSTTSession();\n      clearTimeout(timeout);\n    }\n  }\n\n  useEffect(() => {\n    document.addEventListener(\"keydown\", handleKeyPress);\n    return () => {\n      document.removeEventListener(\"keydown\", handleKeyPress);\n    };\n  }, [handleKeyPress]);\n\n  useEffect(() => {\n    if (!!window)\n      window.addEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate);\n    return () =>\n      window?.removeEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate);\n  }, []);\n\n  useEffect(() => {\n    if (transcript?.length > 0 && listening) {\n      const previousTranscript = previousTranscriptRef.current;\n      const newContent = transcript.slice(previousTranscript.length);\n\n      // Stream just the diff of the new content since transcript is an accumulating string.\n      // and not just the new content transcribed.\n      if (newContent.length > 0)\n        sendCommand({ text: newContent, writeMode: \"append\" });\n\n      previousTranscriptRef.current = transcript;\n      clearTimeout(timeout);\n      timeout = setTimeout(() => {\n        endSTTSession();\n      }, SILENCE_INTERVAL);\n    }\n  }, [transcript, listening]);\n\n  if (!browserSupportsSpeechRecognition) return null;\n  return (\n    <div\n      data-tooltip-id=\"tooltip-microphone-btn\"\n      data-tooltip-content={`${t(\"chat_window.microphone\")} (CTRL + M)`}\n      aria-label={t(\"chat_window.microphone\")}\n      onClick={listening ? endSTTSession : startSTTSession}\n      className={`group border-none relative flex justify-center items-center cursor-pointer w-8 h-8 rounded-full hover:bg-zinc-700 light:hover:bg-slate-200 ${\n        listening ? \"bg-zinc-700 light:bg-slate-200\" : \"\"\n      }`}\n    >\n      <Microphone\n        weight=\"regular\"\n        size={18}\n        className={`pointer-events-none text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-600 shrink-0 ${\n          listening\n            ? \"animate-pulse-glow !text-white light:!text-slate-800\"\n            : \"\"\n        }`}\n      />\n      <Tooltip\n        id=\"tooltip-microphone-btn\"\n        place=\"top\"\n        delayShow={300}\n        className=\"tooltip !text-xs z-99\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/StopGenerationButton/index.jsx",
    "content": "import { ABORT_STREAM_EVENT } from \"@/utils/chat\";\nimport { Tooltip } from \"react-tooltip\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function StopGenerationButton() {\n  const { t } = useTranslation();\n  function emitHaltEvent() {\n    window.dispatchEvent(new CustomEvent(ABORT_STREAM_EVENT));\n  }\n\n  return (\n    <>\n      <button\n        type=\"button\"\n        onClick={emitHaltEvent}\n        data-tooltip-id=\"stop-generation-button\"\n        data-tooltip-content={t(\"chat_window.stop_generating\")}\n        className=\"border-none inline-flex justify-center items-center rounded-full cursor-pointer w-8 h-8 bg-white light:bg-slate-800 hover:opacity-80 transition-opacity\"\n        aria-label=\"Stop generating\"\n      >\n        <div className=\"w-3.5 h-3.5 rounded-[4px] bg-zinc-800 light:bg-white\" />\n      </button>\n      <Tooltip\n        id=\"stop-generation-button\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip !text-xs z-99\"\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/TextSizeMenu/index.jsx",
    "content": "import { useState, useRef } from \"react\";\nimport { TextT } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\nimport { useTranslation } from \"react-i18next\";\nimport { useTheme } from \"@/hooks/useTheme\";\n\nexport default function TextSizeButton() {\n  const tooltipRef = useRef(null);\n  const { t } = useTranslation();\n  const { theme } = useTheme();\n\n  const toggleTooltip = () => {\n    if (!tooltipRef.current) return;\n    tooltipRef.current.isOpen\n      ? tooltipRef.current.close()\n      : tooltipRef.current.open();\n  };\n\n  return (\n    <>\n      <div\n        id=\"text-size-btn\"\n        data-tooltip-id=\"tooltip-text-size-btn\"\n        aria-label={t(\"chat_window.text_size\")}\n        onClick={toggleTooltip}\n        className=\"border-none flex justify-center items-center opacity-60 hover:opacity-100 light:opacity-100 light:hover:opacity-60 cursor-pointer\"\n      >\n        <TextT\n          color=\"var(--theme-sidebar-footer-icon-fill)\"\n          weight=\"fill\"\n          className=\"w-[20px] h-[20px] pointer-events-none text-white\"\n        />\n      </div>\n      <Tooltip\n        ref={tooltipRef}\n        id=\"tooltip-text-size-btn\"\n        place=\"top\"\n        opacity={1}\n        clickable={true}\n        delayShow={300}\n        delayHide={800}\n        arrowColor={\n          theme === \"light\"\n            ? \"var(--theme-modal-border)\"\n            : \"var(--theme-bg-primary)\"\n        }\n        className=\"z-99 !w-[140px] !bg-theme-bg-primary !px-[5px] !rounded-lg !pointer-events-auto light:border-2 light:border-theme-modal-border\"\n      >\n        <TextSizeMenu tooltipRef={tooltipRef} />\n      </Tooltip>\n    </>\n  );\n}\n\nfunction TextSizeMenu({ tooltipRef }) {\n  const { t } = useTranslation();\n  const [selectedSize, setSelectedSize] = useState(\n    window.localStorage.getItem(\"anythingllm_text_size\") || \"normal\"\n  );\n\n  const handleTextSizeChange = (size) => {\n    setSelectedSize(size);\n    window.localStorage.setItem(\"anythingllm_text_size\", size);\n    window.dispatchEvent(new CustomEvent(\"textSizeChange\", { detail: size }));\n    tooltipRef.current?.close();\n  };\n\n  return (\n    <div className=\"flex flex-col justify-start items-stretch gap-1 p-2\">\n      <button\n        onClick={(e) => {\n          e.preventDefault();\n          handleTextSizeChange(\"small\");\n        }}\n        className={`border-none w-full hover:cursor-pointer px-2 py-2 rounded-md flex items-center group ${\n          selectedSize === \"small\"\n            ? \"bg-theme-action-menu-item-hover\"\n            : \"hover:bg-theme-action-menu-item-hover\"\n        }`}\n      >\n        <div className=\"text-theme-text-primary text-xs\">\n          {t(\"chat_window.small\")}\n        </div>\n      </button>\n\n      <button\n        onClick={(e) => {\n          e.preventDefault();\n          handleTextSizeChange(\"normal\");\n        }}\n        className={`border-none w-full hover:cursor-pointer px-2 py-2 rounded-md flex items-center group ${\n          selectedSize === \"normal\"\n            ? \"bg-theme-action-menu-item-hover\"\n            : \"hover:bg-theme-action-menu-item-hover\"\n        }`}\n      >\n        <div className=\"text-theme-text-primary text-sm\">\n          {t(\"chat_window.normal\")}\n        </div>\n      </button>\n\n      <button\n        onClick={(e) => {\n          e.preventDefault();\n          handleTextSizeChange(\"large\");\n        }}\n        className={`border-none w-full hover:cursor-pointer px-2 py-2 rounded-md flex items-center group ${\n          selectedSize === \"large\"\n            ? \"bg-theme-action-menu-item-hover\"\n            : \"hover:bg-theme-action-menu-item-hover\"\n        }`}\n      >\n        <div className=\"text-theme-text-primary text-[16px]\">\n          {t(\"chat_window.large\")}\n        </div>\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/SkillRow/index.jsx",
    "content": "import Toggle from \"@/components/lib/Toggle\";\n\nexport default function SkillRow({\n  name,\n  enabled,\n  onToggle,\n  highlighted = false,\n  disabled = false,\n}) {\n  let classNames = \"flex items-center justify-between px-2 py-1 rounded\";\n  if (highlighted) classNames += \" bg-zinc-700/50 light:bg-slate-100\";\n  else classNames += \" hover:bg-zinc-700/50 light:hover:bg-slate-100\";\n\n  if (disabled) classNames += \" opacity-60 cursor-not-allowed\";\n  else classNames += \" cursor-pointer\";\n  return (\n    <div\n      className={classNames}\n      data-tooltip-id={disabled ? \"agent-skill-disabled-tooltip\" : undefined}\n    >\n      <span className=\"text-xs text-white light:text-slate-900\">{name}</span>\n      <Toggle\n        size=\"sm\"\n        enabled={enabled}\n        onChange={onToggle}\n        disabled={disabled}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/index.jsx",
    "content": "import { useState, useEffect, useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Link } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\nimport Admin from \"@/models/admin\";\nimport AgentPlugins from \"@/models/experimental/agentPlugins\";\nimport AgentFlows from \"@/models/agentFlows\";\nimport {\n  getDefaultSkills,\n  getConfigurableSkills,\n} from \"@/pages/Admin/Agents/skills\";\nimport useToolsMenuItems from \"../../useToolsMenuItems\";\nimport SkillRow from \"./SkillRow\";\nimport { Wrench } from \"@phosphor-icons/react\";\nimport { useIsAgentSessionActive } from \"@/utils/chat/agent\";\n\nexport default function AgentSkillsTab({\n  highlightedIndex = -1,\n  registerItemCount,\n  workspace,\n}) {\n  const { t } = useTranslation();\n  const { showAgentCommand = true } = workspace ?? {};\n  const agentSessionActive = useIsAgentSessionActive();\n  const defaultSkills = getDefaultSkills(t);\n  const configurableSkills = getConfigurableSkills(t);\n  const [disabledDefaults, setDisabledDefaults] = useState([]);\n  const [enabledConfigurable, setEnabledConfigurable] = useState([]);\n  const [importedSkills, setImportedSkills] = useState([]);\n  const [flows, setFlows] = useState([]);\n  const [loading, setLoading] = useState(true);\n  const showAgentCmdActivationAlert = showAgentCommand && !agentSessionActive;\n\n  useEffect(() => {\n    fetchSkillSettings();\n  }, []);\n\n  async function fetchSkillSettings() {\n    try {\n      const [prefs, flowsRes] = await Promise.all([\n        Admin.systemPreferencesByFields([\n          \"disabled_agent_skills\",\n          \"default_agent_skills\",\n          \"imported_agent_skills\",\n        ]),\n        AgentFlows.listFlows(),\n      ]);\n\n      if (prefs?.settings) {\n        setDisabledDefaults(prefs.settings.disabled_agent_skills ?? []);\n        setEnabledConfigurable(prefs.settings.default_agent_skills ?? []);\n        setImportedSkills(prefs.settings.imported_agent_skills ?? []);\n      }\n      if (flowsRes?.flows) setFlows(flowsRes.flows);\n    } catch (e) {\n      console.error(e);\n    } finally {\n      setLoading(false);\n    }\n  }\n\n  function toggleItem(arr, item) {\n    return arr.includes(item) ? arr.filter((s) => s !== item) : [...arr, item];\n  }\n\n  function isSkillEnabled(key) {\n    return key in defaultSkills\n      ? !disabledDefaults.includes(key)\n      : enabledConfigurable.includes(key);\n  }\n\n  async function toggleSkill(key) {\n    if (key in defaultSkills) {\n      const updated = toggleItem(disabledDefaults, key);\n      setDisabledDefaults(updated);\n      await Admin.updateSystemPreferences({\n        disabled_agent_skills: updated.join(\",\"),\n        default_agent_skills: enabledConfigurable.join(\",\"),\n      });\n      return;\n    }\n\n    const updated = toggleItem(enabledConfigurable, key);\n    setEnabledConfigurable(updated);\n    await Admin.updateSystemPreferences({\n      disabled_agent_skills: disabledDefaults.join(\",\"),\n      default_agent_skills: updated.join(\",\"),\n    });\n  }\n\n  async function toggleImportedSkill(skill) {\n    const newActive = !skill.active;\n    setImportedSkills((prev) =>\n      prev.map((s) =>\n        s.hubId === skill.hubId ? { ...s, active: newActive } : s\n      )\n    );\n    await AgentPlugins.toggleFeature(skill.hubId, newActive);\n  }\n\n  async function toggleFlow(flow) {\n    const newActive = !flow.active;\n    setFlows((prev) =>\n      prev.map((f) => (f.uuid === flow.uuid ? { ...f, active: newActive } : f))\n    );\n    await AgentFlows.toggleFlow(flow.uuid, newActive);\n  }\n\n  // Build list of all skill items for rendering/keyboard navigation\n  const items = useMemo(() => {\n    const list = [];\n    for (const [key, { title }] of Object.entries({\n      ...defaultSkills,\n      ...configurableSkills,\n    })) {\n      list.push({\n        id: key,\n        name: title,\n        enabled: isSkillEnabled(key),\n        onToggle: () => toggleSkill(key),\n      });\n    }\n    for (const skill of importedSkills) {\n      list.push({\n        id: skill.hubId,\n        name: skill.name,\n        enabled: skill.active,\n        onToggle: () => toggleImportedSkill(skill),\n      });\n    }\n    for (const flow of flows) {\n      list.push({\n        id: flow.uuid,\n        name: flow.name,\n        enabled: flow.active,\n        onToggle: () => toggleFlow(flow),\n      });\n    }\n    return list;\n  }, [disabledDefaults, enabledConfigurable, importedSkills, flows]);\n\n  useToolsMenuItems({\n    items,\n    highlightedIndex,\n    onSelect: agentSessionActive ? () => {} : (item) => item.onToggle(),\n    registerItemCount,\n  });\n\n  if (loading) return null;\n\n  return (\n    <>\n      {showAgentCmdActivationAlert && (\n        <p className=\"text-xs text-theme-text-secondary text-center py-1\">\n          {t(\"chat_window.use_agent_session_to_use_tools\")}\n        </p>\n      )}\n      {items.map((item, index) => (\n        <SkillRow\n          key={item.id}\n          name={item.name}\n          enabled={item.enabled}\n          onToggle={item.onToggle}\n          highlighted={highlightedIndex === index}\n          disabled={agentSessionActive}\n        />\n      ))}\n      <Link to={paths.settings.agentSkills()}>\n        <button className=\"flex items-center gap-1.5 px-2 h-6 rounded cursor-pointer hover:bg-zinc-700/50 light:hover:bg-slate-100 text-theme-text-primary\">\n          <Wrench size={12} className=\"text-theme-text-primary\" />\n          <span className=\"text-xs text-theme-text-primary\">\n            {t(\"chat_window.manage_agent_skills\")}\n          </span>\n        </button>\n      </Link>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/SlashCommands/SlashCommandRow/index.jsx",
    "content": "import { useState, useRef, useEffect } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { DotsThree } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function SlashCommandRow({\n  command,\n  description,\n  onClick,\n  onEdit,\n  onPublish,\n  showMenu = false,\n  highlighted = false,\n}) {\n  const { t } = useTranslation();\n  const [menuOpen, setMenuOpen] = useState(false);\n  const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });\n  const menuRef = useRef(null);\n  const menuBtnRef = useRef(null);\n\n  useEffect(() => {\n    if (!menuOpen) return;\n    function handleClickOutside(e) {\n      if (\n        menuRef.current &&\n        !menuRef.current.contains(e.target) &&\n        menuBtnRef.current &&\n        !menuBtnRef.current.contains(e.target)\n      ) {\n        setMenuOpen(false);\n      }\n    }\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n  }, [menuOpen]);\n\n  useEffect(() => {\n    if (menuOpen && menuBtnRef.current) {\n      const rect = menuBtnRef.current.getBoundingClientRect();\n      setMenuPosition({\n        top: rect.bottom + window.scrollY,\n        left: rect.right + window.scrollX - 120,\n      });\n    }\n  }, [menuOpen]);\n\n  return (\n    <div\n      onClick={onClick}\n      className={`flex items-center justify-between px-2 py-1 rounded cursor-pointer group relative ${\n        highlighted\n          ? \"bg-zinc-700/50 light:bg-slate-100\"\n          : \"hover:bg-zinc-700/50 light:hover:bg-slate-100\"\n      }`}\n    >\n      <div className=\"flex gap-1.5 items-center text-xs min-w-0 flex-1\">\n        <span className=\"text-white light:text-slate-900 shrink-0\">\n          {command}\n        </span>\n        <span className=\"text-zinc-400 light:text-slate-500 italic truncate\">\n          {description}\n        </span>\n      </div>\n\n      {showMenu && (\n        <div className=\"relative shrink-0 ml-1\">\n          <button\n            ref={menuBtnRef}\n            type=\"button\"\n            onClick={(e) => {\n              e.stopPropagation();\n              setMenuOpen(!menuOpen);\n            }}\n            className=\"border-none cursor-pointer text-zinc-400 light:text-slate-500 p-0.5 hover:text-white light:hover:text-slate-900 rounded opacity-0 group-hover:opacity-100\"\n          >\n            <DotsThree size={16} weight=\"bold\" />\n          </button>\n\n          {menuOpen &&\n            createPortal(\n              <div\n                ref={menuRef}\n                style={{\n                  position: \"fixed\",\n                  top: menuPosition.top,\n                  left: menuPosition.left,\n                }}\n                className=\"z-[9999] bg-zinc-800 light:bg-white border border-zinc-700 light:border-slate-300 rounded-lg shadow-lg min-w-[120px] flex flex-col overflow-hidden\"\n              >\n                <button\n                  type=\"button\"\n                  className=\"border-none px-3 py-1.5 text-xs text-white light:text-slate-900 hover:bg-zinc-700 light:hover:bg-slate-100 cursor-pointer text-left\"\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    setMenuOpen(false);\n                    onEdit?.();\n                  }}\n                >\n                  {t(\"chat_window.edit\")}\n                </button>\n                <button\n                  type=\"button\"\n                  className=\"border-none px-3 py-1.5 text-xs text-white light:text-slate-900 hover:bg-zinc-700 light:hover:bg-slate-100 cursor-pointer text-left\"\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    setMenuOpen(false);\n                    onPublish?.();\n                  }}\n                >\n                  {t(\"chat_window.publish\")}\n                </button>\n              </div>,\n              document.body\n            )}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/SlashCommands/SlashPresets/AddPresetModal.jsx",
    "content": "import { useState } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { CMD_REGEX } from \"./constants\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function AddPresetModal({ isOpen, onClose, onSave }) {\n  const [command, setCommand] = useState(\"\");\n  const { t } = useTranslation();\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = new FormData(e.target);\n    const sanitizedCommand = command.replace(CMD_REGEX, \"\");\n    const saved = await onSave({\n      command: `/${sanitizedCommand}`,\n      prompt: form.get(\"prompt\"),\n      description: form.get(\"description\"),\n    });\n    if (saved) setCommand(\"\");\n  };\n\n  const handleCommandChange = (e) => {\n    const value = e.target.value.replace(CMD_REGEX, \"\");\n    setCommand(value);\n  };\n\n  return (\n    <ModalWrapper isOpen={isOpen}>\n      <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              {t(\"chat_window.add_new_preset\")}\n            </h3>\n          </div>\n          <button\n            onClick={onClose}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div\n          className=\"h-full w-full overflow-y-auto\"\n          style={{ maxHeight: \"calc(100vh - 200px)\" }}\n        >\n          <form onSubmit={handleSubmit}>\n            <div className=\"py-7 px-9 space-y-2 flex-col\">\n              <div className=\"w-full flex flex-col gap-y-4\">\n                <div>\n                  <label\n                    htmlFor=\"command\"\n                    className=\"block mb-2 text-sm font-medium text-white\"\n                  >\n                    {t(\"chat_window.command\")}\n                  </label>\n                  <div className=\"flex items-center\">\n                    <span className=\"text-white text-sm mr-2 font-bold\">/</span>\n                    <input\n                      name=\"command\"\n                      type=\"text\"\n                      id=\"command\"\n                      placeholder={t(\"chat_window.your_command\")}\n                      value={command}\n                      onChange={handleCommandChange}\n                      maxLength={25}\n                      autoComplete=\"off\"\n                      required={true}\n                      className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                    />\n                  </div>\n                </div>\n                <div>\n                  <label\n                    htmlFor=\"prompt\"\n                    className=\"block mb-2 text-sm font-medium text-white\"\n                  >\n                    Prompt\n                  </label>\n                  <textarea\n                    name=\"prompt\"\n                    id=\"prompt\"\n                    autoComplete=\"off\"\n                    placeholder={t(\"chat_window.placeholder_prompt\")}\n                    required={true}\n                    className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  ></textarea>\n                </div>\n                <div>\n                  <label\n                    htmlFor=\"description\"\n                    className=\"block mb-2 text-sm font-medium text-white\"\n                  >\n                    {t(\"chat_window.description\")}\n                  </label>\n                  <input\n                    type=\"text\"\n                    name=\"description\"\n                    id=\"description\"\n                    placeholder={t(\"chat_window.placeholder_description\")}\n                    maxLength={80}\n                    autoComplete=\"off\"\n                    required={true}\n                    className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  />\n                </div>\n              </div>\n            </div>\n            <div className=\"flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b\">\n              <button\n                onClick={onClose}\n                type=\"button\"\n                className=\"transition-all duration-300 bg-transparent text-white hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                {t(\"chat_window.cancel\")}\n              </button>\n              <button\n                type=\"submit\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                {t(\"chat_window.save\")}\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </ModalWrapper>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/SlashCommands/SlashPresets/EditPresetModal.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { CMD_REGEX } from \"./constants\";\n\nexport default function EditPresetModal({\n  isOpen,\n  onClose,\n  onSave,\n  onDelete,\n  preset,\n}) {\n  const [command, setCommand] = useState(\"\");\n  const [deleting, setDeleting] = useState(false);\n\n  useEffect(() => {\n    if (preset && isOpen) {\n      setCommand(preset.command?.slice(1) || \"\");\n    }\n  }, [preset, isOpen]);\n\n  const handleSubmit = (e) => {\n    e.preventDefault();\n    const form = new FormData(e.target);\n    const sanitizedCommand = command.replace(CMD_REGEX, \"\");\n    onSave({\n      id: preset.id,\n      command: `/${sanitizedCommand}`,\n      prompt: form.get(\"prompt\"),\n      description: form.get(\"description\"),\n    });\n  };\n\n  const handleCommandChange = (e) => {\n    const value = e.target.value.replace(CMD_REGEX, \"\");\n    setCommand(value);\n  };\n\n  const handleDelete = async () => {\n    if (!window.confirm(\"Are you sure you want to delete this preset?\")) return;\n\n    setDeleting(true);\n    await onDelete(preset.id);\n    setDeleting(false);\n    onClose();\n  };\n\n  return (\n    <ModalWrapper isOpen={isOpen}>\n      <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Edit Preset\n            </h3>\n          </div>\n          <button\n            onClick={onClose}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div\n          className=\"h-full w-full overflow-y-auto\"\n          style={{ maxHeight: \"calc(100vh - 200px)\" }}\n        >\n          <form onSubmit={handleSubmit}>\n            <div className=\"py-7 px-9 space-y-2 flex-col\">\n              <div className=\"w-full flex flex-col gap-y-4\">\n                <div>\n                  <label\n                    htmlFor=\"command\"\n                    className=\"block mb-2 text-sm font-medium text-white\"\n                  >\n                    Command\n                  </label>\n                  <div className=\"flex items-center\">\n                    <span className=\"text-white text-sm mr-2 font-bold\">/</span>\n                    <input\n                      type=\"text\"\n                      name=\"command\"\n                      placeholder=\"your-command\"\n                      value={command}\n                      onChange={handleCommandChange}\n                      required={true}\n                      className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                    />\n                  </div>\n                </div>\n                <div>\n                  <label\n                    htmlFor=\"prompt\"\n                    className=\"block mb-2 text-sm font-medium text-white\"\n                  >\n                    Prompt\n                  </label>\n                  <textarea\n                    name=\"prompt\"\n                    placeholder=\"This is a test prompt. Please respond with a poem about LLMs.\"\n                    defaultValue={preset.prompt}\n                    required={true}\n                    className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  ></textarea>\n                </div>\n                <div>\n                  <label\n                    htmlFor=\"description\"\n                    className=\"block mb-2 text-sm font-medium text-white\"\n                  >\n                    Description\n                  </label>\n                  <input\n                    type=\"text\"\n                    name=\"description\"\n                    defaultValue={preset.description}\n                    placeholder=\"Responds with a poem about LLMs.\"\n                    required={true}\n                    className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  />\n                </div>\n              </div>\n            </div>\n            <div className=\"flex w-full justify-between items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b\">\n              <button\n                disabled={deleting}\n                onClick={handleDelete}\n                type=\"button\"\n                className=\"border-none transition-all duration-300 bg-transparent text-red-500 hover:bg-red-500/25 px-4 py-2 rounded-lg text-sm disabled:opacity-50\"\n              >\n                {deleting ? \"Deleting...\" : \"Delete Preset\"}\n              </button>\n              <div className=\"flex space-x-2\">\n                <button\n                  onClick={onClose}\n                  type=\"button\"\n                  className=\"border-none transition-all duration-300 bg-transparent text-white hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n                >\n                  Cancel\n                </button>\n                <button\n                  type=\"submit\"\n                  className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n                >\n                  Save\n                </button>\n              </div>\n            </div>\n          </form>\n        </div>\n      </div>\n    </ModalWrapper>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/SlashCommands/SlashPresets/constants.js",
    "content": "export const CMD_REGEX = /[^a-zA-Z0-9_-]/g;\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/SlashCommands/index.jsx",
    "content": "import { useState, useEffect, useMemo, useCallback } from \"react\";\nimport { Plus } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\nimport System from \"@/models/system\";\nimport { useModal } from \"@/hooks/useModal\";\nimport AddPresetModal from \"./SlashPresets/AddPresetModal\";\nimport EditPresetModal from \"./SlashPresets/EditPresetModal\";\nimport PublishEntityModal from \"@/components/CommunityHub/PublishEntityModal\";\nimport showToast from \"@/utils/toast\";\nimport { useIsAgentSessionActive } from \"@/utils/chat/agent\";\nimport { PROMPT_INPUT_EVENT } from \"@/components/WorkspaceChat/ChatContainer/PromptInput\";\nimport useToolsMenuItems from \"../../useToolsMenuItems\";\nimport SlashCommandRow from \"./SlashCommandRow\";\n\nexport default function SlashCommandsTab({\n  sendCommand,\n  setShowing,\n  promptRef,\n  highlightedIndex = -1,\n  registerItemCount,\n}) {\n  const { t } = useTranslation();\n  const isActiveAgentSession = useIsAgentSessionActive();\n  const {\n    isOpen: isAddModalOpen,\n    openModal: openAddModal,\n    closeModal: closeAddModal,\n  } = useModal();\n  const {\n    isOpen: isEditModalOpen,\n    openModal: openEditModal,\n    closeModal: closeEditModal,\n  } = useModal();\n  const {\n    isOpen: isPublishModalOpen,\n    openModal: openPublishModal,\n    closeModal: closePublishModal,\n  } = useModal();\n  const [presets, setPresets] = useState([]);\n  const [selectedPreset, setSelectedPreset] = useState(null);\n  const [presetToPublish, setPresetToPublish] = useState(null);\n\n  useEffect(() => {\n    fetchPresets();\n  }, []);\n\n  const fetchPresets = async () => {\n    const presets = await System.getSlashCommandPresets();\n    setPresets(presets);\n  };\n\n  // Build the list of selectable items for keyboard navigation and rendering\n  // Command names must stay as static English strings since the backend\n  // matches against exact \"/reset\" and \"/exit\" commands.\n  const items = useMemo(() => {\n    const builtIn = isActiveAgentSession\n      ? {\n          command: \"/exit\",\n          description: t(\"chat_window.preset_exit_description\"),\n          autoSubmit: true,\n        }\n      : {\n          command: \"/reset\",\n          description: t(\"chat_window.preset_reset_description\"),\n          autoSubmit: true,\n        };\n\n    return [\n      builtIn,\n      ...presets.map((preset) => ({\n        command: preset.command,\n        description: preset.description,\n        autoSubmit: false,\n        preset,\n      })),\n    ];\n  }, [isActiveAgentSession, presets]);\n\n  const handleUseCommand = useCallback(\n    (command, autoSubmit = false) => {\n      setShowing(false);\n\n      // Auto-submit commands (/reset, /exit) fire immediately\n      if (autoSubmit) {\n        sendCommand({ text: command, autoSubmit: true });\n        promptRef?.current?.focus();\n        return;\n      }\n\n      // Insert the command at the cursor, replacing a trailing \"/\" if present\n      const textarea = promptRef?.current;\n      if (!textarea) return;\n      const cursor = textarea.selectionStart;\n      const value = textarea.value;\n      const charBefore = cursor > 0 ? value[cursor - 1] : \"\";\n      const insertStart = charBefore === \"/\" ? cursor - 1 : cursor;\n      const newValue =\n        value.slice(0, insertStart) + command + value.slice(cursor);\n\n      window.dispatchEvent(\n        new CustomEvent(PROMPT_INPUT_EVENT, {\n          detail: { messageContent: newValue },\n        })\n      );\n      textarea.focus();\n      const newCursor = insertStart + command.length;\n      setTimeout(() => textarea.setSelectionRange(newCursor, newCursor), 0);\n    },\n    [sendCommand, setShowing, promptRef]\n  );\n\n  useToolsMenuItems({\n    items,\n    highlightedIndex,\n    onSelect: (item) => {\n      const text = item.preset ? `${item.command} ` : item.command;\n      handleUseCommand(text, item.autoSubmit);\n    },\n    registerItemCount,\n  });\n\n  const handleSavePreset = async (preset) => {\n    const { error } = await System.createSlashCommandPreset(preset);\n    if (error) {\n      showToast(error, \"error\");\n      return false;\n    }\n    fetchPresets();\n    closeAddModal();\n    return true;\n  };\n\n  const handleEditPreset = (preset) => {\n    setSelectedPreset(preset);\n    openEditModal();\n  };\n\n  const handleUpdatePreset = async (updatedPreset) => {\n    const { error } = await System.updateSlashCommandPreset(\n      updatedPreset.id,\n      updatedPreset\n    );\n    if (error) {\n      showToast(error, \"error\");\n      return;\n    }\n    fetchPresets();\n    closeEditModal();\n    setSelectedPreset(null);\n  };\n\n  const handleDeletePreset = async (presetId) => {\n    await System.deleteSlashCommandPreset(presetId);\n    fetchPresets();\n    closeEditModal();\n    setSelectedPreset(null);\n  };\n\n  const handlePublishPreset = (preset) => {\n    setPresetToPublish({\n      name: preset.command.slice(1),\n      description: preset.description,\n      command: preset.command,\n      prompt: preset.prompt,\n    });\n    openPublishModal();\n  };\n\n  return (\n    <>\n      {items.map((item, index) => (\n        <SlashCommandRow\n          key={item.preset?.id ?? item.command}\n          command={item.command}\n          description={item.description}\n          onClick={() =>\n            handleUseCommand(\n              item.preset ? `${item.command} ` : item.command,\n              item.autoSubmit\n            )\n          }\n          onEdit={item.preset ? () => handleEditPreset(item.preset) : undefined}\n          onPublish={\n            item.preset ? () => handlePublishPreset(item.preset) : undefined\n          }\n          showMenu={!!item.preset}\n          highlighted={highlightedIndex === index}\n        />\n      ))}\n\n      {/* Add new */}\n      {!isActiveAgentSession && (\n        <div\n          onClick={openAddModal}\n          className=\"flex items-center gap-1.5 px-2 py-1 rounded cursor-pointer hover:bg-zinc-700/50 light:hover:bg-slate-100\"\n        >\n          <Plus\n            size={12}\n            weight=\"bold\"\n            className=\"text-white light:text-slate-900\"\n          />\n          <span className=\"text-xs text-white light:text-slate-900\">\n            {t(\"chat_window.add_new\")}\n          </span>\n        </div>\n      )}\n\n      {/* Modals */}\n      <AddPresetModal\n        isOpen={isAddModalOpen}\n        onClose={closeAddModal}\n        onSave={handleSavePreset}\n      />\n      {selectedPreset && (\n        <EditPresetModal\n          isOpen={isEditModalOpen}\n          onClose={() => {\n            closeEditModal();\n            setSelectedPreset(null);\n          }}\n          onSave={handleUpdatePreset}\n          onDelete={handleDeletePreset}\n          preset={selectedPreset}\n        />\n      )}\n      <PublishEntityModal\n        show={isPublishModalOpen}\n        onClose={closePublishModal}\n        entityType=\"slash-command\"\n        entity={presetToPublish}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/index.jsx",
    "content": "import { useState, useEffect, useCallback, useRef, useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport useUser from \"@/hooks/useUser\";\nimport AgentSkillsTab from \"./Tabs/AgentSkills\";\nimport SlashCommandsTab from \"./Tabs/SlashCommands\";\n\nexport const TOOLS_MENU_KEYBOARD_EVENT = \"tools-menu-keyboard\";\nfunction getTabs(t, user) {\n  const tabs = [\n    {\n      key: \"slash-commands\",\n      label: t(\"chat_window.slash_commands\"),\n      component: SlashCommandsTab,\n    },\n  ];\n\n  // Only show agent skills tab for admins or when multiuser mode is off\n  const canSeeAgentSkills =\n    !user?.hasOwnProperty(\"role\") || user.role === \"admin\";\n  if (canSeeAgentSkills) {\n    tabs.push({\n      key: \"agent-skills\",\n      label: t(\"chat_window.agent_skills\"),\n      component: AgentSkillsTab,\n    });\n  }\n\n  return tabs;\n}\n\n/**\n * @param {Workspace} props.workspace - the workspace object\n * @param {boolean} props.showing\n * @param {function} props.setShowing\n * @param {function} props.sendCommand\n * @param {object} props.promptRef\n * @param {boolean} [props.centered] - when true, popup opens below the input\n */\nexport default function ToolsMenu({\n  workspace,\n  showing,\n  setShowing,\n  sendCommand,\n  promptRef,\n  centered = false,\n  highlightedIndexRef,\n}) {\n  const { t } = useTranslation();\n  const { user } = useUser();\n  const TABS = useMemo(() => getTabs(t, user), [t, user]);\n  const [activeTab, setActiveTab] = useState(TABS[0].key);\n  const [highlightedIndex, setHighlightedIndex] = useState(-1);\n  const itemCountRef = useRef(0);\n\n  // Always open to the slash commands\n  useEffect(() => {\n    if (showing) setActiveTab(TABS[0].key);\n  }, [showing]);\n\n  // Reset highlight when switching tabs or closing\n  useEffect(() => {\n    setHighlightedIndex(-1);\n  }, [activeTab, showing]);\n\n  // Keep the parent ref in sync so PromptInput can check it on Enter\n  useEffect(() => {\n    if (highlightedIndexRef) highlightedIndexRef.current = highlightedIndex;\n  }, [highlightedIndex]);\n\n  const registerItemCount = useCallback((count) => {\n    itemCountRef.current = count;\n  }, []);\n\n  useEffect(() => {\n    if (!showing) return;\n\n    function handleKeyboard(e) {\n      const { key } = e.detail;\n\n      if (key === \"ArrowLeft\" || key === \"ArrowRight\") {\n        const currentIdx = TABS.findIndex((tab) => tab.key === activeTab);\n        const nextIdx =\n          key === \"ArrowLeft\"\n            ? (currentIdx - 1 + TABS.length) % TABS.length\n            : (currentIdx + 1) % TABS.length;\n        setActiveTab(TABS[nextIdx].key);\n        return;\n      }\n\n      if (key === \"ArrowUp\" || key === \"ArrowDown\") {\n        const count = itemCountRef.current;\n        if (count === 0) return;\n        setHighlightedIndex((prev) => {\n          if (key === \"ArrowDown\") {\n            return prev < count - 1 ? prev + 1 : 0;\n          }\n          return prev > 0 ? prev - 1 : count - 1;\n        });\n        return;\n      }\n\n      // Enter is handled by the tab components via highlightedIndex\n    }\n\n    window.addEventListener(TOOLS_MENU_KEYBOARD_EVENT, handleKeyboard);\n    return () =>\n      window.removeEventListener(TOOLS_MENU_KEYBOARD_EVENT, handleKeyboard);\n  }, [showing, activeTab]);\n\n  if (!showing) return null;\n\n  const { component: ActiveTab } = TABS.find((tab) => tab.key === activeTab);\n\n  return (\n    <>\n      <div\n        className=\"fixed inset-0 z-40\"\n        onMouseDown={(e) => e.preventDefault()}\n        onClick={() => setShowing(false)}\n      />\n      <div\n        onMouseDown={(e) => {\n          // Prevents prompt textarea from losing focus when clicking inside the menu.\n          // Skip for portaled modals so their inputs can still receive focus.\n          if (e.currentTarget.contains(e.target)) e.preventDefault();\n        }}\n        className={`absolute left-2 right-2 md:left-14 md:right-auto md:w-[400px] z-50 bg-zinc-800 light:bg-white border border-zinc-700 light:border-slate-300 rounded-lg p-3 flex flex-col gap-2.5 shadow-lg overflow-hidden ${\n          centered\n            ? \"top-full mt-2 max-h-[min(360px,calc(100dvh-25rem))]\"\n            : \"bottom-full mb-2 max-h-[min(360px,calc(100dvh-11rem))]\"\n        }`}\n      >\n        <div className=\"flex shrink-0 gap-2.5 items-center\">\n          {TABS.map((tab) => (\n            <TabButton\n              key={tab.key}\n              active={activeTab === tab.key}\n              onClick={() => setActiveTab(tab.key)}\n            >\n              {tab.label}\n            </TabButton>\n          ))}\n        </div>\n\n        <div className=\"flex flex-col gap-1 overflow-y-auto no-scroll flex-1 min-h-0\">\n          <ActiveTab\n            sendCommand={sendCommand}\n            setShowing={setShowing}\n            promptRef={promptRef}\n            highlightedIndex={highlightedIndex}\n            registerItemCount={registerItemCount}\n            workspace={workspace}\n          />\n        </div>\n      </div>\n    </>\n  );\n}\n\nfunction TabButton({ active, onClick, children }) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      className={`border-none cursor-pointer hover:bg-zinc-700/50 light:hover:bg-slate-100 px-1.5 py-0.5 rounded text-[10px] font-medium text-center whitespace-nowrap ${\n        active\n          ? \"bg-zinc-700 text-white light:bg-slate-200 light:text-slate-800\"\n          : \"text-zinc-400 light:text-slate-800\"\n      }`}\n    >\n      {children}\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/useToolsMenuItems.js",
    "content": "import { useEffect } from \"react\";\nimport { TOOLS_MENU_KEYBOARD_EVENT } from \"./\";\n\n/**\n * Shared hook for ToolsMenu tabs that registers the item count\n * for Up/Down navigation and handles Enter to select the highlighted item.\n * @param {Array} items - the list of items rendered in the tab\n * @param {number} highlightedIndex - currently highlighted index from parent\n * @param {function} onSelect - called with the highlighted item on Enter\n * @param {function} registerItemCount - callback to register total item count with parent\n */\nexport default function useToolsMenuItems({\n  items,\n  highlightedIndex,\n  onSelect,\n  registerItemCount,\n}) {\n  useEffect(() => {\n    registerItemCount?.(items.length);\n  }, [items.length, registerItemCount]);\n\n  useEffect(() => {\n    if (highlightedIndex < 0 || highlightedIndex >= items.length) return;\n    function handleEnter(e) {\n      if (e.detail.key !== \"Enter\") return;\n      onSelect(items[highlightedIndex]);\n    }\n    window.addEventListener(TOOLS_MENU_KEYBOARD_EVENT, handleEnter);\n    return () =>\n      window.removeEventListener(TOOLS_MENU_KEYBOARD_EVENT, handleEnter);\n  }, [highlightedIndex, items, onSelect]);\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx",
    "content": "import { useState, useRef, useEffect } from \"react\";\nimport debounce from \"lodash.debounce\";\nimport { ArrowUp, At } from \"@phosphor-icons/react\";\nimport StopGenerationButton from \"./StopGenerationButton\";\nimport SpeechToText from \"./SpeechToText\";\nimport { Tooltip } from \"react-tooltip\";\nimport AttachmentManager from \"./Attachments\";\nimport AttachItem from \"./AttachItem\";\nimport {\n  ATTACHMENTS_PROCESSED_EVENT,\n  ATTACHMENTS_PROCESSING_EVENT,\n  PASTE_ATTACHMENT_EVENT,\n} from \"../DnDWrapper\";\nimport useTextSize from \"@/hooks/useTextSize\";\nimport { useTranslation } from \"react-i18next\";\nimport Appearance from \"@/models/appearance\";\nimport usePromptInputStorage from \"@/hooks/usePromptInputStorage\";\nimport ToolsMenu, { TOOLS_MENU_KEYBOARD_EVENT } from \"./ToolsMenu\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { useIsAgentSessionActive } from \"@/utils/chat/agent\";\n\nexport const PROMPT_INPUT_ID = \"primary-prompt-input\";\nexport const PROMPT_INPUT_EVENT = \"set_prompt_input\";\nconst MAX_EDIT_STACK_SIZE = 100;\n\n/**\n * @param {Workspace} props.workspace - workspace object\n * @param {function} props.submit - form submit handler\n * @param {boolean} props.isStreaming - disables input while streaming response\n * @param {function} props.sendCommand - handler for slash commands and agent mentions\n * @param {Array} [props.attachments] - file attachments array\n * @param {boolean} [props.centered] - renders in centered layout mode (for home page)\n * @param {string} [props.workspaceSlug] - workspace slug for home page context\n * @param {string} [props.threadSlug] - thread slug for home page context\n */\nexport default function PromptInput({\n  workspace = {},\n  submit,\n  isStreaming,\n  sendCommand,\n  attachments = [],\n  centered = false,\n  workspaceSlug = null,\n  threadSlug = null,\n}) {\n  const { t } = useTranslation();\n  const { showAgentCommand = true } = workspace ?? {};\n  const { isDisabled } = useIsDisabled();\n  const agentSessionActive = useIsAgentSessionActive();\n  const [promptInput, setPromptInput] = useState(\"\");\n  const [showTools, setShowTools] = useState(false);\n  const autoOpenedToolsRef = useRef(false);\n  const toolsHighlightRef = useRef(-1);\n  const formRef = useRef(null);\n  const textareaRef = useRef(null);\n  const [_, setFocused] = useState(false);\n  const undoStack = useRef([]);\n  const redoStack = useRef([]);\n  const { textSizeClass } = useTextSize();\n  const [searchParams] = useSearchParams();\n\n  // Synchronizes prompt input value with localStorage, scoped to the current thread.\n  usePromptInputStorage({\n    promptInput,\n    setPromptInput,\n  });\n\n  /*\n   * @checklist-item\n   * If the URL has the agent param, open the agent menu for the user\n   * automatically when the component mounts.\n   */\n  useEffect(() => {\n    if (searchParams.get(\"action\") === \"set-agent-chat\") {\n      sendCommand({ text: \"@agent \" });\n      textareaRef.current?.focus();\n    }\n  }, [textareaRef.current]);\n\n  /**\n   * To prevent too many re-renders we remotely listen for updates from the parent\n   * via an event cycle. Otherwise, using message as a prop leads to a re-render every\n   * change on the input.\n   * @param {{detail: {messageContent: string, writeMode: 'replace' | 'append'}}} e\n   */\n  function handlePromptUpdate(e) {\n    const { messageContent, writeMode = \"replace\" } = e?.detail ?? {};\n    if (writeMode === \"append\") setPromptInput((prev) => prev + messageContent);\n    else if (writeMode === \"prepend\")\n      setPromptInput((prev) => messageContent + \" \" + prev);\n    else setPromptInput(messageContent ?? \"\");\n  }\n\n  useEffect(() => {\n    if (!!window)\n      window.addEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate);\n    return () =>\n      window?.removeEventListener(PROMPT_INPUT_EVENT, handlePromptUpdate);\n  }, []);\n\n  useEffect(() => {\n    if (!isStreaming && textareaRef.current) textareaRef.current.focus();\n    resetTextAreaHeight();\n  }, [isStreaming]);\n\n  /**\n   * Save the current state before changes\n   * @param {number} adjustment\n   */\n  function saveCurrentState(adjustment = 0) {\n    if (undoStack.current.length >= MAX_EDIT_STACK_SIZE)\n      undoStack.current.shift();\n    undoStack.current.push({\n      value: promptInput,\n      cursorPositionStart: textareaRef.current.selectionStart + adjustment,\n      cursorPositionEnd: textareaRef.current.selectionEnd + adjustment,\n    });\n  }\n  const debouncedSaveState = debounce(saveCurrentState, 250);\n\n  function handleSubmit(e) {\n    // Ignore submits from portaled modals (slash command preset forms)\n    if (e.target !== e.currentTarget) return;\n    setFocused(false);\n    setShowTools(false);\n    submit(e);\n  }\n\n  function resetTextAreaHeight() {\n    if (!textareaRef.current) return;\n    textareaRef.current.style.height = \"auto\";\n  }\n\n  /**\n   * Capture enter key press to handle submission, redo, or undo\n   * via keyboard shortcuts\n   * @param {KeyboardEvent} event\n   */\n  function captureEnterOrUndo(event) {\n    // Forward keyboard events to the ToolsMenu when open\n    if (showTools) {\n      if (\n        [\"ArrowUp\", \"ArrowDown\", \"ArrowLeft\", \"ArrowRight\"].includes(event.key)\n      ) {\n        event.preventDefault();\n        window.dispatchEvent(\n          new CustomEvent(TOOLS_MENU_KEYBOARD_EVENT, {\n            detail: { key: event.key },\n          })\n        );\n        return;\n      }\n      // When an item is highlighted via arrow keys, Enter selects it.\n      // Otherwise, Enter falls through to submit the form normally.\n      if (event.key === \"Enter\" && toolsHighlightRef.current >= 0) {\n        event.preventDefault();\n        window.dispatchEvent(\n          new CustomEvent(TOOLS_MENU_KEYBOARD_EVENT, {\n            detail: { key: \"Enter\" },\n          })\n        );\n        return;\n      }\n      if (event.key === \"Escape\") {\n        event.preventDefault();\n        setShowTools(false);\n        textareaRef.current?.focus();\n        return;\n      }\n    }\n\n    // \"/\" toggles the Tools menu only when the input is empty\n    if (\n      event.key === \"/\" &&\n      !event.ctrlKey &&\n      !event.metaKey &&\n      promptInput.trim() === \"\"\n    ) {\n      setShowTools((prev) => {\n        autoOpenedToolsRef.current = !prev;\n        return !prev;\n      });\n      return;\n    }\n\n    // Is simple enter key press w/o shift key\n    if (event.keyCode === 13 && !event.shiftKey) {\n      event.preventDefault();\n      if (isStreaming || isDisabled) return; // Prevent submission if streaming or disabled\n      setShowTools(false);\n      return submit(event);\n    }\n\n    // Is undo with Ctrl+Z or Cmd+Z + Shift key = Redo\n    if (\n      (event.ctrlKey || event.metaKey) &&\n      event.key === \"z\" &&\n      event.shiftKey\n    ) {\n      event.preventDefault();\n      if (redoStack.current.length === 0) return;\n\n      const nextState = redoStack.current.pop();\n      if (!nextState) return;\n\n      undoStack.current.push({\n        value: promptInput,\n        cursorPositionStart: textareaRef.current.selectionStart,\n        cursorPositionEnd: textareaRef.current.selectionEnd,\n      });\n      setPromptInput(nextState.value);\n      setTimeout(() => {\n        textareaRef.current.setSelectionRange(\n          nextState.cursorPositionStart,\n          nextState.cursorPositionEnd\n        );\n      }, 0);\n    }\n\n    // Undo with Ctrl+Z or Cmd+Z\n    if (\n      (event.ctrlKey || event.metaKey) &&\n      event.key === \"z\" &&\n      !event.shiftKey\n    ) {\n      if (undoStack.current.length === 0) return;\n      const lastState = undoStack.current.pop();\n      if (!lastState) return;\n\n      redoStack.current.push({\n        value: promptInput,\n        cursorPositionStart: textareaRef.current.selectionStart,\n        cursorPositionEnd: textareaRef.current.selectionEnd,\n      });\n      setPromptInput(lastState.value);\n      setTimeout(() => {\n        textareaRef.current.setSelectionRange(\n          lastState.cursorPositionStart,\n          lastState.cursorPositionEnd\n        );\n      }, 0);\n    }\n  }\n\n  function adjustTextArea(event) {\n    const element = event.target;\n    element.style.height = \"auto\";\n    element.style.height = `${element.scrollHeight}px`;\n  }\n\n  function handlePasteEvent(e) {\n    e.preventDefault();\n    if (e.clipboardData.items.length === 0) return false;\n\n    // paste any clipboard items that are images.\n    for (const item of e.clipboardData.items) {\n      if (item.type.startsWith(\"image/\")) {\n        const file = item.getAsFile();\n        window.dispatchEvent(\n          new CustomEvent(PASTE_ATTACHMENT_EVENT, {\n            detail: { files: [file] },\n          })\n        );\n        continue;\n      }\n\n      // handle files specifically that are not images as uploads\n      if (item.kind === \"file\") {\n        const file = item.getAsFile();\n        window.dispatchEvent(\n          new CustomEvent(PASTE_ATTACHMENT_EVENT, {\n            detail: { files: [file] },\n          })\n        );\n        continue;\n      }\n    }\n\n    const pasteText = e.clipboardData.getData(\"text/plain\");\n    if (pasteText) {\n      const textarea = textareaRef.current;\n      const start = textarea.selectionStart;\n      const end = textarea.selectionEnd;\n      const newPromptInput =\n        promptInput.substring(0, start) +\n        pasteText +\n        promptInput.substring(end);\n      setPromptInput(newPromptInput);\n\n      // Set the cursor position after the pasted text\n      // we need to use setTimeout to prevent the cursor from being set to the end of the text\n      setTimeout(() => {\n        textarea.selectionStart = textarea.selectionEnd =\n          start + pasteText.length;\n        adjustTextArea({ target: textarea });\n      }, 0);\n    }\n    return;\n  }\n\n  function handleChange(e) {\n    debouncedSaveState(-1);\n    adjustTextArea(e);\n    const value = e.target.value;\n    setPromptInput(value);\n\n    // Auto-dismiss the tools menu when the \"/\" that opened it is modified\n    if (autoOpenedToolsRef.current && showTools && value !== \"/\") {\n      setShowTools(false);\n      autoOpenedToolsRef.current = false;\n    }\n  }\n\n  return (\n    <div\n      className={\n        centered\n          ? \"w-full relative flex justify-center items-center\"\n          : \"w-full fixed md:absolute bottom-0 left-0 z-10 flex justify-center items-center pwa:pb-5\"\n      }\n    >\n      <form\n        onSubmit={handleSubmit}\n        className={\n          centered\n            ? \"flex flex-col gap-y-1 rounded-t-lg w-full items-center\"\n            : \"flex flex-col gap-y-1 rounded-t-lg md:w-full w-full mx-auto max-w-[750px] items-center\"\n        }\n      >\n        <div\n          className={`flex items-center rounded-lg md:w-full ${centered ? \"mb-0\" : \"mb-4\"}`}\n        >\n          <div className=\"relative w-[95vw] md:w-[750px]\">\n            <ToolsMenu\n              workspace={workspace}\n              showing={showTools}\n              setShowing={setShowTools}\n              sendCommand={sendCommand}\n              promptRef={textareaRef}\n              centered={centered}\n              highlightedIndexRef={toolsHighlightRef}\n            />\n            <div className=\"bg-zinc-800 light:bg-white light:border light:border-slate-300 rounded-[20px] pwa:rounded-3xl flex flex-col px-5 overflow-hidden\">\n              <AttachmentManager attachments={attachments} />\n              <div className=\"flex items-center\">\n                <textarea\n                  id={PROMPT_INPUT_ID}\n                  ref={textareaRef}\n                  onChange={handleChange}\n                  onKeyDown={captureEnterOrUndo}\n                  onPaste={(e) => {\n                    saveCurrentState();\n                    handlePasteEvent(e);\n                  }}\n                  required={true}\n                  onFocus={() => setFocused(true)}\n                  onBlur={(e) => {\n                    setFocused(false);\n                    adjustTextArea(e);\n                  }}\n                  value={promptInput}\n                  spellCheck={Appearance.get(\"enableSpellCheck\")}\n                  className={`border-none cursor-text max-h-[50vh] md:max-h-[350px] md:min-h-[40px] pt-[20px] w-full leading-5 text-white light:text-slate-600 bg-transparent placeholder:text-white/60 light:placeholder:text-slate-400 resize-none active:outline-none focus:outline-none flex-grow pwa:!text-[16px] ${textSizeClass}`}\n                  placeholder={t(\"chat_window.send_message\")}\n                />\n              </div>\n              <div className=\"flex justify-between items-center pt-3.5 pb-3\">\n                <div className=\"flex items-center gap-x-0.25\">\n                  <div className=\"flex items-center gap-x-1\">\n                    <AttachItem\n                      workspaceSlug={workspaceSlug}\n                      workspaceThreadSlug={threadSlug}\n                    />\n                    <AgentSessionButton\n                      sendCommand={sendCommand}\n                      promptInput={promptInput}\n                      textareaRef={textareaRef}\n                      visible={!agentSessionActive & showAgentCommand}\n                    />\n                  </div>\n                  <ToolsButton\n                    showTools={showTools}\n                    setShowTools={setShowTools}\n                    textareaRef={textareaRef}\n                    autoOpenedToolsRef={autoOpenedToolsRef}\n                  />\n                </div>\n                <div className=\"flex gap-x-2 items-center\">\n                  <SpeechToText sendCommand={sendCommand} />\n                  {isStreaming ? (\n                    <StopGenerationButton />\n                  ) : (\n                    <SendPromptButton\n                      formRef={formRef}\n                      promptInput={promptInput}\n                      isDisabled={isDisabled}\n                    />\n                  )}\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </form>\n    </div>\n  );\n}\n\nfunction AgentSessionButton({\n  sendCommand,\n  promptInput,\n  textareaRef,\n  visible = true,\n}) {\n  const { t } = useTranslation();\n  if (!visible) return null;\n\n  function handleClick() {\n    try {\n      if (promptInput?.trim()?.startsWith(\"@agent\")) return;\n      sendCommand({ text: \"@agent\", writeMode: \"prepend\" });\n    } finally {\n      textareaRef?.current?.focus();\n    }\n  }\n\n  return (\n    <>\n      <button\n        type=\"button\"\n        onClick={handleClick}\n        data-tooltip-id=\"agent-session\"\n        data-tooltip-content={t(\"chat_window.start_agent_session\")}\n        aria-label={t(\"chat_window.start_agent_session\")}\n        className=\"group border-none relative flex justify-center items-center cursor-pointer w-6 h-6 rounded-full hover:bg-zinc-700 light:hover:bg-slate-200\"\n      >\n        <At\n          size={18}\n          className=\"pointer-events-none text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-600 shrink-0\"\n        />\n      </button>\n      <Tooltip\n        id=\"agent-session\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip !text-xs z-99\"\n      />\n    </>\n  );\n}\n\nfunction ToolsButton({\n  showTools,\n  setShowTools,\n  textareaRef,\n  autoOpenedToolsRef,\n}) {\n  const { t } = useTranslation();\n\n  return (\n    <button\n      id=\"tools-btn\"\n      type=\"button\"\n      onClick={() => {\n        autoOpenedToolsRef.current = false;\n        setShowTools(!showTools);\n        textareaRef.current?.focus();\n      }}\n      className={`group border-none cursor-pointer flex items-center justify-center h-6 px-2 rounded-full ${\n        showTools\n          ? \"bg-zinc-700 light:bg-slate-200\"\n          : \"hover:bg-zinc-700 light:hover:bg-slate-200\"\n      }`}\n    >\n      <span\n        className={`text-sm font-medium ${\n          showTools\n            ? \"text-white light:text-slate-800\"\n            : \"text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-800\"\n        }`}\n      >\n        {t(\"chat_window.tools\")}\n      </span>\n    </button>\n  );\n}\n\nfunction SendPromptButton({ formRef, promptInput, isDisabled }) {\n  const { t } = useTranslation();\n\n  return (\n    <>\n      <button\n        ref={formRef}\n        type=\"submit\"\n        disabled={isDisabled || !promptInput.trim().length}\n        className={`border-none flex justify-center items-center rounded-full w-8 h-8 transition-all ${\n          promptInput.trim().length && !isDisabled\n            ? \"cursor-pointer bg-white hover:bg-zinc-200 light:bg-slate-800 light:hover:bg-slate-600\"\n            : \"cursor-not-allowed bg-zinc-600 light:bg-slate-400\"\n        }`}\n        data-tooltip-id=\"send-prompt\"\n        data-tooltip-content={\n          isDisabled\n            ? t(\"chat_window.attachments_processing\")\n            : t(\"chat_window.send\")\n        }\n        aria-label={t(\"chat_window.send\")}\n      >\n        <ArrowUp\n          className=\"w-[18px] h-[18px] pointer-events-none text-zinc-800 light:text-white\"\n          weight=\"bold\"\n        />\n        <span className=\"sr-only\">{t(\"chat_window.send\")}</span>\n      </button>\n      <Tooltip\n        id=\"send-prompt\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip !text-xs z-99\"\n      />\n    </>\n  );\n}\n\n/**\n * Handle event listeners to prevent the send button from being used\n * for whatever reason that may we may want to prevent the user from sending a message.\n */\nfunction useIsDisabled() {\n  const [isDisabled, setIsDisabled] = useState(false);\n\n  /**\n   * Handle attachments processing and processed events\n   * to prevent the send button from being clicked when attachments are processing\n   * or else the query may not have relevant context since RAG is not yet ready.\n   */\n  useEffect(() => {\n    if (!window) return;\n    const onProcessing = () => setIsDisabled(true);\n    const onProcessed = () => setIsDisabled(false);\n\n    window.addEventListener(ATTACHMENTS_PROCESSING_EVENT, onProcessing);\n    window.addEventListener(ATTACHMENTS_PROCESSED_EVENT, onProcessed);\n\n    return () => {\n      window.removeEventListener(ATTACHMENTS_PROCESSING_EVENT, onProcessing);\n      window.removeEventListener(ATTACHMENTS_PROCESSED_EVENT, onProcessed);\n    };\n  }, []);\n\n  return { isDisabled };\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/SourcesSidebar/MobileCitationModal/SourceDetailView/index.jsx",
    "content": "import { Fragment } from \"react\";\nimport { CaretLeft, Info, X } from \"@phosphor-icons/react\";\nimport { decode as HTMLDecode } from \"he\";\nimport truncate from \"truncate\";\nimport { useTranslation } from \"react-i18next\";\nimport { omitChunkHeader } from \"../../../ChatHistory/Citation\";\nimport { toPercentString } from \"@/utils/numbers\";\n\nexport default function SourceDetailView({ source, onBack, onClose }) {\n  const { t } = useTranslation();\n  return (\n    <>\n      <div className=\"flex items-center justify-between\">\n        <button\n          onClick={onBack}\n          type=\"button\"\n          className=\"text-white/60 light:text-slate-400 hover:text-white light:hover:text-slate-900 transition-colors\"\n        >\n          <CaretLeft size={20} weight=\"bold\" />\n        </button>\n        <p className=\"font-semibold text-base leading-6 text-white light:text-slate-900 truncate px-2\">\n          {truncate(source.title, 30)}\n        </p>\n        <button\n          onClick={onClose}\n          type=\"button\"\n          className=\"text-white/60 light:text-slate-400 hover:text-white light:hover:text-slate-900 transition-colors\"\n        >\n          <X size={16} weight=\"bold\" />\n        </button>\n      </div>\n      <div className=\"flex flex-col overflow-y-auto no-scroll\">\n        {source.chunks.map(({ text, score }, idx) => (\n          <Fragment key={idx}>\n            <div className=\"flex flex-col gap-y-1 py-4\">\n              <p className=\"text-sm leading-[20px] text-white light:text-slate-900\">\n                {HTMLDecode(omitChunkHeader(text))}\n              </p>\n              {!!score && (\n                <div className=\"flex items-center text-xs text-white/60 light:text-slate-500 gap-x-1\">\n                  <Info size={14} />\n                  <p>\n                    {toPercentString(score)} {t(\"chat_window.similarity_match\")}\n                  </p>\n                </div>\n              )}\n            </div>\n            {idx !== source.chunks.length - 1 && (\n              <hr className=\"border-zinc-700 light:border-slate-300\" />\n            )}\n          </Fragment>\n        ))}\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/SourcesSidebar/MobileCitationModal/index.jsx",
    "content": "import { X } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { combineLikeSources } from \"../../ChatHistory/Citation\";\nimport SourceDetailView from \"./SourceDetailView\";\nimport SourceItem from \"../SourceItem\";\n\nexport default function MobileCitationModal({\n  sources: rawSources,\n  isOpen,\n  selectedSource,\n  setSelectedSource,\n  onClose,\n}) {\n  const sources = combineLikeSources(rawSources);\n  const { t } = useTranslation();\n\n  return (\n    <ModalWrapper isOpen={isOpen}>\n      <div className=\"fixed inset-0\" onClick={onClose} />\n      <div className=\"relative z-10 w-[calc(100%-40px)] max-h-[70vh] rounded-[16px] bg-zinc-800 light:bg-white light:border-2 light:border-slate-300 p-4 flex flex-col gap-4\">\n        {selectedSource ? (\n          <SourceDetailView\n            source={selectedSource}\n            onBack={() => setSelectedSource(null)}\n            onClose={onClose}\n          />\n        ) : (\n          <>\n            <div className=\"flex items-center justify-between\">\n              <p className=\"font-semibold text-base leading-6 text-white light:text-slate-900\">\n                {t(\"chat_window.sources\")}\n              </p>\n              <button\n                onClick={onClose}\n                type=\"button\"\n                className=\"text-white/60 light:text-slate-400 hover:text-white light:hover:text-slate-900 transition-colors\"\n              >\n                <X size={16} weight=\"bold\" />\n              </button>\n            </div>\n            <div className=\"flex flex-col gap-3 overflow-y-auto no-scroll\">\n              {sources.map((source, idx) => (\n                <SourceItem\n                  key={source.title || idx}\n                  source={source}\n                  onClick={() => setSelectedSource(source)}\n                />\n              ))}\n            </div>\n          </>\n        )}\n      </div>\n    </ModalWrapper>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/SourcesSidebar/SourceItem/index.jsx",
    "content": "import { parseChunkSource, SourceTypeCircle } from \"../../ChatHistory/Citation\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function SourceItem({ source, onClick }) {\n  const { t } = useTranslation();\n  const info = parseChunkSource(source);\n  const subtitle = info.isUrl ? info.text : t(\"chat_window.document\");\n\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      className=\"flex flex-col gap-[2px] items-start w-full text-left hover:opacity-75 transition-opacity\"\n    >\n      <div className=\"flex gap-[6px] items-start w-full\">\n        <SourceTypeCircle\n          type={info.icon}\n          size={16}\n          iconSize={10}\n          url={info.href}\n        />\n        <p className=\"flex-1 font-medium text-sm text-white light:text-slate-900 leading-[15px] truncate\">\n          {source.title}\n        </p>\n      </div>\n      <div className=\"flex flex-col gap-[2px] pl-[22px] text-[10px] text-zinc-400 light:text-slate-500 leading-[14px]\">\n        <p>{subtitle}</p>\n        <p>{t(\"chat_window.source_count\", { count: source.references })}</p>\n      </div>\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/SourcesSidebar/index.jsx",
    "content": "import { createContext, useContext, useState } from \"react\";\nimport { isMobile } from \"react-device-detect\";\nimport { useTranslation } from \"react-i18next\";\nimport { X } from \"@phosphor-icons/react\";\nimport {\n  combineLikeSources,\n  CitationDetailModal,\n} from \"../ChatHistory/Citation\";\nimport MobileCitationModal from \"./MobileCitationModal\";\nimport SourceItem from \"./SourceItem\";\n\nexport const SourcesSidebarContext = createContext();\n\nexport function SourcesSidebarProvider({ children }) {\n  const [sources, setSources] = useState([]);\n  const [sidebarOpen, setSidebarOpen] = useState(false);\n\n  function openSidebar(newSources) {\n    setSources(newSources);\n    setSidebarOpen(true);\n  }\n\n  function closeSidebar() {\n    setSidebarOpen(false);\n  }\n\n  return (\n    <SourcesSidebarContext.Provider\n      value={{ sources, sidebarOpen, openSidebar, closeSidebar }}\n    >\n      {children}\n    </SourcesSidebarContext.Provider>\n  );\n}\n\nexport function useSourcesSidebar() {\n  return useContext(SourcesSidebarContext);\n}\n\nexport default function SourcesSidebar() {\n  const { sources, sidebarOpen, closeSidebar } = useSourcesSidebar();\n  const { t } = useTranslation();\n  const [selectedSource, setSelectedSource] = useState(null);\n\n  const combined = combineLikeSources(sources);\n\n  if (isMobile) {\n    return (\n      <MobileCitationModal\n        sources={sources}\n        isOpen={sidebarOpen}\n        selectedSource={selectedSource}\n        setSelectedSource={setSelectedSource}\n        onClose={() => {\n          setSelectedSource(null);\n          closeSidebar();\n        }}\n      />\n    );\n  }\n\n  return (\n    <>\n      <div\n        className=\"h-full overflow-hidden transition-all duration-500 flex-shrink-0\"\n        style={{ width: sidebarOpen ? \"366px\" : \"0px\" }}\n      >\n        <div\n          className=\"ml-4 w-[350px] bg-zinc-900 light:bg-white light:border-2 light:border-slate-300 md:rounded-[16px] p-4 flex flex-col gap-4 overflow-hidden mt-[72px]\"\n          style={{ maxHeight: \"calc(100% - 88px)\" }}\n        >\n          <div className=\"flex items-start justify-between\">\n            <p className=\"font-medium text-base leading-6 text-white light:text-slate-900\">\n              {t(\"chat_window.sources\")}\n            </p>\n            <button\n              onClick={closeSidebar}\n              type=\"button\"\n              className=\"text-white/60 light:text-slate-400 hover:text-white light:hover:text-slate-900 transition-colors\"\n            >\n              <X size={16} weight=\"bold\" />\n            </button>\n          </div>\n          <div className=\"flex flex-col gap-3 overflow-y-auto no-scroll\">\n            {combined.map((source, idx) => (\n              <SourceItem\n                key={source.title || idx}\n                source={source}\n                onClick={() => setSelectedSource(source)}\n              />\n            ))}\n          </div>\n        </div>\n      </div>\n      {selectedSource && (\n        <CitationDetailModal\n          source={selectedSource}\n          onClose={() => setSelectedSource(null)}\n        />\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/TextSizeMenu/index.jsx",
    "content": "import { useState, useRef, useEffect, useMemo } from \"react\";\nimport { SlidersHorizontal } from \"@phosphor-icons/react\";\nimport useLoginMode from \"@/hooks/useLoginMode\";\nimport { useTranslation } from \"react-i18next\";\n\nfunction getTextSizes(t) {\n  return [\n    { key: \"small\", label: t(\"chat_window.small\"), textClass: \"text-xs\" },\n    { key: \"normal\", label: t(\"chat_window.normal\"), textClass: \"text-sm\" },\n    { key: \"large\", label: t(\"chat_window.large\"), textClass: \"text-base\" },\n  ];\n}\n\nexport default function TextSizeMenu() {\n  const { t } = useTranslation();\n  const TEXT_SIZES = useMemo(() => getTextSizes(t), [t]);\n  const mode = useLoginMode();\n  const [showMenu, setShowMenu] = useState(false);\n  const [selectedSize, setSelectedSize] = useState(\n    window.localStorage.getItem(\"anythingllm_text_size\") || \"normal\"\n  );\n  const menuRef = useRef(null);\n  const buttonRef = useRef(null);\n\n  useEffect(() => {\n    if (!showMenu) return;\n    function handleClickOutside(e) {\n      if (\n        menuRef.current &&\n        !menuRef.current.contains(e.target) &&\n        buttonRef.current &&\n        !buttonRef.current.contains(e.target)\n      ) {\n        setShowMenu(false);\n      }\n    }\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n  }, [showMenu]);\n\n  function handleTextSizeChange(size) {\n    setSelectedSize(size);\n    window.localStorage.setItem(\"anythingllm_text_size\", size);\n    window.dispatchEvent(new CustomEvent(\"textSizeChange\", { detail: size }));\n  }\n\n  // User icon is visible when login mode is active (single with password or multi-user)\n  const hasUserIcon = mode !== null;\n\n  return (\n    <div\n      className={`absolute top-3 md:top-5 z-30 ${hasUserIcon ? \"right-[55px] md:right-[67px]\" : \"right-4 md:right-6\"}`}\n    >\n      <button\n        ref={buttonRef}\n        type=\"button\"\n        onClick={() => setShowMenu(!showMenu)}\n        className={`group border-none cursor-pointer flex items-center justify-center w-[35px] h-[35px] rounded-full transition-all ${\n          showMenu\n            ? \"bg-zinc-700 light:bg-slate-200\"\n            : \"hover:bg-zinc-700 light:hover:bg-slate-200\"\n        }`}\n      >\n        <SlidersHorizontal\n          size={18}\n          className={\n            showMenu\n              ? \"text-white light:text-slate-800\"\n              : \"text-zinc-300 light:text-slate-600 group-hover:text-white light:group-hover:text-slate-800\"\n          }\n        />\n      </button>\n\n      {showMenu && (\n        <div\n          ref={menuRef}\n          className=\"absolute right-0 top-[42px] bg-zinc-800 light:bg-white border border-zinc-700 light:border-slate-300 rounded-lg p-3 w-[200px] flex flex-col gap-1 shadow-lg\"\n        >\n          <p className=\"text-[10px] font-medium text-zinc-400 light:text-slate-500 px-2 mb-0.5\">\n            {t(\"chat_window.text_size_label\")}\n          </p>\n          {TEXT_SIZES.map(({ key, label, textClass }) => (\n            <div\n              key={key}\n              onClick={() => handleTextSizeChange(key)}\n              className={`flex items-center px-2 py-1 rounded cursor-pointer ${\n                selectedSize === key\n                  ? \"bg-zinc-700 light:bg-slate-200\"\n                  : \"hover:bg-zinc-700/50 light:hover:bg-slate-100\"\n              }`}\n            >\n              <span className={`${textClass} text-white light:text-slate-900`}>\n                {label}\n              </span>\n            </div>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/WorkspaceModelPicker/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport { useTranslation } from \"react-i18next\";\nimport useUser from \"@/hooks/useUser\";\nimport { useModal } from \"@/hooks/useModal\";\nimport LLMSelectorModal from \"../PromptInput/LLMSelector/index\";\nimport SetupProvider from \"../PromptInput/LLMSelector/SetupProvider\";\nimport {\n  SAVE_LLM_SELECTOR_EVENT,\n  PROVIDER_SETUP_EVENT,\n} from \"../PromptInput/LLMSelector/action\";\nimport Workspace from \"@/models/workspace\";\nimport System from \"@/models/system\";\nimport { SIDEBAR_TOGGLE_EVENT } from \"@/components/Sidebar/SidebarToggle\";\n\nfunction fetchModelName(slug, setModelName) {\n  if (!slug) return;\n  Promise.all([Workspace.bySlug(slug), System.keys()]).then(\n    ([workspace, systemSettings]) => {\n      const model = workspace.chatModel ?? systemSettings?.LLMModel ?? \"\";\n      setModelName(model);\n    }\n  );\n}\n\nexport default function WorkspaceModelPicker({ workspaceSlug = null }) {\n  const { t } = useTranslation();\n  const { slug: urlSlug } = useParams();\n  const slug = urlSlug ?? workspaceSlug;\n  const { user } = useUser();\n  const [showSelector, setShowSelector] = useState(false);\n  const [modelName, setModelName] = useState(\"\");\n  const {\n    isOpen: isSetupProviderOpen,\n    openModal: openSetupProviderModal,\n    closeModal: closeSetupProviderModal,\n  } = useModal();\n  const [config, setConfig] = useState({ settings: {}, provider: null });\n  const [refreshKey, setRefreshKey] = useState(0);\n  const [sidebarOpen, setSidebarOpen] = useState(\n    () => window.localStorage.getItem(\"anythingllm_sidebar_toggle\") !== \"closed\"\n  );\n\n  useEffect(() => {\n    const handleToggle = (e) => setSidebarOpen(e.detail.open);\n    window.addEventListener(SIDEBAR_TOGGLE_EVENT, handleToggle);\n    return () => window.removeEventListener(SIDEBAR_TOGGLE_EVENT, handleToggle);\n  }, []);\n\n  // Fetch current model name for display\n  useEffect(() => fetchModelName(slug, setModelName), [slug]);\n\n  // Close selector and refresh model name when model is saved\n  useEffect(() => {\n    function handleSave() {\n      setShowSelector(false);\n      fetchModelName(slug, setModelName);\n    }\n    window.addEventListener(SAVE_LLM_SELECTOR_EVENT, handleSave);\n    return () =>\n      window.removeEventListener(SAVE_LLM_SELECTOR_EVENT, handleSave);\n  }, [slug]);\n\n  // Handle provider setup request\n  useEffect(() => {\n    function handleProviderSetup(e) {\n      const { provider, settings } = e.detail;\n      setConfig({ settings, provider });\n      setTimeout(() => openSetupProviderModal(), 300);\n    }\n    window.addEventListener(PROVIDER_SETUP_EVENT, handleProviderSetup);\n    return () =>\n      window.removeEventListener(PROVIDER_SETUP_EVENT, handleProviderSetup);\n  }, []);\n\n  // This feature is disabled for multi-user instances where the user is not an admin\n  if (!!user && user.role !== \"admin\") return null;\n  if (!slug) return null;\n\n  return (\n    <>\n      {showSelector && (\n        <div\n          className=\"fixed inset-0 z-20\"\n          onClick={() => setShowSelector(false)}\n        />\n      )}\n      <div\n        className={`hidden md:block absolute top-2 z-30 transition-all duration-500 ${\n          sidebarOpen ? \"left-3\" : \"left-11\"\n        }`}\n      >\n        <button\n          type=\"button\"\n          onClick={() => setShowSelector(!showSelector)}\n          className={`group border-none cursor-pointer px-2.5 py-1 flex items-center rounded-full transition-all ${\n            showSelector\n              ? \"bg-zinc-700 light:bg-slate-200\"\n              : \"hover:bg-zinc-700 light:hover:bg-slate-200\"\n          }`}\n        >\n          <span\n            className={`text-xs ${\n              showSelector\n                ? \"text-white light:text-slate-800\"\n                : \"text-zinc-500 light:text-slate-500 group-hover:text-white light:group-hover:text-slate-800\"\n            }`}\n          >\n            {modelName || t(\"chat_window.select_model\")}\n          </span>\n        </button>\n\n        {showSelector && (\n          <div className=\"absolute left-0 top-full mt-1 bg-zinc-800 light:bg-white border border-zinc-700 light:border-slate-300 rounded-xl shadow-lg w-[620px] overflow-hidden\">\n            <LLMSelectorModal\n              key={refreshKey}\n              workspaceSlug={slug}\n              initialProvider={config.provider?.value}\n            />\n          </div>\n        )}\n      </div>\n\n      <SetupProvider\n        isOpen={isSetupProviderOpen}\n        closeModal={closeSetupProviderModal}\n        postSubmit={() => {\n          closeSetupProviderModal();\n          setRefreshKey((k) => k + 1);\n        }}\n        settings={config.settings}\n        llmProvider={config.provider}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/ChatContainer/index.jsx",
    "content": "import { useState, useEffect, useContext, useRef } from \"react\";\nimport ChatHistory from \"./ChatHistory\";\nimport { CLEAR_ATTACHMENTS_EVENT, DndUploaderContext } from \"./DnDWrapper\";\nimport PromptInput, {\n  PROMPT_INPUT_EVENT,\n  PROMPT_INPUT_ID,\n} from \"./PromptInput\";\nimport Workspace from \"@/models/workspace\";\nimport handleChat, { ABORT_STREAM_EVENT } from \"@/utils/chat\";\nimport { isMobile } from \"react-device-detect\";\nimport { SidebarMobileHeader } from \"../../Sidebar\";\nimport { useNavigate, useParams } from \"react-router-dom\";\nimport { v4 } from \"uuid\";\nimport handleSocketResponse, {\n  websocketURI,\n  AGENT_SESSION_END,\n  AGENT_SESSION_START,\n  setAgentSessionActive,\n} from \"@/utils/chat/agent\";\nimport DnDFileUploaderWrapper from \"./DnDWrapper\";\nimport SpeechRecognition, {\n  useSpeechRecognition,\n} from \"react-speech-recognition\";\nimport { ChatTooltips } from \"./ChatTooltips\";\nimport { MetricsProvider } from \"./ChatHistory/HistoricalMessage/Actions/RenderMetrics\";\nimport useChatContainerQuickScroll from \"@/hooks/useChatContainerQuickScroll\";\nimport { PENDING_HOME_MESSAGE } from \"@/utils/constants\";\nimport { clearPromptInputDraft } from \"@/hooks/usePromptInputStorage\";\nimport { safeJsonParse } from \"@/utils/request\";\nimport { useTranslation } from \"react-i18next\";\nimport paths from \"@/utils/paths\";\nimport QuickActions from \"@/components/lib/QuickActions\";\nimport SuggestedMessages from \"@/components/lib/SuggestedMessages\";\nimport TextSizeMenu from \"./TextSizeMenu\";\nimport WorkspaceModelPicker from \"./WorkspaceModelPicker\";\nimport SourcesSidebar, { SourcesSidebarProvider } from \"./SourcesSidebar\";\n\nexport default function ChatContainer({ workspace, knownHistory = [] }) {\n  const navigate = useNavigate();\n  const { t } = useTranslation();\n  const { threadSlug = null } = useParams();\n  const [loadingResponse, setLoadingResponse] = useState(false);\n  const [chatHistory, setChatHistory] = useState(knownHistory);\n  const [socketId, setSocketId] = useState(null);\n  const [websocket, setWebsocket] = useState(null);\n  const { files, parseAttachments } = useContext(DndUploaderContext);\n  const { chatHistoryRef } = useChatContainerQuickScroll();\n  const pendingMessageChecked = useRef(false);\n\n  const { listening, resetTranscript } = useSpeechRecognition({\n    clearTranscriptOnListen: true,\n  });\n\n  /**\n   * Emit an update to the state of the prompt input without directly\n   * passing a prop in so that it does not re-render constantly.\n   * @param {string} messageContent - The message content to set\n   * @param {'replace' | 'append'} writeMode - Replace current text or append to existing text (default: replace)\n   */\n  function setMessageEmit(messageContent = \"\", writeMode = \"replace\") {\n    window.dispatchEvent(\n      new CustomEvent(PROMPT_INPUT_EVENT, {\n        detail: { messageContent, writeMode },\n      })\n    );\n  }\n\n  const handleSubmit = async (event) => {\n    event.preventDefault();\n    const currentMessage =\n      document.getElementById(PROMPT_INPUT_ID)?.value || \"\";\n    if (!currentMessage) return false;\n\n    // Clear the localStorage draft for this thread/workspace so that if the\n    // PromptInput remounts (empty→chat transition), it won't restore stale text\n    clearPromptInputDraft(threadSlug ?? workspace.slug);\n\n    const prevChatHistory = [\n      ...chatHistory,\n      {\n        content: currentMessage,\n        role: \"user\",\n        attachments: parseAttachments(),\n      },\n      {\n        content: \"\",\n        role: \"assistant\",\n        pending: true,\n        userMessage: currentMessage,\n        animate: true,\n      },\n    ];\n\n    if (listening) {\n      // Stop the mic if the send button is clicked\n      endSTTSession();\n    }\n    setChatHistory(prevChatHistory);\n    setMessageEmit(\"\");\n    setLoadingResponse(true);\n  };\n\n  function endSTTSession() {\n    SpeechRecognition.stopListening();\n    resetTranscript();\n  }\n\n  const regenerateAssistantMessage = (chatId) => {\n    const updatedHistory = chatHistory.slice(0, -1);\n    const lastUserMessage = updatedHistory.slice(-1)[0];\n    Workspace.deleteChats(workspace.slug, [chatId])\n      .then(() =>\n        sendCommand({\n          text: lastUserMessage.content,\n          autoSubmit: true,\n          history: updatedHistory,\n          attachments: lastUserMessage?.attachments,\n        })\n      )\n      .catch((e) => console.error(e));\n  };\n\n  /**\n   * Send a command to the LLM prompt input.\n   * @param {Object} options - Arguments to send to the LLM\n   * @param {string} options.text - The text to send to the LLM\n   * @param {boolean} options.autoSubmit - Determines if the text should be sent immediately or if it should be added to the message state (default: false)\n   * @param {Object[]} options.history - The history of the chat prior to this message for overriding the current chat history\n   * @param {Object[import(\"./DnDWrapper\").Attachment]} options.attachments - The attachments to send to the LLM for this message\n   * @param {'replace' | 'append' | 'prepend'} options.writeMode - Replace current text or append to existing text (default: replace)\n   * @returns {void}\n   */\n  const sendCommand = async ({\n    text = \"\",\n    autoSubmit = false,\n    history = [],\n    attachments = [],\n    writeMode = \"replace\",\n  } = {}) => {\n    // If we are not auto-submitting, we can just emit the text to the prompt input.\n    if (!autoSubmit) {\n      setMessageEmit(text, writeMode);\n      return;\n    }\n\n    if (writeMode === \"prepend\") {\n      const currentText = document.getElementById(PROMPT_INPUT_ID)?.value ?? \"\";\n      text = currentText + \" \" + text;\n    }\n\n    // If we are auto-submitting in append mode\n    // than we need to update text with whatever is in the prompt input + the text we are sending.\n    // @note: `message` will not work here since it is not updated yet.\n    // If text is still empty, after this, then we should just return.\n    if (writeMode === \"append\") {\n      const currentText = document.getElementById(PROMPT_INPUT_ID)?.value ?? \"\";\n      text = currentText + text;\n    }\n\n    if (!text || text === \"\") return false;\n\n    // Clear the localStorage draft so that if the PromptInput remounts\n    // (e.g. /reset causing empty→chat or chat→empty transitions),\n    // it won't restore stale text.\n    clearPromptInputDraft(threadSlug ?? workspace.slug);\n\n    // If we are auto-submitting\n    // Then we can replace the current text since this is not accumulating.\n    let prevChatHistory;\n    if (history.length > 0) {\n      // use pre-determined history chain.\n      prevChatHistory = [\n        ...history,\n        {\n          content: \"\",\n          role: \"assistant\",\n          pending: true,\n          userMessage: text,\n          attachments,\n          animate: true,\n        },\n      ];\n    } else {\n      prevChatHistory = [\n        ...chatHistory,\n        {\n          content: text,\n          role: \"user\",\n          attachments,\n        },\n        {\n          content: \"\",\n          role: \"assistant\",\n          pending: true,\n          userMessage: text,\n          attachments,\n          animate: true,\n        },\n      ];\n    }\n\n    setChatHistory(prevChatHistory);\n    setMessageEmit(\"\");\n    setLoadingResponse(true);\n  };\n\n  useEffect(() => {\n    if (pendingMessageChecked.current || !workspace?.slug) return;\n    pendingMessageChecked.current = true;\n\n    const pending = safeJsonParse(sessionStorage.getItem(PENDING_HOME_MESSAGE));\n    if (pending?.message) {\n      setTimeout(() => {\n        sessionStorage.removeItem(PENDING_HOME_MESSAGE);\n        sendCommand({\n          text: pending.message,\n          attachments: pending.attachments || [],\n          autoSubmit: true,\n        });\n      }, 100);\n    }\n  }, [workspace?.slug]);\n\n  useEffect(() => {\n    async function fetchReply() {\n      const promptMessage =\n        chatHistory.length > 0 ? chatHistory[chatHistory.length - 1] : null;\n      const remHistory = chatHistory.length > 0 ? chatHistory.slice(0, -1) : [];\n      var _chatHistory = [...remHistory];\n\n      // Override hook for new messages to now go to agents until the connection closes\n      if (!!websocket) {\n        if (!promptMessage || !promptMessage?.userMessage) return false;\n        const attachments = promptMessage?.attachments ?? parseAttachments();\n        window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT));\n        websocket.send(\n          JSON.stringify({\n            type: \"awaitingFeedback\",\n            feedback: promptMessage?.userMessage,\n            attachments,\n          })\n        );\n        return;\n      }\n\n      if (!promptMessage || !promptMessage?.userMessage) return false;\n\n      // If running and edit or regeneration, this history will already have attachments\n      // so no need to parse the current state.\n      const attachments = promptMessage?.attachments ?? parseAttachments();\n      window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT));\n\n      await Workspace.multiplexStream({\n        workspaceSlug: workspace.slug,\n        threadSlug,\n        prompt: promptMessage.userMessage,\n        chatHandler: (chatResult) =>\n          handleChat(\n            chatResult,\n            setLoadingResponse,\n            setChatHistory,\n            remHistory,\n            _chatHistory,\n            setSocketId\n          ),\n        attachments,\n      });\n      return;\n    }\n    loadingResponse === true && fetchReply();\n  }, [loadingResponse, chatHistory, workspace]);\n\n  // TODO: Simplify this WSS stuff\n  useEffect(() => {\n    let socket = null;\n\n    function handleWSS() {\n      try {\n        if (!socketId || !!websocket) return;\n        socket = new WebSocket(\n          `${websocketURI()}/api/agent-invocation/${socketId}`\n        );\n        socket.supportsAgentStreaming = false;\n\n        window.addEventListener(ABORT_STREAM_EVENT, () => {\n          setAgentSessionActive(false);\n          window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));\n          socket?.close();\n        });\n\n        socket.addEventListener(\"message\", (event) => {\n          setLoadingResponse(true);\n          try {\n            handleSocketResponse(socket, event, setChatHistory);\n          } catch {\n            console.error(\"Failed to parse data\");\n            setAgentSessionActive(false);\n            window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));\n            socket.close();\n          }\n          setLoadingResponse(false);\n        });\n\n        socket.addEventListener(\"close\", (_event) => {\n          setAgentSessionActive(false);\n          window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));\n          setChatHistory((prev) => [\n            ...prev.filter((msg) => !!msg.content),\n            {\n              uuid: v4(),\n              type: \"statusResponse\",\n              content: \"Agent session complete.\",\n              role: \"assistant\",\n              sources: [],\n              closed: true,\n              error: null,\n              animate: false,\n              pending: false,\n            },\n          ]);\n          setLoadingResponse(false);\n          setWebsocket(null);\n          setSocketId(null);\n        });\n        setWebsocket(socket);\n        setAgentSessionActive(true);\n        window.dispatchEvent(new CustomEvent(AGENT_SESSION_START));\n        window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT));\n      } catch (e) {\n        setChatHistory((prev) => [\n          ...prev.filter((msg) => !!msg.content),\n          {\n            uuid: v4(),\n            type: \"abort\",\n            content: e.message,\n            role: \"assistant\",\n            sources: [],\n            closed: true,\n            error: e.message,\n            animate: false,\n            pending: false,\n          },\n        ]);\n        setLoadingResponse(false);\n        setWebsocket(null);\n        setSocketId(null);\n      }\n    }\n    handleWSS();\n\n    return () => {\n      if (socket) {\n        setAgentSessionActive(false);\n        window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));\n        socket.close();\n      }\n    };\n  }, [socketId]);\n\n  const isEmpty =\n    chatHistory.length === 0 && !sessionStorage.getItem(PENDING_HOME_MESSAGE);\n\n  if (isEmpty) {\n    return (\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-zinc-900 light:bg-white w-full h-full overflow-hidden border-none light:border-solid light:border light:border-theme-modal-border\"\n      >\n        {isMobile && <SidebarMobileHeader />}\n        <TextSizeMenu />\n        <WorkspaceModelPicker workspaceSlug={workspace.slug} />\n        <DnDFileUploaderWrapper>\n          <div className=\"flex flex-col h-full w-full items-center justify-center\">\n            <div className=\"flex flex-col items-center w-full max-w-[750px]\">\n              <h1 className=\"text-white text-xl md:text-2xl mb-11 text-center\">\n                {t(\"main-page.greeting\")}\n              </h1>\n              <PromptInput\n                workspace={workspace}\n                submit={handleSubmit}\n                isStreaming={loadingResponse}\n                sendCommand={sendCommand}\n                attachments={files}\n                centered={true}\n              />\n              <QuickActions\n                hasAvailableWorkspace={!!workspace}\n                onCreateAgent={() => navigate(paths.settings.agentSkills())}\n                onEditWorkspace={() =>\n                  navigate(\n                    paths.workspace.settings.generalAppearance(workspace.slug)\n                  )\n                }\n                onUploadDocument={() =>\n                  document.getElementById(\"dnd-chat-file-uploader\")?.click()\n                }\n              />\n            </div>\n            <SuggestedMessages\n              suggestedMessages={workspace?.suggestedMessages}\n              sendCommand={sendCommand}\n            />\n          </div>\n        </DnDFileUploaderWrapper>\n        <ChatTooltips />\n      </div>\n    );\n  }\n\n  return (\n    <SourcesSidebarProvider>\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative flex md:ml-[2px] md:mr-[16px] md:my-[16px] w-full h-full z-[2]\"\n      >\n        <TextSizeMenu />\n        <div className=\"flex-1 min-w-0 transition-all duration-500 relative md:rounded-[16px] bg-zinc-900 light:bg-white text-white light:text-slate-900 h-full overflow-hidden border-none light:border-solid light:border light:border-theme-modal-border\">\n          {isMobile && <SidebarMobileHeader />}\n          <WorkspaceModelPicker workspaceSlug={workspace.slug} />\n          <DnDFileUploaderWrapper>\n            <div className=\"flex flex-col h-full w-full pb-20 md:pb-0\">\n              <div className=\"contents\">\n                <MetricsProvider>\n                  <ChatHistory\n                    ref={chatHistoryRef}\n                    history={chatHistory}\n                    workspace={workspace}\n                    sendCommand={sendCommand}\n                    updateHistory={setChatHistory}\n                    regenerateAssistantMessage={regenerateAssistantMessage}\n                  />\n                </MetricsProvider>\n                <PromptInput\n                  workspace={workspace}\n                  submit={handleSubmit}\n                  isStreaming={loadingResponse}\n                  sendCommand={sendCommand}\n                  attachments={files}\n                  centered={false}\n                />\n              </div>\n            </div>\n          </DnDFileUploaderWrapper>\n          <ChatTooltips />\n        </div>\n        <SourcesSidebar />\n      </div>\n    </SourcesSidebarProvider>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/LoadingChat/index.jsx",
    "content": "import { isMobile } from \"react-device-detect\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\n\nexport default function LoadingChat() {\n  const highlightColor = \"var(--theme-bg-primary)\";\n  const baseColor = \"var(--theme-bg-secondary)\";\n  return (\n    <div\n      className=\"transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll no-scroll p-4\"\n      style={{ height: \"calc(100% - 32px)\" }}\n    >\n      <Skeleton.default\n        height=\"100px\"\n        width=\"100%\"\n        highlightColor={highlightColor}\n        baseColor={baseColor}\n        count={1}\n        className=\"max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6\"\n        containerClassName=\"flex justify-start\"\n      />\n      <Skeleton.default\n        height=\"100px\"\n        width={isMobile ? \"70%\" : \"45%\"}\n        baseColor={baseColor}\n        highlightColor={highlightColor}\n        count={1}\n        className=\"max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6\"\n        containerClassName=\"flex justify-end\"\n      />\n      <Skeleton.default\n        height=\"100px\"\n        width={isMobile ? \"55%\" : \"30%\"}\n        baseColor={baseColor}\n        highlightColor={highlightColor}\n        count={1}\n        className=\"max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6\"\n        containerClassName=\"flex justify-start\"\n      />\n      <Skeleton.default\n        height=\"100px\"\n        width={isMobile ? \"88%\" : \"25%\"}\n        baseColor={baseColor}\n        highlightColor={highlightColor}\n        count={1}\n        className=\"max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6\"\n        containerClassName=\"flex justify-end\"\n      />\n      <Skeleton.default\n        height=\"160px\"\n        width=\"100%\"\n        baseColor={baseColor}\n        highlightColor={highlightColor}\n        count={1}\n        className=\"max-w-full md:max-w-[80%] p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6\"\n        containerClassName=\"flex justify-start\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/WorkspaceChat/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport Workspace from \"@/models/workspace\";\nimport LoadingChat from \"./LoadingChat\";\nimport ChatContainer from \"./ChatContainer\";\nimport paths from \"@/utils/paths\";\nimport ModalWrapper from \"../ModalWrapper\";\nimport { useParams } from \"react-router-dom\";\nimport { DnDFileUploaderProvider } from \"./ChatContainer/DnDWrapper\";\nimport { WarningCircle } from \"@phosphor-icons/react\";\nimport {\n  TTSProvider,\n  useWatchForAutoPlayAssistantTTSResponse,\n} from \"../contexts/TTSProvider\";\nimport { PENDING_HOME_MESSAGE } from \"@/utils/constants\";\n\nexport default function WorkspaceChat({ loading, workspace }) {\n  useWatchForAutoPlayAssistantTTSResponse();\n  const { threadSlug = null } = useParams();\n  const [history, setHistory] = useState([]);\n  const [loadingHistory, setLoadingHistory] = useState(true);\n\n  useEffect(() => {\n    async function getHistory() {\n      if (loading) return;\n      if (!workspace?.slug) {\n        setLoadingHistory(false);\n        return false;\n      }\n\n      const chatHistory = threadSlug\n        ? await Workspace.threads.chatHistory(workspace.slug, threadSlug)\n        : await Workspace.chatHistory(workspace.slug);\n\n      setHistory(chatHistory);\n      setLoadingHistory(false);\n    }\n    getHistory();\n  }, [workspace, loading]);\n\n  const hasPendingMessage = !!sessionStorage.getItem(PENDING_HOME_MESSAGE);\n  if (loadingHistory) {\n    if (hasPendingMessage) {\n      return (\n        <div className=\"transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full\" />\n      );\n    }\n    return <LoadingChat />;\n  }\n  if (!loading && !loadingHistory && !workspace) {\n    return (\n      <>\n        {loading === false && !workspace && (\n          <ModalWrapper isOpen={true}>\n            <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden\">\n              <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n                <div className=\"w-full flex gap-x-2 items-center\">\n                  <WarningCircle\n                    className=\"text-red-500 w-6 h-6\"\n                    weight=\"fill\"\n                  />\n                  <h3 className=\"text-xl font-semibold text-red-500 overflow-hidden overflow-ellipsis whitespace-nowrap\">\n                    Workspace not found\n                  </h3>\n                </div>\n              </div>\n              <div className=\"py-7 px-9 space-y-2 flex-col\">\n                <p className=\"text-white text-sm\">\n                  The workspace you're looking for is not available. It may have\n                  been deleted or you may not have access to it.\n                </p>\n              </div>\n              <div className=\"flex w-full justify-end items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b\">\n                <a\n                  href={paths.home()}\n                  className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n                >\n                  Return to homepage\n                </a>\n              </div>\n            </div>\n          </ModalWrapper>\n        )}\n        <LoadingChat />\n      </>\n    );\n  }\n\n  setEventDelegatorForCodeSnippets();\n  return (\n    <TTSProvider>\n      <DnDFileUploaderProvider workspace={workspace} threadSlug={threadSlug}>\n        <ChatContainer workspace={workspace} knownHistory={history} />\n      </DnDFileUploaderProvider>\n    </TTSProvider>\n  );\n}\n\n// Enables us to safely markdown and sanitize all responses without risk of injection\n// but still be able to attach a handler to copy code snippets on all elements\n// that are code snippets.\nfunction copyCodeSnippet(uuid) {\n  const target = document.querySelector(`[data-code=\"${uuid}\"]`);\n  if (!target) return false;\n  const markdown =\n    target.parentElement?.parentElement?.querySelector(\n      \"pre:first-of-type\"\n    )?.innerText;\n  if (!markdown) return false;\n\n  window.navigator.clipboard.writeText(markdown);\n  target.classList.add(\"text-green-500\");\n  const originalText = target.innerHTML;\n  target.innerText = \"Copied!\";\n  target.setAttribute(\"disabled\", true);\n\n  setTimeout(() => {\n    target.classList.remove(\"text-green-500\");\n    target.innerHTML = originalText;\n    target.removeAttribute(\"disabled\");\n  }, 2500);\n}\n\n// Listens and hunts for all data-code-snippet clicks.\nexport function setEventDelegatorForCodeSnippets() {\n  document?.addEventListener(\"click\", function (e) {\n    const target = e.target.closest(\"[data-code-snippet]\");\n    const uuidCode = target?.dataset?.code;\n    if (!uuidCode) return false;\n    copyCodeSnippet(uuidCode);\n  });\n}\n"
  },
  {
    "path": "frontend/src/components/contexts/TTSProvider.jsx",
    "content": "import { createContext, useContext, useEffect, useState } from \"react\";\nimport System from \"@/models/system\";\nimport Appearance from \"@/models/appearance\";\n\nconst ASSISTANT_MESSAGE_COMPLETE_EVENT = \"ASSISTANT_MESSAGE_COMPLETE_EVENT\";\nconst TTSProviderContext = createContext();\n\n/**\n * This component is used to provide the TTS provider context to the application.\n *\n * TODO: This context provider simply wraps around the System.keys() call to get the TTS provider settings.\n * However, we use .keys() in a ton of places and it might make more sense to make a generalized hook that\n * can be used anywhere we need to get _any_ setting from the System by just grabbing keys() and reusing it\n * as a hook where needed.\n *\n * For now, since TTSButtons are rendered on every message, we can save a ton of requests by just using this\n * hook where for now so we can recycle the TTS settings in the chat container.\n */\nexport function TTSProvider({ children }) {\n  const [settings, setSettings] = useState({});\n  const [provider, setProvider] = useState(\"native\");\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function getSettings() {\n      const _settings = await System.keys();\n      setProvider(_settings?.TextToSpeechProvider ?? \"native\");\n      setSettings(_settings);\n      setLoading(false);\n    }\n    getSettings();\n  }, []);\n\n  return (\n    <TTSProviderContext.Provider\n      value={{\n        settings,\n        provider,\n        loading,\n      }}\n    >\n      {children}\n    </TTSProviderContext.Provider>\n  );\n}\n\n/**\n * This hook is used to get the TTS provider settings easily without\n * having to refetch the settings from the System.keys() call each component mount.\n *\n * @returns {{settings: {TTSPiperTTSVoiceModel: string|null}, provider: string, loading: boolean}} The TTS provider settings.\n */\nexport function useTTSProvider() {\n  const context = useContext(TTSProviderContext);\n  if (!context)\n    throw new Error(\"useTTSProvider must be used within a TTSProvider\");\n  return context;\n}\n\n/**\n * This function will emit the ASSISTANT_MESSAGE_COMPLETE_EVENT event.\n *\n * This event is used to notify the TTSProvider that a message has been fully generated and that the TTS response\n * should be played if the user setting is enabled.\n *\n * @param {string} chatId - The chatId of the message that has been fully generated.\n */\nexport function emitAssistantMessageCompleteEvent(chatId) {\n  window.dispatchEvent(\n    new CustomEvent(ASSISTANT_MESSAGE_COMPLETE_EVENT, { detail: { chatId } })\n  );\n}\n\n/**\n * This hook will establish a listener for the ASSISTANT_MESSAGE_COMPLETE_EVENT event.\n * When the event is triggered, the hook will attempt to play the TTS response for the given chatId.\n * It will attempt to play the TTS response for the given chatId until it is successful or the maximum number of attempts\n * is reached.\n *\n * This is accomplished by looking for a button with the data-auto-play-chat-id attribute that matches the chatId.\n */\nexport function useWatchForAutoPlayAssistantTTSResponse() {\n  const autoPlayAssistantTtsResponse = Appearance.get(\n    \"autoPlayAssistantTtsResponse\"\n  );\n\n  function handleAutoPlayTTSEvent(event) {\n    let autoPlayAttempts = 0;\n    const { chatId } = event.detail;\n\n    /**\n     * Attempt to play the TTS response for the given chatId.\n     * This is a recursive function that will attempt to play the TTS response\n     * for the given chatId until it is successful or the maximum number of attempts\n     * is reached.\n     * @returns {boolean} true if the TTS response was played, false otherwise.\n     */\n    function attemptToPlay() {\n      const playBtn = document.querySelector(\n        `[data-auto-play-chat-id=\"${chatId}\"]`\n      );\n      if (!playBtn) {\n        autoPlayAttempts++;\n        if (autoPlayAttempts > 3) return false;\n        setTimeout(() => {\n          attemptToPlay();\n        }, 1000 * autoPlayAttempts);\n        return false;\n      }\n      playBtn.click();\n      return true;\n    }\n    setTimeout(() => {\n      attemptToPlay();\n    }, 800);\n  }\n\n  // Only bother to listen for these events if the user has autoPlayAssistantTtsResponse\n  // setting enabled.\n  useEffect(() => {\n    if (autoPlayAssistantTtsResponse) {\n      window.addEventListener(\n        ASSISTANT_MESSAGE_COMPLETE_EVENT,\n        handleAutoPlayTTSEvent\n      );\n      return () => {\n        window.removeEventListener(\n          ASSISTANT_MESSAGE_COMPLETE_EVENT,\n          handleAutoPlayTTSEvent\n        );\n      };\n    } else {\n      console.log(\"Assistant TTS auto-play is disabled\");\n    }\n  }, [autoPlayAssistantTtsResponse]);\n}\n"
  },
  {
    "path": "frontend/src/components/lib/CTAButton/index.jsx",
    "content": "export default function CTAButton({\n  children,\n  disabled = false,\n  onClick,\n  className = \"\",\n}) {\n  return (\n    <button\n      disabled={disabled}\n      onClick={() => onClick?.()}\n      className={`border-none text-xs px-4 py-1 font-semibold light:text-[#ffffff] rounded-lg bg-primary-button hover:bg-secondary hover:text-white h-[34px] -mr-8 whitespace-nowrap w-fit ${className}`}\n    >\n      <div className=\"flex items-center justify-center gap-2\">{children}</div>\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/lib/ModelTable/index.jsx",
    "content": "import { useRef, useState, useEffect } from \"react\";\nimport {\n  CaretDown,\n  CaretRight,\n  Cpu,\n  Circle,\n  DotsThreeVertical,\n  CloudArrowDown,\n  CircleNotch,\n} from \"@phosphor-icons/react\";\nimport pluralize from \"pluralize\";\nimport { titleCase } from \"text-case\";\nimport { humanFileSize } from \"@/utils/numbers\";\nimport MonoProviderIcon from \"../MonoProviderIcon\";\n\n/**\n * @typedef {Object} ModelDefinition\n * @property {string} id - The ID of the model.\n * @property {'CPU' | 'GPU' | 'NPU'} deviceType - The device type of the model.\n * @property {number} modelSize - The size of the model in megabytes.\n * @property {boolean} downloaded - Whether the model is downloaded.\n */\n\n/**\n * @param {object} props - The props of the component.\n * @param {string} props.alias - The alias of the model.\n * @param {Array<ModelDefinition>} props.models - The models to display.\n * @param {(model: string, progressCallback: (percentage: number) => void) => void} props.downloadModel - The function to download the model.\n * @param {(model: string) => void} props.uninstallModel - The function to uninstall the model.\n * @param {(model: string) => void} props.setActiveModel - The function to set the active model.\n * @param {string} props.selectedModelId - The ID of the selected model.\n * @param {object} props.ui - The UI configuration.\n * @param {boolean} props.ui.showRuntime - Whether to show the runtime.\n * @returns {React.ReactNode}\n */\nexport default function ModelTable({\n  alias = \"\",\n  models = [],\n  downloadModel = null,\n  uninstallModel = null,\n  setActiveModel = () => {},\n  selectedModelId = \"\",\n  ui = {\n    showRuntime: true,\n  },\n}) {\n  const [showAll, setShowAll] = useState(\n    models.some((model) => model.downloaded)\n  );\n  const totalModels = models.length;\n\n  return (\n    <div className=\"flex flex-col w-full border-b border-theme-modal-border py-[18px]\">\n      <button\n        type=\"button\"\n        onClick={() => setShowAll(!showAll)}\n        className=\"border-none text-theme-text-secondary text-sm font-medium hover:underline flex items-center gap-x-[8px]\"\n      >\n        {showAll ? (\n          <CaretDown\n            size={16}\n            weight=\"bold\"\n            className=\"text-theme-text-secondary\"\n          />\n        ) : (\n          <CaretRight\n            size={16}\n            weight=\"bold\"\n            className=\"text-theme-text-secondary\"\n          />\n        )}\n        <div className=\"flex items-center gap-x-[4px]\">\n          <MonoProviderIcon\n            provider={alias}\n            match=\"pattern\"\n            size={16}\n            className=\"text-theme-text-primary\"\n          />\n          <p className=\"flex items-center gap-x-1 text-theme-text-primary text-base font-bold\">\n            {titleCase(alias)}\n            <span className=\"text-theme-text-secondary font-normal text-sm\">\n              ({totalModels} {pluralize(\"Model\", totalModels)})\n            </span>\n          </p>\n        </div>\n      </button>\n      <div hidden={!showAll} className=\"mt-[16px]\">\n        <div className=\"w-full flex flex-col gap-y-[8px]\">\n          {models.map((model) => (\n            <ModelRow\n              key={model.id}\n              alias={alias}\n              model={model}\n              downloadModel={downloadModel}\n              uninstallModel={uninstallModel}\n              setActiveModel={setActiveModel}\n              selectedModelId={selectedModelId}\n              ui={ui}\n            />\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction DeviceTypeTagWrapper({ text, bgClass, textClass }) {\n  return (\n    <div\n      className={\n        bgClass + \" px-1.5 py-1 rounded-full flex items-center gap-x-1 w-fit\"\n      }\n    >\n      <Cpu size={14} weight=\"bold\" className={textClass} />\n      <p className={textClass + \" text-xs\"}>{text}</p>\n    </div>\n  );\n}\n\n/**\n * @param {{deviceType: ModelDefinition[\"deviceType\"]}} deviceType\n * @returns {React.ReactNode}\n */\nfunction DeviceTypeTag({ deviceType }) {\n  switch (deviceType?.toLowerCase()) {\n    case \"cpu\":\n      return (\n        <DeviceTypeTagWrapper\n          text=\"CPU\"\n          bgClass=\"bg-zinc-800 light:bg-zinc-200\"\n          textClass=\"text-theme-text-primary\"\n        />\n      );\n    case \"gpu\":\n      return (\n        <DeviceTypeTagWrapper\n          text=\"GPU\"\n          bgClass=\"bg-green-800 light:bg-green-200\"\n          textClass=\"text-theme-text-primary\"\n        />\n      );\n    case \"npu\":\n      return (\n        <DeviceTypeTagWrapper\n          text=\"NPU\"\n          bgClass=\"bg-indigo-800 light:bg-indigo-200\"\n          textClass=\"text-theme-text-primary\"\n        />\n      );\n    default:\n      return (\n        <DeviceTypeTagWrapper\n          text=\"CPU\"\n          bgClass=\"bg-zinc-800 light:bg-zinc-200\"\n          textClass=\"text-theme-text-primary\"\n        />\n      );\n  }\n}\n\n/**\n * @param {object} props - The props of the component.\n * @param {ModelDefinition} props.model - The model to display.\n * @param {(model: string, progressCallback: (percentage: number) => void) => Promise<void>} props.downloadModel - The function to download the model.\n * @param {(model: string) => Promise<void>} props.uninstallModel - The function to uninstall the model.\n * @param {(model: string) => void} props.setActiveModel - The function to set the active model.\n * @param {string} props.selectedModelId - The ID of the selected model.\n * @param {object} props.ui - The UI configuration.\n * @param {boolean} props.ui.showRuntime - Whether to show the runtime.\n * @returns {React.ReactNode}\n */\nfunction ModelRow({\n  alias,\n  model,\n  downloadModel = null,\n  uninstallModel = null,\n  setActiveModel,\n  selectedModelId,\n  ui = {\n    showRuntime: true,\n  },\n}) {\n  const modelRowRef = useRef(null);\n  const [showOptions, setShowOptions] = useState(false);\n  const [processing, setProcessing] = useState(false);\n  const [downloadPercentage, setDownloadPercentage] = useState(0);\n  const fileSize =\n    typeof model.size === \"number\"\n      ? humanFileSize(model.size * 1e6, true, 2)\n      : (model.size ?? \"Unknown size\");\n  const [isActiveModel, setIsActiveModel] = useState(\n    selectedModelId === model.id\n  );\n\n  async function handleSetActiveModel() {\n    setDownloadPercentage(0);\n    if (model.downloaded) setActiveModel(model.id);\n    else {\n      try {\n        if (!downloadModel) return;\n        setProcessing(true);\n        await downloadModel(model.id, fileSize, (percentage) => {\n          setDownloadPercentage(percentage);\n        });\n      } catch {\n      } finally {\n        setProcessing(false);\n      }\n    }\n  }\n\n  async function handleUninstallModel() {\n    if (!uninstallModel) return;\n    try {\n      setProcessing(true);\n      await uninstallModel(model.id);\n    } catch {\n    } finally {\n      setProcessing(false);\n    }\n  }\n\n  useEffect(() => {\n    if (selectedModelId === model.id) {\n      setIsActiveModel(true);\n      modelRowRef.current.classList.add(\"!bg-gray-200/10\");\n      setTimeout(\n        () => modelRowRef.current.classList.remove(\"!bg-gray-200/10\"),\n        800\n      );\n    } else {\n      setIsActiveModel(false);\n    }\n  }, [selectedModelId]);\n\n  return (\n    <div\n      ref={modelRowRef}\n      className=\"w-full grid grid-cols-[1fr_auto_1fr] items-center gap-x-4 transition-all duration-300 rounded-lg\"\n    >\n      <button\n        type=\"button\"\n        className=\"border-none flex items-center gap-x-[8px] whitespace-nowrap py-[8px]\"\n        disabled={processing}\n        onClick={handleSetActiveModel}\n      >\n        {ui.showRuntime && <DeviceTypeTag deviceType={model.deviceType} />}\n        {!ui.showRuntime &&\n          model.downloaded &&\n          alias === \"Downloaded Models\" && (\n            <MonoProviderIcon\n              provider={model.organization}\n              match=\"pattern\"\n              size={16}\n              className=\"text-theme-text-primary\"\n            />\n          )}\n        <p className=\"text-theme-text-primary text-base\">{model.name}</p>\n        <p className=\"text-theme-text-secondary opacity-70 text-base\">\n          {fileSize}\n        </p>\n      </button>\n\n      <div className=\"justify-self-start\">\n        <RenderStatus model={model} isActiveModel={isActiveModel} />\n      </div>\n\n      <div className=\"relative justify-self-end\">\n        {uninstallModel && model.downloaded ? (\n          <>\n            <button\n              type=\"button\"\n              className=\"border-none hover:bg-white/20 rounded-lg p-1\"\n              onClick={() => setShowOptions(!showOptions)}\n            >\n              <DotsThreeVertical\n                size={22}\n                weight=\"bold\"\n                className=\"text-theme-text-primary cursor-pointer\"\n              />\n            </button>\n            {showOptions && (\n              <div className=\"absolute top-[20px] right-[20px] bg-theme-action-menu-bg border border-theme-modal-border rounded-lg py-2 px-4 shadow-lg\">\n                <button\n                  type=\"button\"\n                  className=\"border-none font-medium group\"\n                  onClick={handleUninstallModel}\n                >\n                  <p className=\"text-sm text-theme-text-primary group-hover:underline group-hover:text-theme-text-secondary\">\n                    Uninstall\n                  </p>\n                </button>\n              </div>\n            )}\n          </>\n        ) : null}\n        {!model.downloaded && !processing && (\n          <button\n            type=\"button\"\n            data-tooltip-id=\"install-model-tooltip\"\n            data-tooltip-place=\"top\"\n            data-tooltip-delay-show={300}\n            data-tooltip-content={`Install ${model.organization}:${model.name}`}\n            className=\"border-none hover:bg-white/20 light:hover:bg-black/5 rounded-lg p-2 flex items-center gap-x-1 cursor-pointer\"\n            onClick={handleSetActiveModel}\n          >\n            <CloudArrowDown\n              size={20}\n              weight=\"bold\"\n              className=\"text-theme-text-primary\"\n            />\n          </button>\n        )}\n        {!model.downloaded && processing && (\n          <div className=\"flex items-center justify-center gap-x-[10px] whitespace-nowrap\">\n            {!downloadPercentage && (\n              <CircleNotch\n                size={16}\n                weight=\"bold\"\n                className=\"text-theme-text-primary animate-spin\"\n              />\n            )}\n            <p className=\"text-theme-text-secondary text-sm\">\n              {downloadPercentage}%\n            </p>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction RenderStatus({ model, isActiveModel }) {\n  if (isActiveModel) {\n    return (\n      <div className=\"flex items-center justify-center gap-x-[10px] whitespace-nowrap\">\n        <Circle size={8} weight=\"fill\" className=\"text-green-500\" />\n        <p className=\"text-theme-text-primary text-sm\">Active</p>\n      </div>\n    );\n  }\n\n  if (!isActiveModel && model.downloaded) {\n    return (\n      <p className=\"text-theme-text-secondary text-sm italic whitespace-nowrap\">\n        Installed\n      </p>\n    );\n  }\n\n  if (!model.downloaded) {\n    return (\n      <p className=\"text-theme-text-secondary text-sm italic whitespace-nowrap\">\n        Not Installed\n      </p>\n    );\n  }\n  return null;\n}\n"
  },
  {
    "path": "frontend/src/components/lib/ModelTable/layout.jsx",
    "content": "import { useState } from \"react\";\nimport {\n  ArrowClockwise,\n  CircleNotch,\n  MagnifyingGlass,\n} from \"@phosphor-icons/react\";\n\nexport default function ModelTableLayout({\n  children,\n  fetchModels = null,\n  searchQuery = \"\",\n  setSearchQuery = () => {},\n  loading = false,\n}) {\n  const [isRefreshing, setIsRefreshing] = useState(false);\n  async function refreshModels() {\n    setIsRefreshing(true);\n    try {\n      await fetchModels?.();\n    } catch {\n    } finally {\n      setIsRefreshing(false);\n    }\n  }\n\n  return (\n    <div className=\"flex flex-col w-full\">\n      <div className=\"flex gap-x-2 items-center pb-[8px]\">\n        <label className=\"text-theme-text-primary text-base font-semibold\">\n          Available Models\n        </label>\n      </div>\n      <div className=\"flex w-full items-center gap-x-[16px]\">\n        <div className=\"relative flex-1 flex-grow\">\n          <MagnifyingGlass\n            size={16}\n            weight=\"bold\"\n            color=\"var(--theme-text-primary)\"\n            className=\"absolute left-[9px] top-[10px] text-theme-settings-input-placeholder peer-focus:invisible\"\n          />\n          <input\n            type=\"search\"\n            placeholder=\"Search models\"\n            value={searchQuery}\n            disabled={loading}\n            className=\"min-h-[32px] border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 pl-[30px] py-2 search-input disabled:opacity-50 disabled:cursor-not-allowed\"\n            onChange={(e) => {\n              e.preventDefault();\n              e.stopPropagation();\n              setSearchQuery(e.target.value);\n            }}\n          />\n        </div>\n        {!!fetchModels && (\n          <button\n            type=\"button\"\n            onClick={refreshModels}\n            disabled={isRefreshing || loading}\n            className=\"border-none text-theme-text-secondary text-sm font-medium hover:bg-white/10 light:hover:bg-black/5 rounded-lg px-2 h-full flex items-center gap-x-1 disabled:opacity-50 disabled:cursor-not-allowed\"\n          >\n            {isRefreshing ? (\n              <CircleNotch className=\"w-4 h-4 text-theme-text-secondary animate-spin\" />\n            ) : (\n              <ArrowClockwise\n                weight=\"bold\"\n                className=\"w-4 h-4 text-theme-text-secondary\"\n              />\n            )}\n            <span\n              className={`text-sm font-medium ${isRefreshing ? \"hidden\" : \"text-theme-text-secondary\"}`}\n            >\n              Refresh Models\n            </span>\n          </button>\n        )}\n      </div>\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/lib/ModelTable/loading.jsx",
    "content": "import * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\n\nexport default function ModelTableLoadingSkeleton() {\n  return (\n    <div className=\"flex flex-col w-full gap-y-4 pt-4\">\n      <Skeleton.default\n        height={100}\n        width=\"100%\"\n        count={7}\n        highlightColor=\"var(--theme-settings-input-active)\"\n        baseColor=\"var(--theme-settings-input-bg)\"\n        enableAnimation={true}\n        containerClassName=\"w-fill flex gap-[8px] flex-col p-0\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/lib/MonoProviderIcon/index.jsx",
    "content": "/* eslint-disable react-hooks/static-components */\n// https://lobehub.com/icons for all the icons\nimport OpenAI from \"@lobehub/icons/es/OpenAI/components/Mono\";\nimport Anthropic from \"@lobehub/icons/es/Anthropic/components/Mono\";\nimport Google from \"@lobehub/icons/es/Google/components/Mono\";\nimport Gemma from \"@lobehub/icons/es/Gemma/components/Mono\";\nimport Gemini from \"@lobehub/icons/es/Gemini/components/Mono\";\nimport Microsoft from \"@lobehub/icons/es/Microsoft/components/Mono\";\nimport Meta from \"@lobehub/icons/es/Meta/components/Mono\";\nimport Mistral from \"@lobehub/icons/es/Mistral/components/Mono\";\nimport Azure from \"@lobehub/icons/es/Azure/components/Mono\";\nimport AzureAI from \"@lobehub/icons/es/AzureAI/components/Mono\";\nimport DeepSeek from \"@lobehub/icons/es/DeepSeek/components/Mono\";\nimport HuggingFace from \"@lobehub/icons/es/HuggingFace/components/Mono\";\nimport Qwen from \"@lobehub/icons/es/Qwen/components/Mono\";\nimport IBM from \"@lobehub/icons/es/IBM/components/Mono\";\nimport Bytedance from \"@lobehub/icons/es/ByteDance/components/Mono\";\nimport Kimi from \"@lobehub/icons/es/Kimi/components/Mono\";\nimport Snowflake from \"@lobehub/icons/es/Snowflake/components/Mono\";\nimport Liquid from \"@lobehub/icons/es/Liquid/components/Mono\";\n\n// Direct provider key -> icon mapping for exact matches\nconst providerIcons = {\n  openai: OpenAI,\n  anthropic: Anthropic,\n  google: Google,\n  microsoft: Microsoft,\n  gemma: Gemma,\n  gemini: Gemini,\n  meta: Meta,\n  mistral: Mistral,\n  azure: Azure,\n  azureai: AzureAI,\n  deepseek: DeepSeek,\n  huggingface: HuggingFace,\n  qwen: Qwen,\n  qwq: Qwen,\n  ibm: IBM,\n  bytedance: Bytedance,\n  kimi: Kimi,\n  liquid: Liquid,\n};\n\n// Pattern matching rules: regex pattern -> icon component\n// These are checked in order, first match wins\nconst modelPatterns = [\n  { pattern: /^gpt/i, icon: OpenAI },\n  { pattern: /^o\\d+/i, icon: OpenAI }, // o1, o3, etc.\n  { pattern: /^claude-/i, icon: Anthropic },\n  { pattern: /^gemini-/i, icon: Gemini },\n  { pattern: /gemma/i, icon: Gemma },\n  { pattern: /llama/i, icon: Meta },\n  { pattern: /^meta/i, icon: Meta },\n  {\n    pattern: /^(mistral|devstral|mixtral|magistral|codestral|ministral)/i,\n    icon: Mistral,\n  },\n  { pattern: /^deepseek/i, icon: DeepSeek },\n  { pattern: /^qwen/i, icon: Qwen },\n  { pattern: /^qwq/i, icon: Qwen },\n  { pattern: /^phi/i, icon: Microsoft },\n  { pattern: /^granite/i, icon: IBM },\n  { pattern: /^doubao/i, icon: Bytedance },\n  { pattern: /^moonshot/i, icon: Kimi },\n  { pattern: /^smol/i, icon: HuggingFace },\n  { pattern: /^seed/i, icon: Bytedance },\n  { pattern: /^kimi/i, icon: Kimi },\n  { pattern: /^snowflake/i, icon: Snowflake },\n  { pattern: /^lfm/i, icon: Liquid },\n];\n\n/**\n * Find icon by matching model name against known patterns\n * @param {string} modelName - The model name to match\n * @returns {React.ComponentType|null}\n */\nfunction findIconByModelName(modelName) {\n  if (!modelName) return null;\n  const match = modelPatterns.find(({ pattern }) => pattern.test(modelName));\n  return match?.icon || null;\n}\n\n/**\n * @param {object} props - The props of the component.\n * @param {string} props.provider - The provider key (for exact match) or model name (for pattern match).\n * @param {('exact'|'pattern')} props.match - Match mode: 'exact' for provider key, 'pattern' for model name matching.\n * @param {number} props.size - The size of the icon.\n * @param {string} props.className - The class name of the icon.\n * @param {string} props.fallbackIconKey - The key of the fallback icon to use if no icon is found.\n * @returns {React.ReactNode}\n */\nexport default function MonoProviderIcon({\n  provider,\n  match = \"exact\",\n  size = 24,\n  className = \"\",\n  fallbackIconKey = null,\n}) {\n  let Icon = null;\n\n  if (match === \"exact\") Icon = providerIcons[provider?.toLowerCase()];\n  else if (match === \"pattern\") Icon = findIconByModelName(provider);\n  if (!Icon && fallbackIconKey && providerIcons[fallbackIconKey])\n    Icon = providerIcons[fallbackIconKey];\n  if (!Icon) return null;\n  return <Icon size={size} className={className} />;\n}\n"
  },
  {
    "path": "frontend/src/components/lib/QuickActions/index.jsx",
    "content": "import { useTranslation } from \"react-i18next\";\nimport useUser from \"@/hooks/useUser\";\n\n/**\n * Quick action buttons for home and empty workspace states.\n * @param {Object} props\n * @param {boolean} props.hasAvailableWorkspace - Whether the user has a workspace they can use\n * @param {Function} props.onCreateAgent - Handler for \"Create an Agent\" action\n * @param {Function} props.onEditWorkspace - Handler for \"Edit Workspace\" action\n * @param {Function} props.onUploadDocument - Handler for \"Upload a Document\" action\n */\nexport default function QuickActions({\n  hasAvailableWorkspace,\n  onCreateAgent,\n  onEditWorkspace,\n  onUploadDocument,\n}) {\n  const { t } = useTranslation();\n  const { user } = useUser();\n\n  return (\n    <div className=\"flex flex-wrap justify-center gap-2 mt-6\">\n      <QuickActionButton\n        label={t(\"main-page.quickActions.createAgent\")}\n        onClick={onCreateAgent}\n        show={!user || [\"admin\"].includes(user?.role)}\n      />\n      <QuickActionButton\n        label={t(\"main-page.quickActions.editWorkspace\")}\n        onClick={onEditWorkspace}\n        show={\n          hasAvailableWorkspace &&\n          (!user || [\"admin\", \"manager\"].includes(user?.role))\n        }\n      />\n      <QuickActionButton\n        label={t(\"main-page.quickActions.uploadDocument\")}\n        onClick={onUploadDocument}\n        // Any user can upload documents.\n        show={true}\n      />\n    </div>\n  );\n}\n\nfunction QuickActionButton({ label, onClick, show = true }) {\n  if (!show) return null;\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      className=\"px-4 py-2 rounded-full bg-theme-bg-chat-input text-white/80 text-sm font-normal leading-5 hover:bg-zinc-700 light:hover:bg-black/20 transition-colors light:text-theme-text-primary\"\n    >\n      {label}\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/lib/SuggestedMessages/index.jsx",
    "content": "export default function SuggestedMessages({\n  suggestedMessages = [],\n  sendCommand,\n}) {\n  if (!suggestedMessages?.length) return null;\n\n  return (\n    <div className=\"flex flex-col w-full max-w-[650px] mt-6 px-4\">\n      {suggestedMessages.map((msg, index) => {\n        const text = msg.heading?.trim()\n          ? `${msg.heading.trim()} ${msg.message?.trim() || \"\"}`\n          : msg.message?.trim() || \"\";\n        if (!text) return null;\n\n        return (\n          <div key={index}>\n            {index > 0 && (\n              <div className=\"border-t border-zinc-800 light:border-theme-chat-input-border\" />\n            )}\n            <button\n              type=\"button\"\n              onClick={() => sendCommand({ text, autoSubmit: true })}\n              className=\"w-full text-left py-3 px-3 text-white/80 text-sm font-normal leading-5 hover:text-white transition-colors light:text-theme-text-primary light:hover:text-theme-text-primary/80 hover:bg-zinc-800 light:hover:bg-black/20 rounded-lg\"\n            >\n              {text}\n            </button>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/components/lib/Toggle/index.jsx",
    "content": "import { Info } from \"@phosphor-icons/react\";\n\nconst TOGGLE_STYLES = {\n  sm: \"h-[12px] w-[20px] after:h-[8px] after:w-[8px] after:top-[2px] after:left-[2px] peer-checked:after:translate-x-full\",\n  md: \"h-[16px] w-[28px] after:h-[12px] after:w-[12px] after:top-[2px] after:left-[2px] peer-checked:after:translate-x-full\",\n  lg: \"h-[19px] w-[36px] after:h-[15px] after:w-[15px] after:top-[2px] after:left-[2px] peer-checked:after:translate-x-[17px]\",\n};\n\nconst LABEL_STYLES = {\n  sm: {\n    label: \"text-[12px] leading-[10px] font-medium mt-[1.5px]\",\n    description: \"text-[10px] leading-[16px] font-normal\",\n    gap: \"gap-[2px]\",\n  },\n  md: {\n    label: \"text-[14px] leading-[18px] font-medium -mt-[2px]\",\n    description: \"text-[12px] leading-[16px] font-normal\",\n    gap: \"gap-[2px]\",\n  },\n  lg: {\n    label: \"text-[16px] leading-[14px] font-medium mt-[2.5px]\",\n    description: \"text-[14px] leading-[24px] font-normal\",\n    gap: \"gap-[2px]\",\n  },\n};\n\n/**\n * @param {Object} props - Component props\n * @param {string} [props.className] - Additional CSS classes\n * @param {boolean} [props.enabled] - Controlled checked state\n * @param {(checked: boolean) => void} [props.onChange] - Change handler receiving new checked state\n * @param {boolean} [props.disabled=false] - Whether toggle is disabled\n * @param {\"sm\" | \"md\" | \"lg\"} [props.size=\"sm\"] - Toggle size\n * @param {string} [props.name] - Input name for form submission\n * @param {string} [props.label] - Label text next to toggle\n * @param {string} [props.description] - Description text below label\n * @param {\"default\" | \"horizontal\"} [props.variant=\"default\"] - Layout variant\n * @param {string} [props.hint] - Tooltip ID for info icon hint next to label\n * @param {string} [props.value] - Input value for form submission\n */\nexport default function Toggle({\n  className,\n  enabled,\n  onChange,\n  disabled = false,\n  size = \"sm\",\n  name,\n  label,\n  description,\n  variant = \"default\",\n  hint,\n  value,\n}) {\n  const inputProps =\n    enabled !== undefined\n      ? { checked: enabled, onChange: (e) => onChange?.(e.target.checked) }\n      : { defaultChecked: false };\n\n  const labelStyles = LABEL_STYLES[size] || LABEL_STYLES.sm;\n\n  if (variant === \"horizontal\") {\n    return (\n      <label\n        className={`flex items-start justify-between max-w-[700px] ${disabled ? \"cursor-not-allowed opacity-50\" : \"cursor-pointer\"} ${className ?? \"\"}`}\n      >\n        <TextContent\n          label={label}\n          description={description}\n          labelStyles={labelStyles}\n          hint={hint}\n        />\n        <div className=\"shrink-0 ml-4\">\n          <ToggleSwitch\n            name={name}\n            disabled={disabled}\n            size={size}\n            inputProps={inputProps}\n            value={value}\n          />\n        </div>\n      </label>\n    );\n  }\n\n  return (\n    <label\n      className={`inline-flex items-start ${disabled ? \"cursor-not-allowed opacity-50\" : \"cursor-pointer\"} ${className ?? \"\"}`}\n    >\n      <ToggleSwitch\n        name={name}\n        disabled={disabled}\n        size={size}\n        inputProps={inputProps}\n        value={value}\n      />\n      {(label || description) && (\n        <div className=\"ml-3\">\n          <TextContent\n            label={label}\n            description={description}\n            labelStyles={labelStyles}\n            hint={hint}\n          />\n        </div>\n      )}\n    </label>\n  );\n}\n\nfunction ToggleSwitch({ name, disabled, size, inputProps, value }) {\n  return (\n    <>\n      <input\n        type=\"checkbox\"\n        name={name}\n        disabled={disabled}\n        className=\"peer sr-only\"\n        value={value}\n        {...inputProps}\n      />\n      <div\n        className={`\n          relative shrink-0 peer pointer-events-none rounded-full\n          ${TOGGLE_STYLES[size] || TOGGLE_STYLES.sm}\n          after:absolute after:rounded-full after:bg-white\n          after:transition-all after:content-['']\n          peer-focus:ring-2\n          bg-zinc-500 light:bg-zinc-300 peer-focus:ring-zinc-700 light:peer-focus:bg-green-100 light:peer-focus:ring-green-200\n          peer-checked:bg-green-400 peer-checked:peer-focus:bg-green-300 peer-checked:peer-focus:ring-green-900 light:peer-checked:peer-focus:bg-green-300 light:peer-checked:peer-focus:ring-green-200\n        `}\n      />\n    </>\n  );\n}\n\nfunction TextContent({ label, description, labelStyles = {}, hint }) {\n  if (!label && !description) return null;\n  return (\n    <div className={`flex flex-col ${labelStyles.gap}`}>\n      {label && (\n        <span\n          className={`flex items-center gap-x-1 text-white light:text-slate-950 ${labelStyles.label}`}\n        >\n          {label}\n          {hint && (\n            <Info\n              size={14}\n              className=\"text-theme-text-secondary cursor-pointer\"\n              data-tooltip-id={hint}\n            />\n          )}\n        </span>\n      )}\n      {description && (\n        <span\n          className={`text-zinc-400 light:text-zinc-600 ${labelStyles.description}`}\n        >\n          {description}\n        </span>\n      )}\n    </div>\n  );\n}\n\n/**\n * Simple toggle switch that doesn't use label/input to avoid focus-scroll issues\n */\nexport function SimpleToggleSwitch({\n  className,\n  enabled,\n  onChange,\n  disabled = false,\n  size = \"sm\",\n}) {\n  return (\n    <div\n      role=\"switch\"\n      aria-checked={enabled}\n      tabIndex={0}\n      onClick={(e) => {\n        e.stopPropagation();\n        onChange(!enabled);\n      }}\n      onKeyDown={(e) => {\n        if (e.key === \"Enter\" || e.key === \" \") {\n          e.preventDefault();\n          e.stopPropagation();\n          onChange(!enabled);\n        }\n      }}\n      className={`\n        relative shrink-0 cursor-pointer rounded-full ${disabled ? \"cursor-not-allowed opacity-50\" : \"cursor-pointer\"}\n        ${size === \"sm\" ? \"h-[12px] w-[20px]\" : size === \"md\" ? \"h-[16px] w-[28px]\" : \"h-[19px] w-[36px]\"}\n        transition-colors duration-200\n        ${enabled ? \"bg-green-400\" : \"bg-zinc-500\"}\n        ${className}\n      `}\n    >\n      <div\n        className={`\n          absolute top-[2px] left-[2px]\n          ${size === \"sm\" ? \"h-[8px] w-[8px]\" : size === \"md\" ? \"h-[12px] w-[12px]\" : \"h-[15px] w-[15px]\"}\n          rounded-full bg-white\n          transition-transform duration-200\n          ${enabled ? \"translate-x-full\" : \"translate-x-0\"}\n        `}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/hooks/useAppVersion.js",
    "content": "import { useEffect, useState } from \"react\";\nimport System from \"../models/system\";\n\n/**\n * Hook to fetch the app version.\n * @returns {Object} The app version.\n * @returns {string | null} version - The app version.\n * @returns {boolean} isLoading - Whether the app version is loading.\n */\nexport default function useAppVersion() {\n  const [version, setVersion] = useState(null);\n  const [isLoading, setIsLoading] = useState(true);\n\n  useEffect(() => {\n    System.fetchAppVersion()\n      .then(setVersion)\n      .finally(() => setIsLoading(false));\n  }, []);\n  return { version, isLoading };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useChatContainerQuickScroll.js",
    "content": "import { useEffect, useRef } from \"react\";\nimport { isMac } from \"@/utils/keyboardShortcuts\";\n\n/**\n * Hook for scrolling the chat container using keyboard shortcuts.\n * @returns {Object} - An object containing the chat history ref and the scroll to top and bottom functions.\n */\nexport default function useChatContainerQuickScroll() {\n  const chatHistoryRef = useRef(null);\n\n  const scrollToTop = (event) => {\n    event.preventDefault();\n    chatHistoryRef.current.scrollToTop();\n  };\n\n  const scrollToBottom = (event) => {\n    event.preventDefault();\n    chatHistoryRef.current.scrollToBottom();\n  };\n\n  useEffect(() => {\n    function handleScrollShortcuts(event) {\n      const modifierPressed = isMac ? event.metaKey : event.ctrlKey;\n      if (!modifierPressed || !chatHistoryRef.current) return;\n      if (event.key !== \"ArrowUp\" && event.key !== \"ArrowDown\") return;\n\n      // Don't hijack cursor movement when a text input is focused\n      const tag = document.activeElement?.tagName;\n      if (tag === \"TEXTAREA\" || tag === \"INPUT\") return;\n\n      switch (event.key) {\n        case \"ArrowUp\":\n          event.preventDefault();\n          scrollToTop(event);\n          break;\n        case \"ArrowDown\":\n          event.preventDefault();\n          scrollToBottom(event);\n          break;\n        default:\n          break;\n      }\n    }\n\n    window.addEventListener(\"keydown\", handleScrollShortcuts);\n    return () => window.removeEventListener(\"keydown\", handleScrollShortcuts);\n  }, []);\n\n  return { chatHistoryRef };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useChatHistoryScrollHandle.js",
    "content": "import { useImperativeHandle } from \"react\";\n\n/**\n * Exposes scroll control methods (scrollToTop, scrollToBottom) via a forwarded ref.\n * This allows parent components to programmatically scroll the chat history.\n *\n * @param {React.Ref} ref - The forwarded ref from the parent component\n * @param {React.RefObject} chatHistoryRef - Ref to the scrollable chat history DOM element\n * @param {Object} options - Configuration options\n * @param {Function} options.setIsUserScrolling - Setter to mark user-initiated scrolling\n * @param {boolean} options.isStreaming - Whether chat is currently streaming a response\n * @param {Function} options.scrollToBottom - Internal scroll to bottom function\n */\nexport default function useChatHistoryScrollHandle(\n  ref,\n  chatHistoryRef,\n  { setIsUserScrolling, isStreaming, scrollToBottom }\n) {\n  useImperativeHandle(\n    ref,\n    () => ({\n      scrollToTop() {\n        if (chatHistoryRef.current) {\n          setIsUserScrolling(true);\n          chatHistoryRef.current.scrollTo({\n            top: 0,\n            behavior: \"smooth\",\n          });\n        }\n      },\n      scrollToBottom() {\n        setIsUserScrolling(true);\n        scrollToBottom(isStreaming ? false : true);\n      },\n    }),\n    [isStreaming]\n  );\n}\n"
  },
  {
    "path": "frontend/src/hooks/useCommunityHubAuth.js",
    "content": "import { useState, useEffect } from \"react\";\nimport CommunityHub from \"@/models/communityHub\";\n\n/**\n * Hook to check if the user is authenticated with the community hub by checking\n * the user defined connection key in the settings.\n * @returns {{isAuthenticated: boolean, loading: boolean}} An object containing the authentication status and loading state.\n */\nexport function useCommunityHubAuth() {\n  const [isAuthenticated, setIsAuthenticated] = useState(false);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function checkCommunityHubAuth() {\n      setLoading(true);\n      try {\n        const { connectionKey } = await CommunityHub.getSettings();\n        setIsAuthenticated(!!connectionKey);\n      } catch (error) {\n        console.error(\"Error checking hub auth:\", error);\n        setIsAuthenticated(false);\n      } finally {\n        setLoading(false);\n      }\n    }\n    checkCommunityHubAuth();\n  }, []);\n\n  return { isAuthenticated, loading };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useCopyText.js",
    "content": "import { THOUGHT_REGEX_COMPLETE } from \"@/components/WorkspaceChat/ChatContainer/ChatHistory/ThoughtContainer\";\nimport { useState } from \"react\";\n\nexport default function useCopyText(delay = 2500) {\n  const [copied, setCopied] = useState(false);\n  const copyText = async (content) => {\n    if (!content) return;\n\n    // Filter thinking blocks from the content if they exist\n    const nonThinkingContent = content.replace(THOUGHT_REGEX_COMPLETE, \"\");\n    navigator?.clipboard?.writeText(nonThinkingContent);\n    setCopied(nonThinkingContent);\n    setTimeout(() => {\n      setCopied(false);\n    }, delay);\n  };\n\n  return { copyText, copied };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useGetProvidersModels.js",
    "content": "import System from \"@/models/system\";\nimport { useEffect, useState } from \"react\";\n\n// Providers which cannot use this feature for workspace<>model selection\nexport const DISABLED_PROVIDERS = [\n  \"azure\",\n  \"textgenwebui\",\n  \"generic-openai\",\n  \"bedrock\",\n];\nconst PROVIDER_DEFAULT_MODELS = {\n  openai: [],\n  gemini: [],\n  anthropic: [],\n  azure: [],\n  lmstudio: [],\n  localai: [],\n  ollama: [],\n  togetherai: [],\n  fireworksai: [],\n  \"nvidia-nim\": [],\n  groq: [],\n  cohere: [\n    \"command-r\",\n    \"command-r-plus\",\n    \"command\",\n    \"command-light\",\n    \"command-nightly\",\n    \"command-light-nightly\",\n  ],\n  textgenwebui: [],\n  \"generic-openai\": [],\n  bedrock: [],\n  xai: [\"grok-beta\"],\n};\n\n// For providers with large model lists (e.g. togetherAi) - we subgroup the options\n// by their creator organization (eg: Meta, Mistral, etc)\n// which makes selection easier to read.\nfunction groupModels(models) {\n  return models.reduce((acc, model) => {\n    acc[model.organization] = acc[model.organization] || [];\n    acc[model.organization].push(model);\n    return acc;\n  }, {});\n}\n\nconst groupedProviders = [\n  \"togetherai\",\n  \"fireworksai\",\n  \"openai\",\n  \"novita\",\n  \"openrouter\",\n  \"ppio\",\n  \"docker-model-runner\",\n  \"sambanova\",\n];\nexport default function useGetProviderModels(provider = null) {\n  const [defaultModels, setDefaultModels] = useState([]);\n  const [customModels, setCustomModels] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function fetchProviderModels() {\n      if (!provider) return;\n      setLoading(true);\n      const { models = [] } = await System.customModels(provider);\n      if (\n        PROVIDER_DEFAULT_MODELS.hasOwnProperty(provider) &&\n        !groupedProviders.includes(provider)\n      ) {\n        setDefaultModels(PROVIDER_DEFAULT_MODELS[provider]);\n      } else {\n        setDefaultModels([]);\n      }\n\n      groupedProviders.includes(provider)\n        ? setCustomModels(groupModels(models))\n        : setCustomModels(models);\n      setLoading(false);\n    }\n    fetchProviderModels();\n  }, [provider]);\n\n  return { defaultModels, customModels, loading };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useLanguageOptions.js",
    "content": "import i18n from \"@/i18n\";\nimport { resources as languages } from \"@/locales/resources\";\n\nexport function useLanguageOptions() {\n  const supportedLanguages = Object.keys(languages);\n  const languageNames = new Intl.DisplayNames(supportedLanguages, {\n    type: \"language\",\n  });\n  const changeLanguage = (newLang = \"en\") => {\n    if (!Object.keys(languages).includes(newLang)) return false;\n    i18n.changeLanguage(newLang);\n  };\n\n  return {\n    currentLanguage: i18n.language || \"en\",\n    supportedLanguages,\n    getLanguageName: (lang = \"en\") => languageNames.of(lang),\n    changeLanguage,\n  };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useLoginMode.js",
    "content": "import { useEffect, useState } from \"react\";\nimport { AUTH_TOKEN, AUTH_USER } from \"@/utils/constants\";\n\nexport default function useLoginMode() {\n  const [mode, setMode] = useState(null);\n\n  useEffect(() => {\n    if (!window) return;\n    const user = !!window.localStorage.getItem(AUTH_USER);\n    const token = !!window.localStorage.getItem(AUTH_TOKEN);\n    let _mode = null;\n    if (user && token) _mode = \"multi\";\n    if (!user && token) _mode = \"single\";\n    setMode(_mode);\n  }, [window]);\n\n  return mode;\n}\n"
  },
  {
    "path": "frontend/src/hooks/useLogo.js",
    "content": "import { useContext } from \"react\";\nimport { LogoContext } from \"../LogoContext\";\n\nexport default function useLogo() {\n  const { logo, setLogo, loginLogo, isCustomLogo } = useContext(LogoContext);\n  return { logo, setLogo, loginLogo, isCustomLogo };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useModal.js",
    "content": "import { useState } from \"react\";\n\nexport function useModal() {\n  const [isOpen, setIsOpen] = useState(false);\n\n  const openModal = () => setIsOpen(true);\n  const closeModal = () => setIsOpen(false);\n\n  return { isOpen, openModal, closeModal };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useOnboardingComplete.js",
    "content": "import { useEffect } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\nimport System from \"@/models/system\";\nimport paths from \"@/utils/paths\";\n\nexport default function useRedirectToHomeOnOnboardingComplete() {\n  const navigate = useNavigate();\n  useEffect(() => {\n    async function checkOnboardingComplete() {\n      const onboardingComplete = await System.isOnboardingComplete();\n      if (onboardingComplete === false) return;\n      navigate(paths.home());\n    }\n    checkOnboardingComplete();\n  }, []);\n}\n"
  },
  {
    "path": "frontend/src/hooks/usePfp.js",
    "content": "import { useContext } from \"react\";\nimport { PfpContext } from \"../PfpContext\";\n\nexport default function usePfp() {\n  const { pfp, setPfp } = useContext(PfpContext);\n  return { pfp, setPfp };\n}\n"
  },
  {
    "path": "frontend/src/hooks/usePrefersDarkMode.js",
    "content": "export default function usePrefersDarkMode() {\n  if (window?.matchMedia) {\n    if (window?.matchMedia(\"(prefers-color-scheme: dark)\")?.matches) {\n      return true;\n    }\n    return false;\n  }\n  return false;\n}\n"
  },
  {
    "path": "frontend/src/hooks/usePromptInputStorage.js",
    "content": "import { USER_PROMPT_INPUT_MAP } from \"@/utils/constants\";\nimport { useEffect, useMemo } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport debounce from \"lodash.debounce\";\nimport { safeJsonParse } from \"@/utils/request\";\n\n/**\n * Synchronizes prompt input value with localStorage, scoped to the current thread.\n *\n * Persists unsent prompt text across page refreshes and navigation. Each thread/workspace maintains\n * its own draft state independently. Storage key is determined by thread slug (if in a thread) or\n * workspace slug (if in default chat).\n *\n * Storage format (stored under USER_PROMPT_INPUT_MAP key):\n * ```json\n * {\n *   \"thread-slug\": \"user's draft message...\",\n *   \"workspace-slug\": \"another draft message...\"\n * }\n * ```\n *\n * @param {Object} props\n * @param {string} props.promptInput - Current prompt input value to sync\n * @param {Function} props.setPromptInput - State setter function for prompt input\n * @returns {void}\n */\n/**\n * Immediately clears the stored draft for a given thread/workspace key.\n * Used before state updates that may remount PromptInput to prevent\n * stale text from being restored.\n * @param {string} storageKey - thread slug or workspace slug\n */\nexport function clearPromptInputDraft(storageKey) {\n  try {\n    const map = safeJsonParse(localStorage.getItem(USER_PROMPT_INPUT_MAP), {});\n    map[storageKey] = \"\";\n    localStorage.setItem(USER_PROMPT_INPUT_MAP, JSON.stringify(map));\n  } catch {}\n}\n\nexport default function usePromptInputStorage({ promptInput, setPromptInput }) {\n  const { threadSlug = null, slug: workspaceSlug } = useParams();\n  useEffect(() => {\n    const serializedPromptInputMap =\n      localStorage.getItem(USER_PROMPT_INPUT_MAP) || \"{}\";\n\n    const promptInputMap = safeJsonParse(serializedPromptInputMap, {});\n\n    const userPromptInputValue = promptInputMap[threadSlug ?? workspaceSlug];\n    if (userPromptInputValue) {\n      setPromptInput(userPromptInputValue);\n    }\n  }, []);\n\n  const debouncedWriteToStorage = useMemo(\n    () =>\n      debounce((value, slug) => {\n        const serializedPromptInputMap =\n          localStorage.getItem(USER_PROMPT_INPUT_MAP) || \"{}\";\n        const promptInputMap = safeJsonParse(serializedPromptInputMap, {});\n        promptInputMap[slug] = value;\n        localStorage.setItem(\n          USER_PROMPT_INPUT_MAP,\n          JSON.stringify(promptInputMap)\n        );\n      }, 500),\n    []\n  );\n\n  useEffect(() => {\n    debouncedWriteToStorage(promptInput, threadSlug ?? workspaceSlug);\n\n    return () => {\n      debouncedWriteToStorage.cancel();\n    };\n  }, [promptInput, threadSlug, workspaceSlug, debouncedWriteToStorage]);\n}\n"
  },
  {
    "path": "frontend/src/hooks/useProviderEndpointAutoDiscovery.js",
    "content": "import { useEffect, useState } from \"react\";\nimport System from \"@/models/system\";\n\nexport default function useProviderEndpointAutoDiscovery({\n  provider = null,\n  initialBasePath = \"\",\n  initialAuthToken = null,\n  ENDPOINTS = [],\n}) {\n  const [loading, setLoading] = useState(false);\n  const [basePath, setBasePath] = useState(initialBasePath);\n  const [basePathValue, setBasePathValue] = useState(initialBasePath);\n\n  const [authToken, setAuthToken] = useState(initialAuthToken);\n  const [authTokenValue, setAuthTokenValue] = useState(initialAuthToken);\n  const [autoDetectAttempted, setAutoDetectAttempted] = useState(false);\n  const [showAdvancedControls, setShowAdvancedControls] = useState(true);\n\n  async function autoDetect() {\n    setLoading(true);\n    setAutoDetectAttempted(true);\n    const possibleEndpoints = [];\n    ENDPOINTS.forEach((endpoint) => {\n      possibleEndpoints.push(\n        new Promise((resolve, reject) => {\n          System.customModels(provider, authTokenValue, endpoint, 2_000)\n            .then((results) => {\n              if (!results?.models || results.models.length === 0)\n                throw new Error(\"No models\");\n              resolve({ endpoint, models: results.models });\n            })\n            .catch(() => {\n              reject(`${provider} @ ${endpoint} did not resolve.`);\n            });\n        })\n      );\n    });\n\n    const { endpoint, models } = await Promise.any(possibleEndpoints)\n      .then((resolved) => resolved)\n      .catch(() => {\n        console.error(\"All endpoints failed to resolve.\");\n        return { endpoint: null, models: null };\n      });\n\n    if (models !== null) {\n      setBasePath(endpoint);\n      setBasePathValue(endpoint);\n      setLoading(false);\n      setShowAdvancedControls(false);\n      return;\n    }\n\n    setLoading(false);\n    setShowAdvancedControls(true);\n  }\n\n  function handleAutoDetectClick(e) {\n    e.preventDefault();\n    autoDetect();\n  }\n\n  function handleBasePathChange(e) {\n    const value = e.target.value;\n    setBasePathValue(value);\n  }\n\n  function handleBasePathBlur() {\n    setBasePath(basePathValue);\n  }\n\n  function handleAuthTokenChange(e) {\n    const value = e.target.value;\n    setAuthTokenValue(value);\n  }\n\n  function handleAuthTokenBlur() {\n    setAuthToken(authTokenValue);\n  }\n\n  useEffect(() => {\n    if (!initialBasePath && !autoDetectAttempted) autoDetect(true);\n  }, [initialBasePath, initialAuthToken, autoDetectAttempted]);\n\n  return {\n    autoDetecting: loading,\n    autoDetectAttempted,\n    showAdvancedControls,\n    setShowAdvancedControls,\n    basePath: {\n      value: basePath,\n      set: setBasePathValue,\n      onChange: handleBasePathChange,\n      onBlur: handleBasePathBlur,\n    },\n    basePathValue: {\n      value: basePathValue,\n      set: setBasePathValue,\n    },\n    authToken: {\n      value: authToken,\n      set: setAuthTokenValue,\n      onChange: handleAuthTokenChange,\n      onBlur: handleAuthTokenBlur,\n    },\n    authTokenValue: {\n      value: authTokenValue,\n      set: setAuthTokenValue,\n    },\n    handleAutoDetectClick,\n    runAutoDetect: autoDetect,\n  };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useQuery.js",
    "content": "export default function useQuery() {\n  return new URLSearchParams(window.location.search);\n}\n"
  },
  {
    "path": "frontend/src/hooks/useScrollActiveItemIntoView.js",
    "content": "import { useEffect, useRef } from \"react\";\n\n/**\n * Hook that scrolls an element into view when it becomes active.\n * @param {Object} options - The options for the hook.\n * @param {boolean} options.isActive - Whether the element is currently active.\n * @param {\"smooth\" | \"instant\" | \"auto\"} options.behavior - The scroll behavior.\n * @param {\"start\" | \"center\" | \"end\" | \"nearest\"} options.block - The vertical alignment of the element within the scrollable container.\n * @returns {{ ref: React.RefObject<HTMLElement> }} An object containing the ref to attach to the target element.\n */\nexport default function useScrollActiveItemIntoView({\n  isActive,\n  behavior,\n  block,\n}) {\n  const ref = useRef(null);\n\n  useEffect(() => {\n    if (isActive) {\n      ref.current.scrollIntoView({\n        behavior,\n        block,\n      });\n    }\n  }, [isActive]);\n\n  return {\n    ref,\n  };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useSimpleSSO.js",
    "content": "import { useEffect, useState } from \"react\";\nimport System from \"@/models/system\";\n\n/**\n * Checks if Simple SSO is enabled and if the user should be redirected to the SSO login page.\n * @returns {{loading: boolean, ssoConfig: {enabled: boolean, noLogin: boolean, noLoginRedirect: string | null}}}\n */\nexport default function useSimpleSSO() {\n  const [loading, setLoading] = useState(true);\n  const [ssoConfig, setSsoConfig] = useState({\n    enabled: false,\n    noLogin: false,\n    noLoginRedirect: null,\n  });\n\n  useEffect(() => {\n    async function checkSsoConfig() {\n      try {\n        const settings = await System.keys();\n        setSsoConfig({\n          enabled: settings?.SimpleSSOEnabled,\n          noLogin: settings?.SimpleSSONoLogin,\n          noLoginRedirect: settings?.SimpleSSONoLoginRedirect,\n        });\n      } catch (e) {\n        console.error(e);\n      } finally {\n        setLoading(false);\n      }\n    }\n    checkSsoConfig();\n  }, []);\n\n  return { loading, ssoConfig };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useTextSize.js",
    "content": "import { useState, useEffect } from \"react\";\n\nexport default function useTextSize() {\n  const [textSize, setTextSize] = useState(\"normal\");\n  const [textSizeClass, setTextSizeClass] = useState(\"text-[14px]\");\n\n  const getTextSizeClass = (size) => {\n    switch (size) {\n      case \"small\":\n        return \"text-[12px]\";\n      case \"large\":\n        return \"text-[18px]\";\n      default:\n        return \"text-[14px]\";\n    }\n  };\n\n  useEffect(() => {\n    const storedTextSize = window.localStorage.getItem(\"anythingllm_text_size\");\n    if (storedTextSize) {\n      setTextSize(storedTextSize);\n      setTextSizeClass(getTextSizeClass(storedTextSize));\n    }\n\n    const handleTextSizeChange = (event) => {\n      const size = event.detail;\n      setTextSize(size);\n      setTextSizeClass(getTextSizeClass(size));\n    };\n\n    window.addEventListener(\"textSizeChange\", handleTextSizeChange);\n    return () => {\n      window.removeEventListener(\"textSizeChange\", handleTextSizeChange);\n    };\n  }, []);\n\n  return { textSize, textSizeClass };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useTheme.js",
    "content": "import { REFETCH_LOGO_EVENT } from \"@/LogoContext\";\nimport { useState, useEffect } from \"react\";\n\nconst availableThemes = {\n  system: \"System\",\n  light: \"Light\",\n  dark: \"Dark\",\n};\n\n/**\n * Determines the current theme of the application.\n * \"system\" follows the OS preference, \"light\" and \"dark\" force that mode.\n * @returns {{theme: ('system' | 'light' | 'dark'), setTheme: function, availableThemes: object}}\n */\nexport function useTheme() {\n  const [theme, _setTheme] = useState(() => {\n    const stored = localStorage.getItem(\"theme\");\n    if (stored === \"default\") return \"dark\"; // migrate legacy value\n    return stored || \"system\";\n  });\n\n  const [systemTheme, setSystemTheme] = useState(() =>\n    window.matchMedia?.(\"(prefers-color-scheme: light)\").matches\n      ? \"light\"\n      : \"dark\"\n  );\n\n  // Listen for OS level theme changes\n  useEffect(() => {\n    if (!window.matchMedia) return;\n    const mql = window.matchMedia(\"(prefers-color-scheme: light)\");\n    const handler = (e) => setSystemTheme(e.matches ? \"light\" : \"dark\");\n    mql.addEventListener(\"change\", handler);\n    return () => mql.removeEventListener(\"change\", handler);\n  }, []);\n\n  const resolvedTheme = theme === \"system\" ? systemTheme : theme;\n\n  useEffect(() => {\n    document.documentElement.setAttribute(\"data-theme\", resolvedTheme);\n    document.body.classList.toggle(\"light\", resolvedTheme === \"light\");\n    localStorage.setItem(\"theme\", theme);\n    window.dispatchEvent(new Event(REFETCH_LOGO_EVENT));\n  }, [resolvedTheme, theme]);\n\n  // In development, attach keybind combinations to toggle theme\n  useEffect(() => {\n    if (!import.meta.env.DEV) return;\n    function toggleOnKeybind(e) {\n      if (e.metaKey && e.key === \".\") {\n        e.preventDefault();\n        _setTheme((prev) => (prev === \"light\" ? \"dark\" : \"light\"));\n      }\n    }\n    document.addEventListener(\"keydown\", toggleOnKeybind);\n    return () => document.removeEventListener(\"keydown\", toggleOnKeybind);\n  }, []);\n\n  /**\n   * Sets the theme of the application and runs any\n   * other necessary side effects\n   * @param {string} newTheme The new theme to set\n   */\n  function setTheme(newTheme) {\n    _setTheme(newTheme);\n  }\n\n  return { theme, setTheme, availableThemes };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useUser.js",
    "content": "import { useContext } from \"react\";\nimport { AuthContext } from \"@/AuthContext\";\n\n// interface IStore {\n//   store: {\n//     user: {\n//       id: string;\n//       username: string | null;\n//       role: string;\n//     };\n//   };\n// }\n\nexport default function useUser() {\n  const context = useContext(AuthContext);\n\n  return { ...context.store };\n}\n"
  },
  {
    "path": "frontend/src/hooks/useWebPushNotifications.js",
    "content": "import { useEffect } from \"react\";\nimport { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\n\nconst PUSH_PUBKEY_URL = `${API_BASE}/web-push/pubkey`;\nconst PUSH_USER_SUBSCRIBE_URL = `${API_BASE}/web-push/subscribe`;\n\n// If you update the service worker, increment this version or else\n// the service worker will not be updated with new changes -\n// Its version ID is independent of the app version to prevent reloading\n// or cache busting when not needed.\nconst SW_VERSION = \"1.0.0\";\n\nfunction log(message, ...args) {\n  if (typeof message === \"object\") message = JSON.stringify(message, null, 2);\n  console.log(`[useWebPushNotifications] ${message}`, ...args);\n}\n\n/**\n * Subscribes to push notifications for the current client - can be called multiple times without re-subscribing\n * or generating infinite tokens.\n * @returns {void}\n */\nexport async function subscribeToPushNotifications() {\n  try {\n    if (!(\"serviceWorker\" in navigator) || !(\"PushManager\" in window)) {\n      log(\"Push notifications not supported\");\n      return;\n    }\n\n    // Check current permission status\n    const permission = await Notification.requestPermission();\n    if (permission !== \"granted\") {\n      log(\"Notification permission not granted\");\n      return;\n    }\n\n    const publicKey = await fetch(PUSH_PUBKEY_URL, { headers: baseHeaders() })\n      .then((res) => res.json())\n      .then(({ publicKey }) => {\n        if (!publicKey) throw new Error(\"No public key found or generated\");\n        return publicKey;\n      })\n      .catch(() => null);\n\n    if (!publicKey) return log(\"No public key found or generated\");\n\n    const swReg = await navigator.serviceWorker.register(\n      `/service-workers/push-notifications.js?v=${SW_VERSION}`\n    );\n\n    // Check for updates\n    swReg.addEventListener(\"updatefound\", () => {\n      const newWorker = swReg.installing;\n      log(\"Service worker update found\");\n\n      newWorker.addEventListener(\"statechange\", () => {\n        if (\n          newWorker.state === \"installed\" &&\n          navigator.serviceWorker.controller\n        ) {\n          // New service worker is installed and ready\n          log(\"New service worker installed, ready to activate\");\n\n          // Optionally show a notification to the user\n          if (confirm(\"A new version is available. Reload to update?\")) {\n            window.location.reload();\n          }\n        }\n      });\n    });\n\n    // Handle service worker updates\n    navigator.serviceWorker.addEventListener(\"controllerchange\", () => {\n      log(\"Service worker controller changed\");\n    });\n\n    if (swReg.installing) {\n      await new Promise((resolve) => {\n        swReg.installing.addEventListener(\"statechange\", () => {\n          if (swReg.installing?.state === \"activated\") resolve();\n        });\n      });\n    } else if (swReg.waiting) {\n      await new Promise((resolve) => {\n        swReg.waiting.addEventListener(\"statechange\", () => {\n          if (swReg.waiting?.state === \"activated\") resolve();\n        });\n      });\n    }\n\n    const subscription = await swReg.pushManager.subscribe({\n      userVisibleOnly: true,\n      applicationServerKey: urlBase64ToUint8Array(publicKey),\n    });\n    await fetch(PUSH_USER_SUBSCRIBE_URL, {\n      method: \"POST\",\n      body: JSON.stringify(subscription),\n      headers: baseHeaders(),\n    });\n  } catch (error) {\n    log(\"Error subscribing to push notifications\", error);\n  }\n}\n\n/**\n * Hook that registers a service worker for push notifications.\n * @returns {void}\n */\nexport default function useWebPushNotifications() {\n  useEffect(() => {\n    subscribeToPushNotifications();\n  }, []);\n}\n\nfunction urlBase64ToUint8Array(base64String) {\n  const padding = \"=\".repeat((4 - (base64String.length % 4)) % 4);\n  const base64 = (base64String + padding)\n    .replace(/\\-/g, \"+\")\n    .replace(/_/g, \"/\");\n  const rawData = atob(base64);\n  return new Uint8Array([...rawData].map((char) => char.charCodeAt(0)));\n}\n"
  },
  {
    "path": "frontend/src/i18n.js",
    "content": "import i18next from \"i18next\";\nimport { initReactI18next } from \"react-i18next\";\nimport LanguageDetector from \"i18next-browser-languagedetector\";\nimport { defaultNS, resources } from \"./locales/resources\";\n\ni18next\n  // https://github.com/i18next/i18next-browser-languageDetector/blob/9efebe6ca0271c3797bc09b84babf1ba2d9b4dbb/src/index.js#L11\n  .use(initReactI18next) // Initialize i18n for React\n  .use(LanguageDetector)\n  .init({\n    fallbackLng: \"en\",\n    debug: import.meta.env.DEV,\n    defaultNS,\n    resources,\n    lowerCaseLng: true,\n    interpolation: {\n      escapeValue: false,\n    },\n  });\n\nexport default i18next;\n"
  },
  {
    "path": "frontend/src/index.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n* {\n  -webkit-font-smoothing: antialiased;\n}\n\n:root {\n  /* Default theme */\n  --theme-loader: #ffffff;\n  --theme-bg-primary: #0e0f0f;\n  --theme-bg-secondary: #1b1b1e;\n  --theme-bg-sidebar: #0e0f0f;\n  --theme-bg-container: #0e0f0f;\n  --theme-bg-chat: #1b1b1e;\n  --theme-bg-chat-input: #27282a;\n  --theme-text-primary: #ffffff;\n  --theme-text-secondary: rgba(255, 255, 255, 0.6);\n  --theme-placeholder: #57585a;\n  --theme-sidebar-item-default: rgba(255, 255, 255, 0.1);\n  --theme-sidebar-item-selected: rgba(255, 255, 255, 0.3);\n  --theme-sidebar-item-hover: #3f3f42;\n  --theme-sidebar-subitem-default: rgba(255, 255, 255, 0.05);\n  --theme-sidebar-subitem-selected: rgba(255, 255, 255, 0.05);\n  --theme-sidebar-thread-selected: rgba(255, 255, 255, 0.05);\n  --theme-popup-menu-bg: #000000;\n\n  --theme-sidebar-subitem-hover: rgba(255, 255, 255, 0.05);\n  --theme-sidebar-border: rgba(255, 255, 255, 0.1);\n  --theme-sidebar-item-workspace-active: #ffffff;\n  --theme-sidebar-item-workspace-inactive: #ffffff;\n\n  --theme-sidebar-footer-icon: rgba(255, 255, 255, 0.1);\n  --theme-sidebar-footer-icon-fill: #ffffff;\n  --theme-sidebar-footer-icon-hover: rgba(255, 255, 255, 0.2);\n\n  --theme-chat-input-border: #525355;\n  --theme-action-menu-bg: #27282a;\n  --theme-action-menu-item-hover: rgba(255, 255, 255, 0.1);\n  --theme-settings-input-bg: #0e0f0f;\n  --theme-settings-input-placeholder: rgba(255, 255, 255, 0.5);\n  --theme-settings-input-active: rgb(255 255 255 / 0.2);\n  --theme-settings-input-text: #ffffff;\n  --theme-modal-border: #3f3f42;\n\n  --theme-button-primary: #46c8ff;\n  --theme-button-primary-hover: #434343;\n\n  --theme-button-cta: #7cd4fd;\n\n  --theme-file-row-even: #0e0f0f;\n  --theme-file-row-odd: #1b1b1e;\n  --theme-file-row-selected-even: rgba(14, 165, 233, 0.2);\n  --theme-file-row-selected-odd: rgba(14, 165, 233, 0.1);\n  --theme-file-picker-hover: rgb(14 165 233 / 0.2);\n\n  --theme-home-text: #ffffff;\n  --theme-home-text-secondary: #9f9fa0;\n  --theme-home-bg-card: #1a1b1b;\n  --theme-home-bg-button: #252626;\n  --theme-home-border: rgba(255, 255, 255, 0.2);\n  --theme-home-button-primary: #36bffa;\n  --theme-home-button-primary-hover: rgba(54, 191, 250, 0.9);\n  --theme-home-button-secondary: #27282a;\n  --theme-home-button-secondary-hover: rgba(54, 191, 250, 0.1);\n  --theme-home-button-secondary-text: #ffffff;\n  --theme-home-button-secondary-hover-text: #36bffa;\n  --theme-home-update-card-bg: #1c1c1c;\n  --theme-home-update-card-hover: #252525;\n  --theme-home-update-source: #53b1fd;\n\n  --theme-checklist-item-bg: #203c48;\n  --theme-checklist-item-bg-hover: #255d75;\n  --theme-checklist-item-text: #b9e6fe;\n  --theme-checklist-item-completed-bg: #36463d;\n  --theme-checklist-item-completed-text: #a6f4c5;\n  --theme-checklist-checkbox-fill: #a6f4c5;\n  --theme-checklist-checkbox-text: #36463d;\n  --theme-checklist-item-hover: #36bffa;\n  --theme-checklist-checkbox-border: #ffffff;\n  --theme-checklist-button-border: #36bffa;\n  --theme-checklist-button-text: #36bffa;\n  --theme-checklist-button-hover-bg: rgba(54, 191, 250, 0.2);\n  --theme-checklist-button-hover-border: rgba(54, 191, 250, 0.8);\n\n  --theme-home-button-secondary-border: #acc1e6;\n  --theme-home-button-secondary-border-hover: #293056;\n\n  --theme-attachment-bg: #18191a;\n  --theme-attachment-error-bg: rgba(180, 35, 24, 0.4);\n  --theme-attachment-success-bg: #18191a;\n  --theme-attachment-text: #ffffff;\n  --theme-attachment-text-secondary: rgba(255, 255, 255, 0.8);\n  --theme-attachment-icon: #ffffff;\n  --theme-attachment-icon-spinner: #ffffff;\n  --theme-attachment-icon-spinner-bg: #27282a;\n\n  --theme-button-text: #a8a9ab;\n  --theme-button-code-hover-text: #7cd4fd;\n  --theme-button-code-hover-bg: #22343f;\n  --theme-button-disable-hover-text: #fec84b;\n  --theme-button-disable-hover-bg: #3a3128;\n  --theme-button-delete-hover-text: #f97066;\n  --theme-button-delete-hover-bg: #37282b;\n}\n\n[data-theme=\"light\"] {\n  --theme-loader: #000000;\n  --theme-bg-primary: #ffffff;\n  --theme-bg-secondary: #ffffff;\n  --theme-bg-sidebar: #edf2fa;\n  --theme-bg-container: #f9fbfd;\n  --theme-popup-menu-bg: #c2e7fe;\n\n  --theme-bg-chat: #ffffff;\n  --theme-bg-chat-input: #eaeaea;\n  --theme-text-primary: #0e0f0f;\n  --theme-text-secondary: #7a7d7e;\n  --theme-placeholder: #9ca3af;\n  --theme-sidebar-item-default: #ffffff;\n  --theme-sidebar-item-selected: #ffffff;\n  --theme-sidebar-item-hover: #c8efff;\n\n  --theme-sidebar-item-text-inactive: #7a7d7e;\n  --theme-sidebar-item-text-active: #184558;\n\n  --theme-sidebar-item-workspace-active: #000000;\n  --theme-sidebar-item-workspace-inactive: #7a7d7e;\n\n  --theme-sidebar-subitem-default: transparent;\n  --theme-sidebar-subitem-selected: #e2e7ee;\n  --theme-sidebar-thread-selected: #ffffff;\n  --theme-sidebar-subitem-hover: #e2e7ee;\n  --theme-sidebar-border: #d3d4d4;\n\n  --theme-sidebar-footer-icon: #ffffff;\n  --theme-sidebar-footer-icon-fill: #6e6f6f;\n  --theme-sidebar-footer-icon-hover: #d8d6d6;\n\n  --theme-chat-input-border: #cccccc;\n  --theme-action-menu-bg: #eaeaea;\n  --theme-action-menu-item-hover: rgba(0, 0, 0, 0.1);\n  --theme-settings-input-bg: #edf2fa;\n  --theme-settings-input-placeholder: rgba(0, 0, 0, 0.5);\n  --theme-settings-input-active: rgb(0 0 0 / 0.2);\n  --theme-settings-input-text: #0e0f0f;\n  --theme-modal-border: #d3d3d3;\n\n  --theme-button-primary: #0ba5ec;\n  --theme-button-primary-hover: #dedede;\n\n  --theme-button-cta: #7cd4fd;\n\n  --theme-file-row-even: #f5f5f5;\n  --theme-file-row-odd: #e9e9e9;\n  --theme-file-row-selected-even: #0ba5ec;\n  --theme-file-row-selected-odd: #0ba5ec;\n  --theme-file-picker-hover: #e2e7ee;\n\n  --theme-home-text: #0e0f0f;\n  --theme-home-text-secondary: #6f6f71;\n  --theme-home-bg-card: #edf2fa;\n  --theme-home-bg-button: #f3f4f6;\n  --theme-home-border: rgba(0, 0, 0, 0.1);\n  --theme-home-button-primary: #36bffa;\n  --theme-home-button-primary-hover: rgba(54, 191, 250, 0.9);\n  --theme-home-button-secondary: #dbe8fe;\n  --theme-home-button-secondary-hover: #b0c8f1;\n  --theme-home-button-secondary-text: #293056;\n  --theme-home-button-secondary-hover-text: #293056;\n  --theme-home-update-card-bg: #edf2fa;\n  --theme-home-update-card-hover: #f3f4f6;\n  --theme-home-update-source: #0284c7;\n\n  --theme-checklist-item-bg: #c7e2ee;\n  --theme-checklist-item-bg-hover: #a3d9f1;\n  --theme-checklist-item-text: #0d3851;\n  --theme-checklist-item-completed-bg: #d8f3ea;\n  --theme-checklist-item-completed-text: #039855;\n  --theme-checklist-checkbox-fill: #6ce9a6;\n  --theme-checklist-checkbox-text: #ffffff;\n  --theme-checklist-item-hover: #0ba5ec;\n  --theme-checklist-checkbox-border: #6b7280;\n  --theme-checklist-button-border: #0ba5ec;\n  --theme-checklist-button-text: #0ba5ec;\n  --theme-checklist-button-hover-bg: rgba(11, 165, 236, 0.1);\n  --theme-checklist-button-hover-border: rgba(11, 165, 236, 0.8);\n\n  --theme-home-button-secondary-border-hover: #293056;\n\n  --theme-attachment-bg: #edf2fa;\n  --theme-attachment-error-bg: rgba(180, 35, 24, 0.3);\n  --theme-attachment-success-bg: #eaeaea;\n  --theme-attachment-text: #0e0f0f;\n  --theme-attachment-text-secondary: rgba(0, 0, 0, 0.8);\n  --theme-attachment-icon: #ffffff;\n  --theme-attachment-icon-spinner: #7cd4fd;\n  --theme-attachment-icon-spinner-bg: #ffffff;\n\n  --theme-button-text: #a8a9ab;\n  --theme-button-code-hover-text: #0ba5ec;\n  --theme-button-code-hover-bg: #e8f7fe;\n  --theme-button-disable-hover-text: #854708;\n  --theme-button-disable-hover-bg: #fef7e6;\n  --theme-button-delete-hover-text: #b42318;\n  --theme-button-delete-hover-bg: #fee4e2;\n}\n\n[data-theme=\"light\"] .text-white {\n  color: var(--theme-text-primary);\n}\n\n[data-theme=\"light\"] .text-description,\n[data-theme=\"light\"] .text-white\\/60 {\n  color: var(--theme-text-secondary);\n}\n\n[data-theme=\"light\"] .bg-theme-bg-secondary {\n  border: 1px solid var(--theme-sidebar-border);\n}\n\n[data-theme=\"light\"] .border-white\\/10 {\n  border-color: var(--theme-sidebar-border);\n}\n\n/*\nThis is to override the default border color for the select and input elements\nin the onboarding flow when the theme is not light. This only applies to the\nonboarding flow since its background is dark and is the same fill as the inputs.\n*/\n[data-layout=\"onboarding\"] > * select:not([data-theme=\"light\"]),\n[data-layout=\"onboarding\"] > * input:not([data-theme=\"light\"]),\n[data-layout=\"onboarding\"] > * textarea:not([data-theme=\"light\"]) {\n  border: 1px solid #ffffff;\n}\n\nhtml,\nbody {\n  padding: 0;\n  margin: 0;\n  font-family:\n    \"plus-jakarta-sans\",\n    -apple-system,\n    BlinkMacSystemFont,\n    Segoe UI,\n    Roboto,\n    Oxygen,\n    Ubuntu,\n    Cantarell,\n    Fira Sans,\n    Droid Sans,\n    Helvetica Neue,\n    sans-serif;\n  background-color: white;\n}\n\n@media (prefers-color-scheme: dark) {\n  body {\n    background-color: #0e0f0f;\n  }\n}\n\n@media (max-width: 600px) {\n  html {\n    overscroll-behavior: none;\n  }\n}\n\na {\n  color: inherit;\n  text-decoration: none;\n}\n\n* {\n  box-sizing: border-box;\n}\n\n.g327 {\n  border-color: #302f30;\n}\n\n@font-face {\n  font-family: \"plus-jakarta-sans\";\n  src: url(\"../public/fonts/PlusJakartaSans.ttf\");\n  font-display: swap;\n}\n\n.grr {\n  grid-template-columns: repeat(2, 1fr);\n}\n\n.greyC {\n  filter: gray;\n  -webkit-filter: grayscale(100%);\n  transition: 0.4s;\n}\n\n.greyC:hover {\n  filter: none;\n  -webkit-filter: none;\n  transition: 0.4s;\n}\n\n.chat__message {\n  transform-origin: 0 100%;\n  transform: scale(0);\n  animation: message 0.15s ease-out 0s forwards;\n  animation-delay: 500ms;\n}\n\n@keyframes message {\n  0% {\n    max-height: 100%;\n  }\n\n  80% {\n    transform: scale(1.1);\n  }\n\n  100% {\n    transform: scale(1);\n    max-height: 100%;\n    overflow: visible;\n    padding-top: 1rem;\n  }\n}\n\n\n@media (prefers-color-scheme: light) {\n  .sidebar-items:after {\n    content: \" \";\n    position: absolute;\n    left: 0;\n    right: 0px;\n    height: 4em;\n    top: 69vh;\n    z-index: 1;\n    pointer-events: none;\n  }\n}\n\n@media (prefers-color-scheme: dark) {\n  .sidebar-items:after {\n    content: \" \";\n    position: absolute;\n    left: 0;\n    right: 0px;\n    height: 4em;\n    top: 69vh;\n    z-index: 1;\n    pointer-events: none;\n  }\n}\n\n@media (prefers-color-scheme: light) {\n  .fade-up-border {\n    background: linear-gradient(\n      to bottom,\n      rgba(220, 221, 223, 10%),\n      rgb(220, 221, 223) 89%\n    );\n  }\n}\n\n@media (prefers-color-scheme: dark) {\n  .fade-up-border {\n    background: linear-gradient(\n      to bottom,\n      rgba(41, 37, 36, 50%),\n      rgb(41 37 36) 90%\n    );\n  }\n}\n\n/**\n * ==============================================\n * Dot Falling\n * ==============================================\n */\n.dot-falling {\n  position: relative;\n  left: -9999px;\n  width: 10px;\n  height: 10px;\n  border-radius: 5px;\n  background-color: #eeeeee;\n  color: #5fa4fa;\n  box-shadow: 9999px 0 0 0 #eeeeee;\n  animation: dot-falling 1.5s infinite linear;\n  animation-delay: 0.1s;\n}\n\n.dot-falling::before,\n.dot-falling::after {\n  content: \"\";\n  display: inline-block;\n  position: absolute;\n  top: 0;\n}\n\n.dot-falling::before {\n  width: 10px;\n  height: 10px;\n  border-radius: 5px;\n  background-color: #eeeeee;\n  color: #eeeeee;\n  animation: dot-falling-before 1.5s infinite linear;\n  animation-delay: 0s;\n}\n\n.dot-falling::after {\n  width: 10px;\n  height: 10px;\n  border-radius: 5px;\n  background-color: #eeeeee;\n  color: #eeeeee;\n  animation: dot-falling-after 1.5s infinite linear;\n  animation-delay: 0.2s;\n}\n\n@keyframes dot-falling {\n  0% {\n    box-shadow: 9999px -15px 0 0 rgba(152, 128, 255, 0);\n  }\n\n  25%,\n  50%,\n  75% {\n    box-shadow: 9999px 0 0 0 #eeeeee;\n  }\n\n  100% {\n    box-shadow: 9999px 15px 0 0 rgba(152, 128, 255, 0);\n  }\n}\n\n@keyframes dot-falling-before {\n  0% {\n    box-shadow: 9984px -15px 0 0 rgba(152, 128, 255, 0);\n  }\n\n  25%,\n  50%,\n  75% {\n    box-shadow: 9984px 0 0 0 #eeeeee;\n  }\n\n  100% {\n    box-shadow: 9984px 15px 0 0 rgba(152, 128, 255, 0);\n  }\n}\n\n@keyframes dot-falling-after {\n  0% {\n    box-shadow: 10014px -15px 0 0 rgba(152, 128, 255, 0);\n  }\n\n  25%,\n  50%,\n  75% {\n    box-shadow: 10014px 0 0 0 #eeeeee;\n  }\n\n  100% {\n    box-shadow: 10014px 15px 0 0 rgba(152, 128, 255, 0);\n  }\n}\n\n.show-scrollbar {\n  overflow-y: scroll !important;\n  scrollbar-width: thin !important;\n  scrollbar-color: rgba(255, 255, 255, 0.3) rgba(0, 0, 0, 0.1) !important;\n  -webkit-overflow-scrolling: touch !important;\n}\n\n.show-scrollbar::-webkit-scrollbar {\n  width: 8px !important;\n  display: block !important;\n  background: transparent !important;\n}\n\n.show-scrollbar::-webkit-scrollbar-track {\n  background: rgba(0, 0, 0, 0.1) !important;\n  margin: 3px !important;\n  border-radius: 4px !important;\n}\n\n.show-scrollbar::-webkit-scrollbar-thumb {\n  background-color: rgba(255, 255, 255, 0.3) !important;\n  border-radius: 4px !important;\n  border: none !important;\n  min-height: 40px !important;\n}\n\n.show-scrollbar::-webkit-scrollbar,\n.show-scrollbar::-webkit-scrollbar-thumb,\n.show-scrollbar::-webkit-scrollbar-track {\n  visibility: visible !important;\n  opacity: 1 !important;\n}\n\n.show-scrollbar,\n.show-scrollbar::-webkit-scrollbar,\n.show-scrollbar::-webkit-scrollbar-thumb,\n.show-scrollbar::-webkit-scrollbar-track {\n  transition: none !important;\n  animation: none !important;\n}\n\n#chat-container::-webkit-scrollbar,\n.no-scroll::-webkit-scrollbar {\n  display: none !important;\n}\n\n/* Hide scrollbar for IE, Edge and Firefox */\n.no-scroll {\n  -ms-overflow-style: none !important;\n  /* IE and Edge */\n  scrollbar-width: none !important;\n  /* Firefox */\n}\n\n.z-99 {\n  z-index: 99;\n}\n\n.z-98 {\n  z-index: 98;\n}\n\n.file-uploader {\n  width: 100% !important;\n  height: 100px !important;\n}\n\n.grid-loader > circle {\n  fill: #008eff;\n}\n\ndialog {\n  pointer-events: none;\n  opacity: 0;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n}\n\ndialog[open] {\n  opacity: 1;\n  pointer-events: inherit;\n}\n\ndialog::backdrop {\n  background: rgba(0, 0, 0, 0.5);\n  backdrop-filter: blur(2px);\n}\n\n.backdrop {\n  background: rgba(0, 0, 0, 0.5);\n  backdrop-filter: blur(2px);\n}\n\n.animate-slow-pulse {\n  transform: scale(1);\n  animation: subtlePulse 20s infinite;\n  will-change: transform;\n}\n\n@keyframes subtlePulse {\n  0% {\n    transform: scale(1);\n  }\n\n  50% {\n    transform: scale(1.1);\n  }\n\n  100% {\n    transform: scale(1);\n  }\n}\n\n@keyframes subtleShift {\n  0% {\n    background-position: 0% 50%;\n  }\n\n  50% {\n    background-position: 100% 50%;\n  }\n\n  100% {\n    background-position: 0% 50%;\n  }\n}\n\n.login-input-gradient {\n  background: linear-gradient(\n    180deg,\n    rgba(61, 65, 71, 0.3) 0%,\n    rgba(44, 47, 53, 0.3) 100%\n  ) !important;\n  box-shadow: 0px 4px 30px rgba(0, 0, 0, 0.25);\n}\n\n.white-fill {\n  fill: white;\n}\n\n.tip:before {\n  content: \"\";\n  display: block;\n  width: 0;\n  height: 0;\n  position: absolute;\n\n  border-bottom: 8px solid transparent;\n  border-top: 8px solid rgba(255, 255, 255, 0.5);\n  border-left: 8px solid transparent;\n  border-right: 8px solid transparent;\n  border-radius: 0px 0px 0px 5px;\n  left: 1%;\n\n  top: 100%;\n}\n\n.user-reply > div:first-of-type {\n  border: 2px solid white;\n}\n\n.reply > *:last-child::after {\n  content: \"|\";\n  animation: blink 1.5s steps(1) infinite;\n  color: white;\n  font-size: 14px;\n}\n\n@keyframes blink {\n  0% {\n    opacity: 0;\n  }\n\n  50% {\n    opacity: 1;\n  }\n\n  100% {\n    opacity: 0;\n  }\n}\n\n@layer components {\n  .radio-container:has(input:checked) {\n    @apply border-blue-500 bg-blue-400/10 text-blue-800;\n  }\n}\n\n.tooltip {\n  @apply !bg-black !text-white !py-2 !px-3 !rounded-md !z-10;\n}\n\n.Toastify__toast-body {\n  white-space: pre-line;\n}\n\n@keyframes slideDown {\n  from {\n    max-height: 0;\n    opacity: 0;\n  }\n\n  to {\n    max-height: 400px;\n    opacity: 1;\n  }\n}\n\n.slide-down {\n  animation: slideDown 0.3s ease-out forwards;\n}\n\n.input-label {\n  @apply text-[14px] font-bold text-white;\n}\n\n/**\n * ==============================================\n * Markdown Styles\n * ==============================================\n */\n.markdown,\n.markdown > * {\n  font-weight: 400;\n}\n\n.markdown h1 {\n  font-size: xx-large;\n  line-height: 1.7;\n  padding-left: 0.3rem;\n}\n\n.markdown h2 {\n  line-height: 1.5;\n  font-size: x-large;\n  padding-left: 0.3rem;\n}\n\n.markdown h3 {\n  line-height: 1.4;\n  font-size: large;\n  padding-left: 0.3rem;\n}\n\n/* Table Styles */\n\n.markdown table {\n  border-collapse: separate;\n}\n\n.markdown th {\n  border-top: none;\n}\n\n.markdown td:first-child,\n.markdown th:first-child {\n  border-left: none;\n}\n\n.markdown table {\n  width: 100%;\n  border-collapse: collapse;\n  color: #bdbdbe;\n  font-size: 13px;\n  margin: 30px 0px;\n  border-radius: 10px;\n  overflow: hidden;\n  font-weight: normal;\n}\n\n.markdown table thead {\n  color: #fff;\n  text-transform: uppercase;\n  font-weight: bolder;\n}\n\n.markdown hr {\n  border: 0;\n  border-top: 1px solid #cdcdcd40;\n  margin: 1rem 0;\n}\n\n.markdown table th,\n.markdown table td {\n  padding: 8px 15px;\n  border-bottom: 1px solid #cdcdcd2e;\n  text-align: left;\n}\n\n.markdown table th {\n  padding: 14px 15px;\n}\n\n.markdown > * a {\n  color: var(--theme-button-cta);\n  text-decoration: underline;\n}\n\n@media (max-width: 600px) {\n  .markdown table th,\n  .markdown table td {\n    padding: 10px;\n  }\n}\n\n[data-theme=\"light\"] .markdown table,\n[data-theme=\"light\"] .markdown table th,\n[data-theme=\"light\"] .markdown table td {\n  color: #000;\n}\n\n/* List Styles */\n.markdown ol {\n  list-style: decimal-leading-zero;\n  padding-left: 0px;\n  padding-top: 10px;\n  margin: 10px;\n}\n\n.markdown ol li {\n  margin-left: 20px;\n  padding-left: 10px;\n  position: relative;\n  transition: all 0.3s ease;\n  line-height: 1.4rem;\n}\n\n.markdown ol li::marker {\n  padding-top: 10px;\n}\n\n.markdown ol li p {\n  margin: 0.5rem;\n  padding-top: 10px;\n}\n\n.markdown ol li a {\n  text-decoration: underline;\n}\n\n.markdown ol li p a {\n  text-decoration: underline;\n}\n\n.markdown ul {\n  list-style: revert-layer;\n  /* color: #cfcfcfcf; */\n  padding-left: 0px;\n  padding-top: 10px;\n  padding-bottom: 10px;\n  margin: 10px;\n}\n\n.markdown ul li::marker {\n  color: #d0d0d0cf;\n  padding-top: 10px;\n}\n\n.markdownul li {\n  margin-left: 20px;\n\n  padding-left: 10px;\n  transition: all 0.3s ease;\n  line-height: 1.4rem;\n}\n\n.markdownul li a {\n  text-decoration: underline;\n}\n\n.markdown ul li > ul {\n  padding-left: 20px;\n  margin: 0px;\n}\n\n.markdown p {\n  font-weight: 400;\n  margin: 0.35rem;\n}\n\n.markdown > p > a,\n.markdown p a {\n  text-decoration: underline;\n}\n\n.markdown {\n  text-wrap: wrap;\n}\n\n.markdown pre {\n  margin: 20px 0;\n}\n\n.markdown strong {\n  font-weight: 600;\n  color: #fff;\n}\n\n.file-row {\n  border-left: none !important;\n  border-right: none !important;\n  border-top: none !important;\n}\n\n.file-row:nth-child(even) {\n  @apply bg-theme-bg-primary;\n  background-color: var(--theme-file-row-even);\n  border-bottom: 1px solid rgba(255, 255, 255, 0.05);\n}\n\n.file-row:nth-child(odd) {\n  @apply bg-theme-bg-secondary;\n  background-color: var(--theme-file-row-odd);\n  border-bottom: 1px solid rgba(255, 255, 255, 0.05);\n}\n\n.file-row.selected:nth-child(even),\n.file-row.selected:nth-child(odd) {\n  background-color: var(--theme-file-row-selected-even);\n  border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n[data-theme=\"light\"] .file-row.selected:nth-child(even),\n[data-theme=\"light\"] .file-row.selected:nth-child(odd) {\n  border-bottom: 1px solid rgba(222, 222, 222, 0.5);\n}\n\n/* Flex upload modal to be a column when on small screens so that the UI\ndoes not extend the close button beyond the viewport. */\n@media (max-width: 1330px) {\n  .upload-modal {\n    @apply !flex-col !items-center !py-4 no-scroll;\n  }\n\n  .upload-modal-arrow {\n    margin-top: 0px !important;\n  }\n}\n\n.upload-modal {\n  @apply flex-row items-start gap-x-6 justify-center;\n}\n\n.upload-modal-arrow {\n  margin-top: 25%;\n}\n\n/* Scrollbar container */\n.white-scrollbar {\n  overflow-y: scroll;\n  scrollbar-width: thin;\n  scrollbar-color: #ffffff #18181b;\n  margin-right: 8px;\n}\n\n/* Webkit browsers (Chrome, Safari) */\n.white-scrollbar::-webkit-scrollbar {\n  width: 3px;\n  background-color: #18181b;\n}\n\n.white-scrollbar::-webkit-scrollbar-track {\n  background-color: #18181b;\n  margin-right: 8px;\n}\n\n.white-scrollbar::-webkit-scrollbar-thumb {\n  background-color: #ffffff;\n  border-radius: 4px;\n  border: 2px solid #18181b;\n}\n\n.white-scrollbar::-webkit-scrollbar-thumb:hover {\n  background-color: #cccccc;\n}\n\n/* Recharts rendering styles */\n.recharts-text > * {\n  fill: #fff;\n}\n\n[data-theme=\"light\"] .recharts-text > * {\n  fill: #000;\n}\n\n.recharts-legend-wrapper {\n  margin-bottom: 10px;\n}\n\n.text-tremor-content {\n  padding-bottom: 10px;\n}\n\n.file-upload {\n  -webkit-animation: fadein 0.3s linear forwards;\n  animation: fadein 0.3s linear forwards;\n}\n\n.file-upload-fadeout {\n  -webkit-animation: fadeout 0.3s linear forwards;\n  animation: fadeout 0.3s linear forwards;\n}\n\n@-webkit-keyframes fadein {\n  0% {\n    opacity: 0;\n  }\n\n  100% {\n    opacity: 1;\n  }\n}\n\n@keyframes fadein {\n  0% {\n    opacity: 0;\n  }\n\n  100% {\n    opacity: 1;\n  }\n}\n\n@-webkit-keyframes fadeout {\n  0% {\n    opacity: 1;\n  }\n\n  100% {\n    opacity: 0;\n  }\n}\n\n@keyframes fadeout {\n  0% {\n    opacity: 1;\n  }\n\n  100% {\n    opacity: 0;\n  }\n}\n\n.search-input::-webkit-search-cancel-button {\n  filter: grayscale(100%) invert(1) brightness(100) opacity(0.5);\n}\n\n[data-theme=\"light\"] .search-input::-webkit-search-cancel-button {\n  filter: grayscale(100%) invert(0) brightness(0) opacity(0.5);\n}\n\n.animate-remove {\n  animation: fadeAndShrink 800ms forwards;\n}\n\n@keyframes fadeAndShrink {\n  50% {\n    opacity: 25%;\n  }\n\n  75% {\n    opacity: 10%;\n  }\n\n  100% {\n    height: 0px;\n    opacity: 0%;\n    display: none;\n  }\n}\n\n/* Math/Katex formatting to prevent duplication of content on screen */\n.katex-html[aria-hidden=\"true\"] {\n  display: none;\n}\n\n.katex-mathml {\n  font-size: 20px;\n}\n\n.rti--container {\n  @apply !bg-theme-settings-input-bg !text-white !placeholder-white !placeholder-opacity-60 !text-sm !rounded-lg !p-2.5;\n}\n\n@keyframes fadeUpIn {\n  0% {\n    opacity: 0;\n    transform: translateY(5px);\n  }\n\n  100% {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n.animate-fadeUpIn {\n  animation: fadeUpIn 0.3s ease-out forwards;\n}\n\n@keyframes bounce-subtle {\n  0%,\n  100% {\n    transform: translateY(0);\n  }\n\n  50% {\n    transform: translateY(-2px);\n  }\n}\n\n@keyframes thoughtTransition {\n  0% {\n    opacity: 0;\n    transform: translateY(10px);\n  }\n\n  30% {\n    opacity: 1;\n    transform: translateY(0);\n  }\n\n  100% {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n.animate-thoughtTransition {\n  animation: thoughtTransition 0.5s ease-out forwards;\n}\n\n.checklist-completed {\n  -webkit-animation: fadein 0.3s linear forwards;\n  animation: fadein 0.3s linear forwards;\n}\n"
  },
  {
    "path": "frontend/src/locales/ar/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"مرحبا في\",\n      getStarted: \"بسم الله\",\n    },\n    llm: {\n      title: \"إعدادات نموذج التعلم العميق المفضّلة\",\n      description:\n        \"يمكن لـِ  إيني ثينك إلْلْمْ العمل مع عدة موفرين لنماذج التعلم العميق لأداء خدمة المحادثات\",\n    },\n    userSetup: {\n      title: \"إنشاء المستعمِل\",\n      description: \".ضبط إعدادات مستعمِلِك\",\n      howManyUsers: \"كم من مستعمِل سيستعمِل هذا المثيل ؟\",\n      justMe: \"فقط أنا\",\n      myTeam: \"فريقي\",\n      instancePassword: \"كلمة مرورالمثيل\",\n      setPassword: \"هل تريد إنشاء كلمة مرور ؟\",\n      passwordReq: \"يجب أن تحتوي كلمة المرور على ثمانية حروف على الأقل\",\n      passwordWarn: \"من المهم حفظ كلمة المرور هذه لأنه لا يمكن استردادها.\",\n      adminUsername: \"اسم مستعمل حساب المشرف\",\n      adminPassword: \"كلمة مرور حساب المشرف\",\n      adminPasswordReq: \"يجب أن تكون كلمات المرور 8 أحرف على الأقل.\",\n      teamHint:\n        \"بمجرد اكتمال الإنشاء  ستكون المشرف الوحيد يمكنك دعوة الآخرين ليكونوا مستعملين أو مشرفين. لا تفقد كلمة المرور الخاصة بك حيث يمكن للمشرفين فقط إعادة تعيين كلمات المرور\",\n    },\n    data: {\n      title: \"معالجة البيانات والخصوصية\",\n      description:\n        \"نحن ملتزمون بالشفافية والمراقبة عندما يتعلق الأمر ببياناتك الشخصية.\",\n      settingsHint: \"يمكن إعادة ضبط هذه الإعدادات في أي وقت.\",\n    },\n    survey: {\n      title: \"مرحباً في إيني ثينك إلْلْمْ\",\n      description:\n        \" بما يتناسب مع احتياجاتك ساعدنا إذا أحببت في تصميم  إيني ثينك إلْلْمْ\",\n      email: \"ما هو بريدك الالكتروني؟\",\n      useCase: \"لماذا ستستخدم إيني ثينك إلْلْمْ؟\",\n      useCaseWork: \"للعمل\",\n      useCasePersonal: \"للاستخدام الشخصي\",\n      useCaseOther: \"شيء آخَر\",\n      comment: \"كيف سمعت عن إيني ثينك إلْلْمْ ؟\",\n      commentPlaceholder:\n        \"أخبرنا كيف وجدتنا!، يوتيوب، تويتر، جيثوب، ريديت وما إلى ذلك -\",\n      skip: \"تخطي الاستطلاع\",\n      thankYou: \"شكرا على تقييماتك!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"اسم مساحة العمل\",\n    user: \"مستعمِل\",\n    selection: \"اختيار النموذج\",\n    saving: \"حفظ...\",\n    save: \"حفظ التغييرات\",\n    previous: \"الصفحة السابقة\",\n    next: \"الصفحة التالية\",\n    optional: \"اختياري\",\n    yes: \"نعم\",\n    no: \"لا\",\n    search: \"بحث\",\n    username_requirements:\n      \"يجب أن يتكون اسم المستخدم من 2 إلى 32 حرفًا، ويبدأ بحرف صغير، ويحتوي فقط على حروف وأرقام وعلامات التسطير والنقاط.\",\n    on: \"على\",\n    none: \"لا\",\n    stopped: \"توقف\",\n    loading: \"تحميل\",\n    refresh: \"استعيد/جدد\",\n  },\n  settings: {\n    title: \"إعدادات المثيل\",\n    invites: \"دعوات\",\n    users: \"مستعملون\",\n    workspaces: \"مساحات العمل\",\n    \"workspace-chats\": \"محادثات مساحة العمل\",\n    customization: \"التخصيص\",\n    \"api-keys\": \"واجهة برمجة التطبيقات للمطورين\",\n    llm: \"النماذج اللغوية الكبيرة\",\n    transcription: \"النسْخ\",\n    embedder: \"مُضمّن\",\n    \"text-splitting\": \"تقسيم النص تقطيعه\",\n    \"voice-speech\": \"الصوت والخطاب\",\n    \"vector-database\": \"قاعدة بيانات المتجهات\",\n    embeds: \"تضمين المحادثة\",\n    security: \"حماية\",\n    \"event-logs\": \"سجلات الأحداث\",\n    privacy: \"الخصوصية والبيانات\",\n    \"ai-providers\": \"موفرو الذكاء الاصطناعي\",\n    \"agent-skills\": \"مهارات الوكيل\",\n    admin: \"مشرف\",\n    tools: \"أدوات\",\n    \"experimental-features\": \"الميزات التجريبية\",\n    contact: \"اتصل بالدعم\",\n    \"browser-extension\": \"ملحق المتصفح\",\n    \"system-prompt-variables\": \"متغيرات المطالبات للنظام\",\n    interface: \"تفضيلات واجهة المستخدم\",\n    branding: \"التسويق بالعلامة التجارية ووضع العلامات التجارية\",\n    chat: \"دردشة\",\n    \"mobile-app\": \"AnythingLLM Mobile\",\n    \"community-hub\": {\n      title: \"مركز المجتمع\",\n      trending: \"استكشف الاتجاهات الرائجة\",\n      \"your-account\": \"حسابك\",\n      \"import-item\": \"استيراد العنصر\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"مرحبا في\",\n      \"placeholder-username\": \"اسم المستعمِل\",\n      \"placeholder-password\": \"كلمة المرور\",\n      login: \"تسجيل الدخول\",\n      validating: \"جاري التحقق...\",\n      \"forgot-pass\": \"هل نسيت كلمة المرور\",\n      reset: \"إعادة الضبط\",\n    },\n    \"sign-in\": \"تسجيل الدخول إلى حساب {{appName}}.\",\n    \"password-reset\": {\n      title: \"إعادة تعيين كلمة المرور\",\n      description:\n        \"قم بإدخال المعلومات اللازمة أدناه لإعادة تعيين كلمة المرور الخاصة بك.\",\n      \"recovery-codes\": \"رموز الاسترداد\",\n      \"back-to-login\": \"العودة إلى تسجيل الدخول\",\n    },\n  },\n  \"new-workspace\": {\n    title: \"مساحة عمل جديدة\",\n    placeholder: \"مساحتي للعمل\",\n  },\n  \"workspaces—settings\": {\n    general: \"الإعدادات العامة\",\n    chat: \"إعدادات المحادثة\",\n    vector: \"قاعدة بيانات المتجهات\",\n    members: \"أعضاء\",\n    agent: \"تكوين الوكيل\",\n  },\n  general: {\n    vector: {\n      title: \"عدد المتجهات\",\n      description:\n        \"العدد الإجمالي للمتجهات في قاعدة بيانات المتجهات الخاصة بك.\",\n    },\n    names: {\n      description: \"سيؤدي هذا فقط إلى تغيير اسم العرض لمساحة العمل الخاصة بك.\",\n    },\n    message: {\n      title: \"رسائل المحادثة المقترحة\",\n      description:\n        \" تخصيص الرسائل التي سيتم اقتراحها لمستعملي مساحة العمل الخاصة بك.\",\n      add: \"إضافة رسالة جديدة\",\n      save: \"حفظ الرسائل\",\n      heading: \"اشرح لي\",\n      body: \"فوائد برنامج إيني ثينك إلْلْمْ\",\n    },\n    delete: {\n      title: \"حذف مساحة العمل\",\n      description:\n        \"احذف مساحة العمل هذه وكل بياناتها. سيؤدي هذا إلى حذف مساحة العمل لجميع المستخدمين.\",\n      delete: \"حذف مساحة العمل\",\n      deleting: \"حذف مساحة العمل...\",\n      \"confirm-start\": \"أنت على وشكِ حذف كامل\",\n      \"confirm-end\":\n        \"لمساحة العمل. سيؤدي هذا إلى إزالة جميع تضمينات المتجهات في قاعدة بيانات المتجهات الخاصة بك.\\n\\nستظل ملفات المصدر الأصلية دون مساس. هذا الإجراء لا رجعة فيه.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"موفر نموذج التعلم العميق لمساحة العمل\",\n      description:\n        \"موفر نموذج التعلم العميق المحدد والنموذج الذي سيتم استخدامه لمساحة العمل هذه. من الوهلة الأولى، يستخدم موفر نموذج التعلم العميق هذا مع إعدادات النظام.\",\n      search: \"البحث عن كل مُوفري نماذج التعلم العميق\",\n    },\n    model: {\n      title: \"نموذج محادثة مساحة العمل\",\n      description:\n        \"نموذج المحادثة المحدد الذي سيتم استخدامه لمساحة العمل هذه. إذا كان غير محدد، فسيتم استخدام نموذج التعلم العميق الافتراضي للنظام.\",\n    },\n    mode: {\n      title: \"وضع المحادثة\",\n      chat: {\n        title: \"المحادثة\",\n        description:\n          'سيوفر إجابات بناءً على المعرفة العامة للنموذج اللغوي الكبير، بالإضافة إلى سياق المستندات.<br />ستحتاج إلى استخدام الأمر \"@agent\" لاستخدام الأدوات.',\n      },\n      query: {\n        title: \"استعلام\",\n        description:\n          'سيوفر الإجابات <b>فقط</b> إذا تم العثور على سياق الوثيقة.<br />ستحتاج إلى استخدام الأمر \"@agent\" لاستخدام الأدوات.',\n      },\n      automatic: {\n        title: \"سيارة\",\n        description:\n          'سيتم استخدام الأدوات تلقائيًا إذا كان النموذج ومزود الخدمة يدعمان استدعاء الأدوات الأصلية. إذا لم يتم دعم الأدوات الأصلية، فستحتاج إلى استخدام الأمر \"@agent\" لاستخدام الأدوات.',\n      },\n    },\n    history: {\n      title: \"سجل المحادثة\",\n      \"desc-start\":\n        \"عدد المحادثات السابقة التي سيتم تضمينها في رد الذاكرة قصيرة المدى.\",\n      recommend: \"الموصى به 20.\",\n      \"desc-end\":\n        \"من المرجح أن يؤدي أي رقم أكبر من 45 إلى فشل مستمر في المحادثة اعتمادًا على حجم الرسالة.\",\n    },\n    prompt: {\n      title: \"النداء\",\n      description:\n        \"النداء التي سيتم استخدامه في مساحة العمل هذه. حدد السياق والتعليمات للذكاء الاصطناعي للاستجابة. يجب عليك تقديم نداء مصمم بعناية حتى يتمكن الذكاء الاصطناعي من إنشاء استجابة دقيقة وذات صلة.\",\n      history: {\n        title: \"سجل تفاعلات النظام\",\n        clearAll: \"مسح الكل\",\n        noHistory: \"لا يوجد سجل تاريخي للنظام.\",\n        restore: \"استعادة\",\n        delete: \"حذف\",\n        deleteConfirm:\n          \"هل أنت متأكد من أنك تريد حذف هذا العنصر من سجل الأنشطة؟\",\n        clearAllConfirm:\n          \"هل أنت متأكد من أنك تريد مسح كل التاريخ؟ لا يمكن التراجع عن هذه العملية.\",\n        expand: \"وسّع\",\n        publish: \"نشر في مركز المجتمع\",\n      },\n    },\n    refusal: {\n      title: \"الرد على رفض وضعية الاستعلام\",\n      \"desc-start\": \"عندما تكون في\",\n      query: \"استعلام\",\n      \"desc-end\":\n        \"وضعٍية ترغب في إرجاع رفض آخر مناسب عندما لا يتم العثور على السياق.\",\n      \"tooltip-title\": \"لماذا أرى هذا؟\",\n      \"tooltip-description\":\n        \"أنت في وضع الاستعلام، والذي يستخدم فقط المعلومات الموجودة في مستنداتك. انتقل إلى وضع الدردشة لإجراء محادثات أكثر مرونة، أو انقر هنا لزيارة وثائقنا لمعرفة المزيد عن أوضاع الدردشة.\",\n    },\n    temperature: {\n      title: \"حرارة نموذج التعلم العميق\",\n      \"desc-start\":\n        'يتحكم هذا الإعداد في مدى \"الإبداع\" الذي ستكون عليه إجابات نموذج التعلم العميق.',\n      \"desc-end\":\n        \"كلما زاد العدد كلما كان الإبداع أكبر. بالنسبة لبعض النماذج، قد يؤدي هذا إلى استجابات غير منسجمة عند ضبطها على رقم مرتفع للغاية.\",\n      hint: \"لدى معظم نماذج التعلم العميق مجالات مقبولة مختلفة من القيم الصالحة. استشر موفر نموذج التعلم العميق الخاص بك للحصول على هذه المعلومات.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"معرف قاعدة بيانات المتجهة\",\n    snippets: {\n      title: \"الحد الأقصى لمقتطفات السياق\",\n      description:\n        \"يتحكم هذا الإعداد في الحد الأقصى لعدد مقتطفات السياق التي سيتم إرسالها إلى نموذج التعلم العميق لكل محادثة أو استعلام.\",\n      recommend: \"الموصى به: 4\",\n    },\n    doc: {\n      title: \"عتبة تشابه المستند\",\n      description:\n        \"الحد الأدنى لدرجة التشابه المطلوبة لاعتبار المصدر مرتبطًا بالمحادثة. وكلما زاد الرقم، كلما كان المصدر أكثر تشابهًا بالمحادثة.\",\n      zero: \"لا قيد\",\n      low: \"منخفضة (درجة التشابه ≥ .25)\",\n      medium: \"متوسطة ​​(درجة التشابه ≥ .50)\",\n      high: \"عالية (درجة التشابه ≥ .75)\",\n    },\n    reset: {\n      reset: \"إعادة تعيين قاعدة بيانات المتجهات\",\n      resetting: \"مسح المتجهات...\",\n      confirm:\n        \"أنت على وشك إعادة تعيين قاعدة بيانات المتجهات الخاصة بمساحة العمل هذه. سيؤدي هذا إلى إزالة جميع تضمينات المتجهات المضمنة حاليًا.\\n\\nستظل ملفات المصدر الأصلية دون مساس. هذا الإجراء لا رجعة فيه.\",\n      error: \"تعذرت إعادة تعيين قاعدة بيانات متجهة مساحة العمل!\",\n      success: \"تم إعادة تعيين قاعدة بيانات متجهة مساحة العمل!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"يعتمد أداء نماذج التعلم العميق التي لا تدعم صراحةً استدعاء الأدوات بشكل كبير على قدرات النموذج ودقته. قد تكون بعض القدرات محدودة أو غير وظيفية.\",\n    provider: {\n      title: \"موفر نموذج التعلم العميق لوكيل مساحة العمل\",\n      description:\n        \"موفر نموذج التعلم العميق والنموذج المحدد الذي سيتم استخدامه لوكيل الخاص بمساحة العمل هذه.\",\n    },\n    mode: {\n      chat: {\n        title: \"نموذج محادثة وكيل مساحة العمل\",\n        description:\n          \"نموذج المحادثة المحدد الذي سيتم استخدامه لوكيل الخاص بمساحة العمل هذه.\",\n      },\n      title: \"نموذج وكيل مساحة العمل\",\n      description:\n        \"نموذج نموذج التعلم العميق المحدد الذي سيتم استخدامه لوكيل الخاص بمساحة العمل هذه.\",\n      wait: \"-- في انتظار النماذج --\",\n    },\n    skill: {\n      rag: {\n        title: \"التوليد المعزز بالاسترجاع والذاكرة طويلة المدى\",\n        description:\n          'اسمح للوكيل بالاستفادة من مستنداتك المحلية للإجابة على استعلام أو اطلب من الوكيل \"تذكر\" أجزاء من المحتوى لاسترجاعها في الذاكرة طويلة المدى.',\n      },\n      view: {\n        title: \"عرض وتلخيص المستندات\",\n        description:\n          \"السماح للوكيل بإدراج وتلخيص محتوى ملفات مساحة العمل المضمنة حاليًا.\",\n      },\n      scrape: {\n        title: \"جمع محتوى المواقع الإلكترونية\",\n        description: \"السماح للوكيل بزيارة مواقع الويب وجمع محتواها.\",\n      },\n      generate: {\n        title: \"إنشاء المخططات البيانية\",\n        description:\n          \"تمكين الوكيل الافتراضي لإنشاء أنواع مختلفة من المخططات من البيانات المقدمة أو المعطاة في المحادثة.\",\n      },\n      save: {\n        title: \"إنشاء الملفات وحفظها في المتصفح\",\n        description:\n          \"تمكين الوكيل الافتراضي من إنشاء الملفات والكتابة عليها وحفظها و تنزيلها في متصفحك.\",\n      },\n      web: {\n        title: \"البحث والتصفح المباشر على الويب\",\n        description:\n          \"اسمح لمسؤولك بالبحث على الإنترنت للإجابة على أسئلتك من خلال الاتصال بمزود خدمة البحث على الإنترنت (SERP).\",\n      },\n      sql: {\n        title: \"موصل SQL\",\n        description:\n          \"اسمح لمسؤولك بالاستفادة من SQL للإجابة على أسئلتك من خلال الاتصال بمقدمي قواعد البيانات المختلفة.\",\n      },\n      default_skill:\n        \"افتراضيًا، يتم تفعيل هذه الميزة، ولكن يمكنك تعطيلها إذا لم ترغب في أن تكون متاحة للممثل.\",\n    },\n    mcp: {\n      title: \"خوادم نظام MCP\",\n      \"loading-from-config\": \"تحميل خوادم MCP من ملف التكوين\",\n      \"learn-more\": \"اعرف المزيد عن خوادم MCP.\",\n      \"no-servers-found\": \"لم يتم العثور على أي خوادم MCP.\",\n      \"tool-warning\":\n        \"لتحقيق أفضل أداء، ضع في اعتبارك تعطيل الأدوات غير الضرورية للحفاظ على السياق.\",\n      \"stop-server\": \"أوقف خادم MCP\",\n      \"start-server\": \"ابدأ خادم MCP\",\n      \"delete-server\": \"حذف خادم MCP\",\n      \"tool-count-warning\":\n        \"يحتوي هذا خادم MCP على <b> أدوات مُفعّلة</b> والتي ستستهلك السياق في كل محادثة. <br /> ضع في اعتبارك تعطيل الأدوات غير المرغوب فيها لتوفير السياق.\",\n      \"startup-command\": \"أمر البدء\",\n      command: \"الأمر\",\n      arguments: \"حجج\",\n      \"not-running-warning\":\n        \"هذا الخادم الخاص بـ MCP غير قيد التشغيل - قد يكون متوقفًا أو يواجه مشكلة عند التشغيل.\",\n      \"tool-call-arguments\": \"وسائط استدعاء الدالة\",\n      \"tools-enabled\": \"الأدوات مفعّلة\",\n    },\n    settings: {\n      title: \"إعدادات مهارات الوكيل\",\n      \"max-tool-calls\": {\n        title: \"الحد الأقصى لعدد طلبات الأدوات في الاستجابة.\",\n        description:\n          \"أقصى عدد من الأدوات التي يمكن للممثل (الوكيل) ربطها لإنشاء استجابة واحدة. وهذا يمنع استدعاءات الأدوات غير المنضبطة والحلقات اللانهائية.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"اختيار المهارات الذكية\",\n        \"beta-badge\": \"بيتا\",\n        description:\n          \"تمكين استخدام أدوات غير محدودة وتقليل استخدام رموز القطع بنسبة تصل إلى 80٪ لكل استعلام - يقوم AnythingLLM تلقائيًا باختيار المهارات المناسبة لكل طلب.\",\n        \"max-tools\": {\n          title: \"أدوات ماكس\",\n          description:\n            \"الحد الأقصى لعدد الأدوات التي يمكن اختيارها لكل استعلام. ونوصي بتعيين هذه القيمة على قيم أعلى بالنسبة للنماذج ذات السياق الأكبر.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"محادثات مساحة العمل\",\n    description:\n      \"هذه هي جميع المحادثات والرسائل المسجلة التي أرسلها المستعملون مرتبة حسب تاريخ إنشائها.\",\n    export: \"تصدير\",\n    table: {\n      id: \"معرف\",\n      by: \"أرسلت بواسطة\",\n      workspace: \"مساحة العمل\",\n      prompt: \"نداء\",\n      response: \"استجابة\",\n      at: \"أرسلت في\",\n    },\n  },\n  api: {\n    title: \" مفاتيح واجهة برمجة التطبيقات.\",\n    description:\n      \"تسمح مفاتيح واجهة برمجة التطبيقات  لحامليها بالوصول إلى مثيل إني ثينك إلْلْم هذا وإدارته برمجيًا.\",\n    link: \"اقرأ وثائق واجهة برمجة التطبيقات .\",\n    generate: \"إنشاء مفتاح واجهة برمجة التطبيقات الجديد\",\n    table: {\n      key: \"مفتاح واجهة برمجة التطبيقات\",\n      by: \"تم الإنشاء بواسطة\",\n      created: \"تم إنشاؤها\",\n    },\n  },\n  llm: {\n    title: \"تفضيل نموذج التعلم العميق\",\n    description:\n      \"هذه هي بيانات الاعتماد والإعدادات الخاصة بنموذج التعلم العميق للمحادثة وموفر التضمين المفضلين لديك . من المهم أن تكون هذه المفاتيح حديثة وصحيحة وإلا فلن يعمل برنامج إني ثينك إلْلْم بشكل صحيح.\",\n    provider: \"موفر نموذج التعلم العميق\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"نقطة نهاية الخدمة في Azure\",\n        api_key: \"مفتاح واجهة برمجة التطبيقات\",\n        chat_deployment_name: \"اسم نشر الدردشة\",\n        chat_model_token_limit: \"حدود عدد الرموز المميزة في نموذج الدردشة\",\n        model_type: \"نوع النموذج\",\n        default: \"افتراضي\",\n        reasoning: \"المنطق\",\n        model_type_tooltip:\n          'إذا كان نظامك يعتمد على نموذج استدلال (مثل o1، o1-mini، o3-mini، إلخ)، فيرجى تعيين هذا الخيار على \"الاستدلال\". وإلا، فقد تفشل طلبات الدردشة الخاصة بك.',\n      },\n    },\n  },\n  transcription: {\n    title: \"تفضيل نموذج النسخ\",\n    description:\n      \"هذه هي بيانات الاعتماد والإعدادات الخاصة بموفر نموذج النسخ المفضل لديك. من المهم أن تكون هذه المفاتيح حديثة وصحيحة وإلا فلن يتم نسخ ملفات الوسائط والصوت.\",\n    provider: \"موفر النسخ\",\n    \"warn-start\":\n      \"يمكن أن يؤدي استخدام نموذج الهمس المحلي على الأجهزة ذات ذاكرة الوصول العشوائي أو وحدة المعالجة المركزية المحدودة إلى تعطيل إني ثينك إلْلْم عند معالجة ملفات الوسائط.\",\n    \"warn-recommend\":\n      \"نوصي بذاكرة وصول عشوائي بسعة 2 جيجابايت على الأقل وتحميل ملفات أقل من 10 ميجا بايت.\",\n    \"warn-end\": \"سيتم تنزيل النموذج المدمج تلقائيًا عند الاستخدام الأول.\",\n  },\n  embedding: {\n    title: \"تفضيل التضمين\",\n    \"desc-start\":\n      \"عند استخدام نموذج تعلم عميق لا يدعم محرك التضمين أصلاً - قد تحتاج إلى تحديد بيانات الاعتماد بالإضافة إلى ذلك لتضمين النص.\",\n    \"desc-end\":\n      \"التضمين هو عملية تحويل النص إلى متجهات. هذه البيانات مطلوبة لتحويل ملفاتك ومطالباتك إلى تنسيق يمكن لـ إني ثينك إلْلْمْ استخدامه للمعالجة.\",\n    provider: {\n      title: \"موفر التضمين\",\n    },\n  },\n  text: {\n    title: \"تقسيم النص وتفضيلات التقطيع\",\n    \"desc-start\":\n      \"في بعض الأحيان، قد ترغب في تغيير الطريقة الافتراضية التي يتم بها تقسيم المستندات الجديدة وتقطيعها قبل إدراجها في قاعدة بيانات المتجهة الخاصة بك.\",\n    \"desc-end\":\n      \"يجب عليك فقط تعديل هذا الإعداد إذا كنت تفهم كيفية عمل تقسيم النص وتأثيراته الجانبية.\",\n    size: {\n      title: \"حجم قطعة النص\",\n      description:\n        \"هذا هو الحد الأقصى لطول الأحرف التي يمكن أن تكون موجودة في متجهة واحدة.\",\n      recommend: \"الحد الأقصى لطول نموذج التضمين هو\",\n    },\n    overlap: {\n      title: \"تداخل قطعة النص\",\n      description:\n        \"هذا هو الحد الأقصى لتداخل الأحرف الذي يحدث أثناء تقطيع قطعتي نص متجاورتين.\",\n    },\n  },\n  vector: {\n    title: \"قاعدة بيانات المتجهة\",\n    description:\n      \"هذه هي بيانات الاعتماد والإعدادات الخاصة بكيفية عمل مثيل إني ثينك إلْلْمْ الخاص بك. من المهم أن تكون هذه المفاتيح حالية وصحيحة.\",\n    provider: {\n      title: \"موفر قاعدة بيانات المتجهة\",\n      description: \"ليست هناك حاجة تعيين إعدادات لانسديبي .\",\n    },\n  },\n  embeddable: {\n    title: \"أدوات المحادثة القابلة للتضمين\",\n    description:\n      \"تعتبر أدوات المحادثة القابلة للتضمين عبارة عن واجهات محادثة عامة مرتبطة بمساحة عمل واحدة. تتيح لك هذه الأدوات إنشاء مساحات عمل يمكنك بعد ذلك نشرها .\",\n    create: \"إنشاء تضمين\",\n    table: {\n      workspace: \"مساحة العمل\",\n      chats: \"المحادثات المرسلة\",\n      active: \"المجالات النشطة\",\n      created: \"تم إنشاؤه\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"تضمين المحادثات\",\n    export: \"تصدير\",\n    description:\n      \"هذه هي جميع المحادثات والرسائل المسجلة من أي تضمين قمت بنشره.\",\n    table: {\n      embed: \"تضمين\",\n      sender: \"مُرسِل\",\n      message: \"رسالة\",\n      response: \"استجابة\",\n      at: \"أرسلت في\",\n    },\n  },\n  event: {\n    title: \"سجلات الحدث\",\n    description:\n      \"عرض كافة الإجراءات والأحداث التي تحدث في هذا المثيل للمراقبة.\",\n    clear: \"محو سجلات الأحداث\",\n    table: {\n      type: \"نوع الحدث\",\n      user: \"مستعمِل\",\n      occurred: \"حدث في\",\n    },\n  },\n  privacy: {\n    title: \"الخصوصية ومعالجة البيانات\",\n    description:\n      \"هذا هو التكوين الخاص بك لكيفية تعامل موفري الطرف الثالث المتصلين و إني ثينك إلْلْمْ مع بياناتك.\",\n    anonymous: \"تم تمكين القياس المستتر عن بعد \",\n  },\n  connectors: {\n    \"search-placeholder\": \"اتصالات البيانات\",\n    \"no-connectors\": \"لم يتم العثور على أي اتصالات بيانات.\",\n    github: {\n      name: \"مستودع GitHub\",\n      description:\n        \"استورد مستودع GitHub بأكمله (سواء كان عامًا أو خاصًا) بنقرة واحدة.\",\n      URL: \"عنوان مستودع GitHub\",\n      URL_explained: \"عنوان مستودع GitHub الذي ترغب في جمعه.\",\n      token: \"رمز الوصول إلى GitHub\",\n      optional: \"اختياري\",\n      token_explained: \"رمز الوصول لمنع تحديد السرعة.\",\n      token_explained_start: \"بدون مساعدة.\",\n      token_explained_link1: \"رمز الوصول الشخصي\",\n      token_explained_middle:\n        \"، قد تحدد واجهة برمجة التطبيقات الخاصة بـ GitHub عدد الملفات التي يمكن جمعها بسبب قيود السرعة. يمكنك\",\n      token_explained_link2: \"إنشاء رمز وصول مؤقت\",\n      token_explained_end: \"لتجنب هذه المشكلة.\",\n      ignores: \"يتجاهل الملف\",\n      git_ignore:\n        \"قم بإدراج قائمة بتنسيق .gitignore لتجاهل الملفات المحددة أثناء عملية الجمع. اضغط على مفتاح الإدخال بعد كل إدخال ترغب في حفظه.\",\n      task_explained:\n        \"بمجرد الانتهاء، ستكون جميع الملفات متاحة لإدراجها في مساحات العمل في أداة اختيار المستندات.\",\n      branch: \"المجلد الذي ترغب في استرداد الملفات منه.\",\n      branch_loading: \"-- تحميل الفروع المتاحة --\",\n      branch_explained: \"المجلد الذي ترغب في استرداد الملفات منه.\",\n      token_information:\n        \"بسبب قيود معدل الوصول إلى واجهة برمجة التطبيقات العامة الخاصة بـ GitHub، لن يتمكن هذا الموصل من جمع الملفات ذات المستوى الأعلى فقط.\",\n      token_personal: \"احصل على رمز وصول شخصي مجاني مع حساب GitHub هنا.\",\n    },\n    gitlab: {\n      name: \"مستودع GitLab\",\n      description:\n        \"استورد مستودع GitLab بالكامل، سواء كان عامًا أو خاصًا، بنقرة واحدة.\",\n      URL: \"عنوان مستودع GitLab\",\n      URL_explained: \"عنوان مستودع GitLab الذي ترغب في جمعه.\",\n      token: \"رمز الوصول إلى GitLab\",\n      optional: \"اختياري\",\n      token_description:\n        \"حدد الكيانات الإضافية التي تريد استردادها من واجهة برمجة التطبيقات الخاصة بـ GitLab.\",\n      token_explained_start: \"بدون مساعدة.\",\n      token_explained_link1: \"رمز الوصول الشخصي\",\n      token_explained_middle:\n        \"، قد تحدد واجهة برمجة التطبيقات الخاصة بـ GitLab عدد الملفات التي يمكن جمعها بسبب قيود السرعة. يمكنك\",\n      token_explained_link2: \"إنشاء رمز وصول مؤقت\",\n      token_explained_end: \"لتجنب هذه المشكلة.\",\n      fetch_issues: \"استرجاع المشكلات بصيغة المستندات\",\n      ignores: \"يتجاهل الملف\",\n      git_ignore:\n        \"قم بإدراج قائمة بتنسيق .gitignore لتجاهل الملفات المحددة أثناء عملية الجمع. اضغط على مفتاح الإدخال بعد كل إدخال ترغب في حفظه.\",\n      task_explained:\n        \"بمجرد الانتهاء، ستكون جميع الملفات متاحة لإدراجها في مساحات العمل في أداة اختيار المستندات.\",\n      branch: \"المجلد الذي ترغب في استرداد الملفات منه\",\n      branch_loading: \"-- تحميل الفروع المتاحة --\",\n      branch_explained: \"المجلد الذي ترغب في استرداد الملفات منه.\",\n      token_information:\n        \"بسبب قيود معدل الوصول إلى واجهة برمجة التطبيقات العامة لـ GitLab، لن يتمكن هذا الموصل من البيانات من جمع الملفات ذات المستوى الأعلى فقط في المستودع.\",\n      token_personal: \"احصل على رمز وصول شخصي مجاني مع حساب GitLab هنا.\",\n    },\n    youtube: {\n      name: \"نص فيديو يوتيوب\",\n      description: \"استيراد نص فيديو يوتيوب بأكمله من رابط.\",\n      URL: \"عنوان الفيديو على يوتيوب\",\n      URL_explained_start:\n        \"أدخل عنوان URL لأي مقطع فيديو على يوتيوب للحصول على نص الفيديو. يجب أن يحتوي الفيديو على\",\n      URL_explained_link: \"الترجمة المصاحبة\",\n      URL_explained_end: \"متاح.\",\n      task_explained:\n        \"بمجرد الانتهاء، سيكون النص متاحًا لإدراجه في مساحات العمل في أداة اختيار المستندات.\",\n    },\n    \"website-depth\": {\n      name: \"أداة لجمع الروابط بكميات كبيرة\",\n      description:\n        \"استخراج محتوى موقع ويب وجميع الروابط الفرعية حتى مستوى معين.\",\n      URL: \"عنوان الموقع الإلكتروني\",\n      URL_explained: \"عنوان الموقع الإلكتروني الذي ترغب في استخراجه.\",\n      depth: \"عمق الغوص\",\n      depth_explained:\n        \"هذا هو عدد الروابط التي يجب على العامل اتباعها من عنوان URL الأصلي.\",\n      max_pages: \"الحد الأقصى لعدد الصفحات\",\n      max_pages_explained: \"الحد الأقصى لعدد الروابط التي يجب استخراجها.\",\n      task_explained:\n        \"بمجرد الانتهاء، سيكون المحتوى الذي تم استخراجه متاحًا لإدراجه في مساحات العمل في أداة اختيار المستندات.\",\n    },\n    confluence: {\n      name: \"التلاقي\",\n      description: \"استيراد صفحة كاملة من Confluence بنقرة واحدة.\",\n      deployment_type: \"نوع نشر التطبيق\",\n      deployment_type_explained:\n        \"حدد ما إذا كان مثيل Confluence الخاص بك مُستضافًا على سحابة Atlassian أم أنه مُستضاف ذاتيًا.\",\n      base_url: \"عنوان قاعدة البيانات\",\n      base_url_explained: \"هذا هو عنوان URL الأساسي لمساحتك في Confluence.\",\n      space_key: \"مفتاح مساحة التجمع\",\n      space_key_explained:\n        \"هذا هو مفتاح المساحات الخاص بمثيل Confluence الخاص بك، والذي سيتم استخدامه. وعادةً ما يبدأ بـ ~\",\n      username: \"اسم المستخدم\",\n      username_explained: \"اسم المستخدم الخاص بك في Confluence\",\n      auth_type: \"نوع المصادقة:\",\n      auth_type_explained:\n        \"حدد نوع المصادقة الذي ترغب في استخدامه للوصول إلى صفحات Confluence الخاصة بك.\",\n      auth_type_username: \"اسم المستخدم ورمز الوصول\",\n      auth_type_personal: \"رمز الوصول الشخصي\",\n      token: \"رمز الوصول إلى منطقة التجمع\",\n      token_explained_start:\n        \"يجب عليك تقديم رمز وصول للمصادقة. يمكنك إنشاء رمز وصول.\",\n      token_explained_link: \"هنا\",\n      token_desc: \"رمز الوصول للمصادقة\",\n      pat_token: \"رمز الوصول الشخصي الخاص بـ Confluence\",\n      pat_token_explained: \"رمز الوصول الشخصي الخاص بك.\",\n      task_explained:\n        \"بمجرد الانتهاء، سيتم توفير محتوى الصفحة للاستخدام في تضمينها في مساحات العمل في أداة اختيار المستندات.\",\n      bypass_ssl: \"تجاوز التحقق من شهادة SSL\",\n      bypass_ssl_explained:\n        \"قم بتمكين هذا الخيار لتجاوز عملية التحقق من شهادة SSL لبيئات Confluence المستضافة ذاتيًا باستخدام شهادة موقعة ذاتيًا.\",\n    },\n    manage: {\n      documents: \"وثائق\",\n      \"data-connectors\": \"وصلات البيانات\",\n      \"desktop-only\":\n        \"تتوفر هذه الإعدادات فقط على جهاز كمبيوتر مكتبي. يرجى الوصول إلى هذه الصفحة على جهاز الكمبيوتر الخاص بك لمواصلة العمل.\",\n      dismiss: \"ارفض\",\n      editing: \"تحرير\",\n    },\n    directory: {\n      \"my-documents\": \"وثائقي\",\n      \"new-folder\": \"مجلد جديد\",\n      \"search-document\": \"البحث عن المستند\",\n      \"no-documents\": \"لا توجد مستندات.\",\n      \"move-workspace\": \"انتقل إلى مساحة العمل\",\n      \"delete-confirmation\":\n        \"هل أنت متأكد من أنك تريد حذف هذه الملفات والمجلدات؟\\nسيؤدي ذلك إلى إزالة الملفات من النظام وإزالتها تلقائيًا من أي مساحات عمل موجودة.\\nهذا الإجراء غير قابل للتراجع.\",\n      \"removing-message\":\n        \"حذف {{count}} مستندًا و {{folderCount}} مجلدًا. يرجى الانتظار.\",\n      \"move-success\": \"تم نقل {{count}} مستندات بنجاح.\",\n      no_docs: \"لا توجد مستندات.\",\n      select_all: \"حدد الكل\",\n      deselect_all: \"إلغاء التحديد الكل\",\n      remove_selected: \"حذف المحدد\",\n      costs: \"*تكلفة ثابتة لإنشاء التمثيلات\",\n      save_embed: \"حفظ و تضمين\",\n      \"total-documents_one\": \"{{count}}\",\n      \"total-documents_other\": \"{{count}} المستندات\",\n    },\n    upload: {\n      \"processor-offline\": \"غير متاح\",\n      \"processor-offline-desc\":\n        \"لا يمكننا تحميل ملفاتك في الوقت الحالي لأن معالج المستندات غير متصل بالإنترنت. يرجى المحاولة مرة أخرى لاحقًا.\",\n      \"click-upload\": \"انقر لتحميل أو اسحب وأفلت\",\n      \"file-types\":\n        \"يدعم ملفات النصوص، وملفات CSV، وجداول البيانات، وملفات الصوت، وغيرها!\",\n      \"or-submit-link\": \"أو قم بإرسال رابط\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"جاري الاسترجاع...\",\n      \"fetch-website\": \"احصل على موقع الويب\",\n      \"privacy-notice\":\n        \"سيتم تحميل هذه الملفات إلى معالج المستندات الذي يعمل على هذه نسخة من AnythingLLM. هذه الملفات لا يتم إرسالها أو مشاركتها مع طرف ثالث.\",\n    },\n    pinning: {\n      what_pinning: 'ما هو عمل \"تثبيت المستندات\"؟',\n      pin_explained_block1:\n        \"عندما تقوم بإرفاق مستند في AnythingLLM، سنقوم بإدخال محتوى المستند بالكامل في نافذة المطالبة الخاصة بـ LLM الخاص بك، وذلك حتى يتمكن LLM من فهم المحتوى بالكامل.\",\n      pin_explained_block2:\n        \"يعمل هذا بشكل أفضل مع **نماذج ذات سياق كبير** أو ملفات صغيرة ولكنها ضرورية لأساس المعرفة الخاص بها.\",\n      pin_explained_block3:\n        'إذا لم تحصل على الإجابات التي ترغب بها بشكل افتراضي من AnythingLLM، فإن استخدام ميزة \"التثبيت\" هو طريقة رائعة للحصول على إجابات ذات جودة أعلى في نقرة واحدة.',\n      accept: \"حسناً، فهمت.\",\n    },\n    watching: {\n      what_watching: \"ما الذي يفعله مشاهدة فيلم وثائقي؟\",\n      watch_explained_block1:\n        \"عندما **تشاهد** مستندًا في AnythingLLM، سيتم **مزامنة** محتوى المستند تلقائيًا من مصدره الأصلي على فترات منتظمة. وهذا سيؤدي إلى تحديث المحتوى تلقائيًا في كل مساحة عمل حيث يتم إدارة هذا الملف.\",\n      watch_explained_block2:\n        \"هذه الميزة تدعم حاليًا المحتوى القائم على الإنترنت، ولن تكون متاحة للمستندات التي يتم تحميلها يدويًا.\",\n      watch_explained_block3_start:\n        \"يمكنك إدارة المستندات التي يتم عرضها من خلال.\",\n      watch_explained_block3_link: \"مدير الملفات\",\n      watch_explained_block3_end: \"نظرة عامة.\\n\\n\\nنظرة عامة.\",\n      accept: \"حسناً، فهمت.\",\n    },\n    obsidian: {\n      vault_location: \"موقع الخزانة\",\n      vault_description:\n        'حدد مجلد \"Obsidian\" الخاص بك لاستيراد جميع الملاحظات وعلاقاتها.',\n      selected_files: \"تم العثور على {{count}} ملفات Markdown.\",\n      importing: \"استيراد الخزانة...\",\n      import_vault: \"استيراد منصة Vault\",\n      processing_time: \"قد يستغرق ذلك بعض الوقت، اعتمادًا على حجم الخزانة.\",\n      vault_warning:\n        \"لتجنب أي تعارضات، تأكد من أن مجلد Obsidian الخاص بك ليس مفتوحًا حاليًا.\",\n    },\n  },\n  chat_window: {\n    send_message: \"أرسل رسالة\",\n    attach_file: \"أرفق ملفًا بهذا الدردشة\",\n    text_size: \"تغيير حجم النص.\",\n    microphone: \"اذكر طلبك.\",\n    send: \"أرسل رسالة فورية إلى مساحة العمل\",\n    attachments_processing: \"جارٍ معالجة المرفقات. يرجى الانتظار...\",\n    tts_speak_message: \"رسالة TTS Speak\",\n    copy: \"انسخ\",\n    regenerate: \"إعادة إنشاء\",\n    regenerate_response: \"أعد الرد\",\n    good_response: \"رد جيد\",\n    more_actions: \"إجراءات إضافية\",\n    fork: \"شوكة\",\n    delete: \"حذف\",\n    cancel: \"إلغاء\",\n    edit_prompt: \"اقتراح التحرير\",\n    edit_response: \"عدّل الرد\",\n    preset_reset_description: \"امسح سجل الدردشة الخاص بك وابدأ محادثة جديدة\",\n    add_new_preset: \"إضافة إعداد مسبق\",\n    command: \"أمر\",\n    your_command: \"أمرك\",\n    placeholder_prompt: \"هذا هو المحتوى الذي سيتم إدخاله أمام سؤالك.\",\n    description: \"وصف\",\n    placeholder_description: \"يستجيب ببيت شعر عن نماذج اللغة الكبيرة.\",\n    save: \"حفظ\",\n    small: \"صغير\",\n    normal: \"طبيعي\",\n    large: \"كبير\",\n    workspace_llm_manager: {\n      search: \"البحث عن مزودي نماذج اللغة الكبيرة\",\n      loading_workspace_settings: \"تحميل إعدادات مساحة العمل...\",\n      available_models: \"الموديلات المتاحة لـ {{provider}}\",\n      available_models_description: \"حدد نموذجًا للاستخدام في هذا المساحة.\",\n      save: \"استخدم هذا النموذج.\",\n      saving: \"تعيين النموذج كإعداد افتراضي للمساحة العملية...\",\n      missing_credentials: \"هذا المزود لا يمتلك المؤهلات اللازمة!\",\n      missing_credentials_description: \"انقر لإعداد بيانات الاعتماد\",\n    },\n    submit: \"إرسال\",\n    edit_info_user:\n      '\"إرسال\" يعيد إنشاء استجابة الذكاء الاصطناعي. \"حفظ\" يقوم بتحديث رسالتك فقط.',\n    edit_info_assistant: \"سيتم حفظ التغييرات مباشرة في هذا الرد.\",\n    see_less: \"اقرأ المزيد\",\n    see_more: \"عرض المزيد\",\n    tools: \"الأدوات\",\n    browse: \"تصفح\",\n    text_size_label: \"حجم النص\",\n    select_model: \"اختر الطراز\",\n    sources: \"مصادر\",\n    document: \"وثيقة\",\n    similarity_match: \"مباراة\",\n    source_count_one: \"{{count}}، المرجع\",\n    source_count_other: \"{{count}} المرجع\",\n    preset_exit_description: \"إيقاف الجلسة الحالية للمتصفح\",\n    add_new: \"أضف جديدًا\",\n    edit: \"تحرير\",\n    publish: \"نشر\",\n    stop_generating: \"توقف عن إنشاء رد\",\n    pause_tts_speech_message: \"توقف عن قراءة النص بصوت مسجل.\",\n    slash_commands: \"أوامر مختصرة\",\n    agent_skills: \"مهارات الوكيل\",\n    manage_agent_skills: \"إدارة مهارات الوكلاء\",\n    agent_skills_disabled_in_session:\n      'لا يمكن تعديل المهارات أثناء جلسة مع عامل. يجب عليك أولاً استخدام الأمر \"/exit\" لإنهاء الجلسة.',\n    start_agent_session: \"ابدأ جلسة الممثل\",\n    use_agent_session_to_use_tools:\n      \"يمكنك استخدام الأدوات المتاحة في الدردشة عن طريق بدء جلسة مع ممثل خدمة العملاء باستخدام الرمز '@agent' في بداية رسالتك.\",\n  },\n  profile_settings: {\n    edit_account: \"تحرير الحساب\",\n    profile_picture: \"صورة الملف الشخصي\",\n    remove_profile_picture: \"حذف صورة الملف الشخصي\",\n    username: \"اسم المستخدم\",\n    new_password: \"كلمة مرور جديدة\",\n    password_description: \"يجب أن يكون طول كلمة المرور 8 أحرف على الأقل.\",\n    cancel: \"إلغاء\",\n    update_account: \"تحديث الحساب\",\n    theme: \"تفضيلات الموضوع\",\n    language: \"اللغة المفضلة\",\n    failed_upload: \"فشل تحميل صورة الملف الشخصي: {{error}}\",\n    upload_success: \"تم تحميل صورة الملف الشخصي.\",\n    failed_remove: \"فشل إزالة صورة الملف الشخصي: {{error}}\",\n    profile_updated: \"تم تحديث الملف الشخصي.\",\n    failed_update_user: \"فشل تحديث المستخدم: {{error}}\",\n    account: \"حساب\",\n    support: \"الدعم\",\n    signout: \"تسجيل الخروج\",\n  },\n  customization: {\n    interface: {\n      title: \"تفضيلات واجهة المستخدم\",\n      description: \"حدد تفضيلات واجهة المستخدم الخاصة بـ AnythingLLM.\",\n    },\n    branding: {\n      title: \"التسويق بالعلامة التجارية ووضع العلامات التجارية\",\n      description:\n        \"قم بتخصيص نسخة AnythingLLM الخاصة بك باستخدام العلامات التجارية الخاصة بك.\",\n    },\n    chat: {\n      title: \"دردشة\",\n      description: \"حدد تفضيلات الدردشة الخاصة بك لـ AnythingLLM.\",\n      auto_submit: {\n        title: \"إرسال تلقائي للمدخلات الصوتية\",\n        description: \"إرسال تلقائي لإدخال الكلام بعد فترة من الصمت\",\n      },\n      auto_speak: {\n        title: \"ردود آلية\",\n        description: \"إجابات تلقائية من الذكاء الاصطناعي\",\n      },\n      spellcheck: {\n        title: \"تمكين التدقيق الإملائي\",\n        description: \"تمكين أو تعطيل التدقيق الإملائي في حقل إدخال الرسائل\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"الموضوع\",\n        description: \"حدد نظام الألوان المفضل لديك للتطبيق.\",\n      },\n      \"show-scrollbar\": {\n        title: \"إظهار شريط التمرير\",\n        description: \"تمكين أو تعطيل شريط التمرير في نافذة الدردشة.\",\n      },\n      \"support-email\": {\n        title: \"دعم البريد الإلكتروني\",\n        description:\n          \"حدد عنوان البريد الإلكتروني للدعم الذي يجب أن يكون متاحًا للمستخدمين عند الحاجة إلى المساعدة.\",\n      },\n      \"app-name\": {\n        title: \"اسم\",\n        description: \"حدد اسمًا يظهر في صفحة تسجيل الدخول لجميع المستخدمين.\",\n      },\n      \"display-language\": {\n        title: \"اللغة المعروضة\",\n        description:\n          \"حدد اللغة المفضلة لعرض واجهة مستخدم AnythingLLM - عند توفر الترجمات.\",\n      },\n      logo: {\n        title: \"شعار العلامة التجارية\",\n        description: \"قم بتحميل شعارك المخصص لعرضه على جميع الصفحات.\",\n        add: \"أضف شعارًا مخصصًا\",\n        recommended: \"الحجم الموصى به: 800 × 200\",\n        remove: \"احذف\",\n        replace: \"استبدل\",\n      },\n      \"browser-appearance\": {\n        title: \"مظهر المتصفح\",\n        description: \"خصص مظهر علامة التبويب والعنوان عند فتح التطبيق.\",\n        tab: {\n          title: \"العنوان\",\n          description: \"حدد عنوان علامة تبويب مخصصًا عند فتح التطبيق في متصفح.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description: \"استخدم أيقونة مخصصة لعلامة المتصفح.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"عناصر تذييل الشريط الجانبي\",\n        description:\n          \"خصص عناصر التذييل المعروضة في الجزء السفلي من الشريط الجانبي.\",\n        icon: \"رمز\",\n        link: \"رابط\",\n      },\n      \"render-html\": {\n        title: \"تحويل HTML إلى تنسيق نصي في الدردشة\",\n        description:\n          \"تقديم استجابات HTML في استجابات المساعد.\\nيمكن أن يؤدي ذلك إلى تحسين كبير في جودة الاستجابة، ولكنه قد يؤدي أيضًا إلى مخاطر أمنية محتملة.\",\n      },\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"إنشاء وكيل\",\n      editWorkspace: \"تعديل مساحة العمل\",\n      uploadDocument: \"تحميل مستند\",\n    },\n    greeting: \"كيف يمكنني مساعدتك اليوم؟\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"اختصارات لوحة المفاتيح\",\n    shortcuts: {\n      settings: \"فتح الإعدادات\",\n      workspaceSettings: \"فتح إعدادات مساحة العمل الحالية\",\n      home: \"اذهب إلى الصفحة الرئيسية\",\n      workspaces: \"إدارة مساحات العمل\",\n      apiKeys: \"إعدادات مفاتيح واجهة برمجة التطبيقات\",\n      llmPreferences: \"تفضيلات نموذج اللغة الكبيرة\",\n      chatSettings: \"إعدادات الدردشة\",\n      help: \"عرض مسرّعات لوحة المفاتيح\",\n      showLLMSelector: \"اختر مساحة العمل\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"نجاح!\",\n        success_description: \"تم نشر مطالبتك في نظامك على منصة المجتمع!\",\n        success_thank_you: \"شكراً لمشاركتكم في المجتمع!\",\n        view_on_hub: \"عرض على منصة المجتمع\",\n        modal_title: \"نص الإشعار\",\n        name_label: \"اسم\",\n        name_description: \"هذا هو اسم العرض الخاص بنظامك.\",\n        name_placeholder: \"تعليمات النظام الخاص بي\",\n        description_label: \"وصف\",\n        description_description:\n          \"هذا هو وصف لتعليمات النظام الخاصة بك. استخدم هذا لوصف الغرض من تعليمات النظام الخاصة بك.\",\n        tags_label: \"الوسوم\",\n        tags_description:\n          \"تُستخدم العلامات لتسمية مطالبتك في النظام لتسهيل البحث. يمكنك إضافة عدة علامات. الحد الأقصى لعدد العلامات هو 5. الحد الأقصى لعدد الأحرف في كل علامة هو 20 حرفًا.\",\n        tags_placeholder: \"أدخل النص واضغط على مفتاح الإدخال لإضافة العلامات\",\n        visibility_label: \"رؤية\",\n        public_description: \"تظهر إشعارات النظام العامة للجميع.\",\n        private_description: \"رسائل التذكير الخاصة مرئية فقط لك.\",\n        publish_button: \"نشر في مركز المجتمع\",\n        submitting: \"نشر...\",\n        prompt_label:\n          \"الرجاء تقديم معلومات حول كيفية الحصول على شهادة في مجال تكنولوجيا المعلومات.\",\n        prompt_description:\n          \"هذا هو الأمر المباشر الفعلي الذي سيتم استخدامه لتوجيه نموذج اللغة الكبير.\",\n        prompt_placeholder: \"أدخل تعليمات النظام هنا...\",\n      },\n      agent_flow: {\n        success_title: \"نجاح!\",\n        success_description: 'تم نشر \"Agent Flow\" الخاص بك في مركز المجتمع!',\n        success_thank_you: \"شكراً لمشاركتكم في المجتمع!\",\n        view_on_hub: \"عرض على منصة المجتمع\",\n        modal_title: \"مخطط تدفق الوكيل\",\n        name_label: \"الاسم\",\n        name_description: \"هذا هو اسم العرض الخاص بمسار الممثل.\",\n        name_placeholder: 'وكيلتي، \"فلو\"',\n        description_label: \"وصف\",\n        description_description:\n          \"هذا هو وصف لتدفق العمل الخاص بك. استخدم هذا لوصف الغرض من تدفق العمل الخاص بك.\",\n        tags_label: \"الوسوم\",\n        tags_description:\n          \"تُستخدم العلامات لتصنيف مسارات عملك لتسهيل البحث. يمكنك إضافة عدة علامات. الحد الأقصى لعدد العلامات هو 5. الحد الأقصى لعدد الأحرف في كل علامة هو 20 حرفًا.\",\n        tags_placeholder: \"أدخل النص واضغط على مفتاح الإدخال لإضافة العلامات\",\n        visibility_label: \"رؤية\",\n        submitting: \"نشر...\",\n        submit: \"نشر في مركز المجتمع\",\n        privacy_note:\n          \"يتم تحميل تدفقات البيانات دائمًا كخاصة لحماية أي بيانات حساسة. يمكنك تغيير مستوى الوصول في مركز المجتمع بعد النشر. يرجى التأكد من أن تدفقك لا يحتوي على أي معلومات حساسة أو خاصة قبل النشر.\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"يتطلب التحقق\",\n          description:\n            \"يجب عليك التحقق من هويتك مع مركز مجتمع AnythingLLM قبل نشر أي محتوى.\",\n          button: \"تواصل مع مركز المجتمع\",\n        },\n      },\n      slash_command: {\n        success_title: \"نجاح!\",\n        success_description: \"تم نشر أمر Slash الخاص بك في مركز المجتمع!\",\n        success_thank_you: \"شكراً لمشاركتكم في المجتمع!\",\n        view_on_hub: \"عرض على منصة المجتمع\",\n        modal_title: \"نشر أمر Slash\",\n        name_label: \"اسم\",\n        name_description: \"هذا هو اسم العرض الخاص بأمرك.\",\n        name_placeholder: \"أمر السلايش الخاص بي\",\n        description_label: \"وصف\",\n        description_description:\n          \"هذا هو وصف أمر السلايش الخاص بك. استخدم هذا لوصف الغرض من أمر السلايش الخاص بك.\",\n        tags_label: \"الوسوم\",\n        tags_description:\n          \"تُستخدم العلامات لتسمية أوامر سلاش الخاصة بك لتسهيل البحث عنها. يمكنك إضافة عدة علامات. الحد الأقصى لعدد العلامات هو 5. الحد الأقصى لعدد الأحرف في كل علامة هو 20 حرفًا.\",\n        tags_placeholder: \"أدخل النص واضغط على مفتاح الإدخال لإضافة العلامات\",\n        visibility_label: \"رؤية\",\n        public_description: \"الأوامر العامة مرئية للجميع.\",\n        private_description: \"الأوامر الخاصة مرئية فقط لك.\",\n        publish_button: \"نشر في مركز المجتمع\",\n        submitting: \"نشر...\",\n        prompt_label: \"الاستعلام\",\n        prompt_description:\n          \"هذا هو الأمر الذي سيتم استخدامه عند تفعيل الأمر الذي يتضمن الشرطة.\",\n        prompt_placeholder: \"أدخل سؤالك هنا...\",\n      },\n    },\n  },\n  security: {\n    title: \"حماية\",\n    multiuser: {\n      title: \"وضعية المستعملين المتعددين\",\n      description:\n        \"قم بإعداد مثيلك لدعم فريقك من خلال تنشيط وضعية المستعملين المتعددين.\",\n      enable: {\n        \"is-enable\": \"تم تمكين وضعية المستعملين المتعددين\",\n        enable: \"تمكين وضعية المستعملين المتعددين\",\n        description:\n          \"افتراضيًا، ستكون أنت المشرف الوحيد. وبصفتك مشرفا ستحتاج إلى إنشاء حسابات لجميع المستعملين أو المشرفين الجدد. لا تفقد كلمة مرورك، حيث يمكن فقط للمستعمل المشرف إعادة تعيين كلمات المرور.\",\n        username: \"اسم المستعمل لحساب المشرف\",\n        password: \"كلمة مرور حساب المشرف\",\n      },\n    },\n    password: {\n      title: \"حماية كلمة المرور\",\n      description:\n        \"إحم مثيل إني ثينك إلْلْمْ بكلمة المرور. إذا نسيتها فلا يوجد طريقة لاستردادها، فاحرص على حفظها.\",\n      \"password-label\": \"كلمة مرور المثيل\",\n    },\n  },\n  home: {\n    welcome: \"مرحبا\",\n    chooseWorkspace: \"اختر مساحة العمل لبدء المحادثة!\",\n    notAssigned:\n      \"لا تم التخصيص لأي مساحة عمل.\\nيرجى الاتصال بمدير المثيل لطلب الوصول إلى مساحة عمل.\",\n    goToWorkspace: 'الذهاب إلى \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/cs/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"Vítejte v\",\n      getStarted: \"Začít\",\n    },\n    llm: {\n      title: \"Preferovaný LLM\",\n      description:\n        \"AnythingLLM může pracovat s mnoha poskytovateli LLM. Toto bude služba, která bude zpracovávat chatování.\",\n    },\n    userSetup: {\n      title: \"Nastavení uživatele\",\n      description: \"Nakonfigurujte svá uživatelská nastavení.\",\n      howManyUsers: \"Kolik uživatelů bude používat tuto instanci?\",\n      justMe: \"Jen já\",\n      myTeam: \"Můj tým\",\n      instancePassword: \"Heslo instance\",\n      setPassword: \"Chcete nastavit heslo?\",\n      passwordReq: \"Hesla musí mít alespoň 8 znaků.\",\n      passwordWarn:\n        \"Je důležité toto heslo uložit, protože neexistuje způsob obnovení.\",\n      adminUsername: \"Uživatelské jméno správce\",\n      adminPassword: \"Heslo správce\",\n      adminPasswordReq: \"Hesla musí mít alespoň 8 znaků.\",\n      teamHint:\n        \"Ve výchozím nastavení budete jediným správcem. Po dokončení onboardingu můžete vytvářet a zvat další uživatele nebo správce. Neztrácejte své heslo, protože pouze správci mohou resetovat hesla.\",\n    },\n    data: {\n      title: \"Zpracování dat a soukromí\",\n      description:\n        \"Jsme odhodláni být transparentní a dávat vám kontrolu nad vašimi osobními údaji.\",\n      settingsHint:\n        \"Tato nastavení lze kdykoliv znovu nakonfigurovat v nastavení.\",\n    },\n    survey: {\n      title: \"Vítejte v AnythingLLM\",\n      description:\n        \"Pomozte nám vybudovat AnythingLLM pro vaše potřeby. Volitelné.\",\n      email: \"Jaký je váš e-mail?\",\n      useCase: \"K čemu budete AnythingLLM používat?\",\n      useCaseWork: \"Pro práci\",\n      useCasePersonal: \"Pro osobní použití\",\n      useCaseOther: \"Jiné\",\n      comment: \"Jak jste se o AnythingLLM dozvěděli?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube atd. - Dejte nám vědět, jak jste nás našli!\",\n      skip: \"Přeskočit průzkum\",\n      thankYou: \"Děkujeme za vaši zpětnou vazbu!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Název pracovního prostoru\",\n    user: \"Uživatel\",\n    selection: \"Výběr modelu\",\n    saving: \"Ukládání...\",\n    save: \"Uložit změny\",\n    previous: \"Předchozí stránka\",\n    next: \"Další stránka\",\n    optional: \"Volitelné\",\n    yes: \"Ano\",\n    no: \"Ne\",\n    search: \"Hledat\",\n    username_requirements:\n      \"Uživatelské jméno musí mít 2–32 znaků, začínat malým písmenem a obsahovat pouze malá písmena, číslice, podtržítka, pomlčky a tečky.\",\n    on: \"Na\",\n    none: \"Žádné\",\n    stopped: \"Zastaveno\",\n    loading: \"Načítání\",\n    refresh: \"Obnovit\",\n  },\n  home: {\n    welcome: \"Vítejte\",\n    chooseWorkspace: \"Vyberte pracovní prostor pro začátek chatu!\",\n    notAssigned:\n      \"V současné době nemáte přiřazen žádný pracovní prostor.\\nKontaktujte svého správce o žádost o přístup k pracovnímu prostoru.\",\n    goToWorkspace: 'Přejít na \"{{workspace}}\"',\n  },\n  settings: {\n    title: \"Nastavení instance\",\n    invites: \"Pozvánky\",\n    users: \"Uživatelé\",\n    workspaces: \"Pracovní prostory\",\n    \"workspace-chats\": \"Chaty pracovních prostorů\",\n    customization: \"Přizpůsobení\",\n    interface: \"Předvolby rozhraní\",\n    branding: \"Značení a bílé označení\",\n    chat: \"Chat\",\n    \"api-keys\": \"API pro vývojáře\",\n    llm: \"LLM\",\n    transcription: \"Přepis\",\n    embedder: \"Embedding\",\n    \"text-splitting\": \"Rozdělení textu a chunkování\",\n    \"voice-speech\": \"Hlas a řeč\",\n    \"vector-database\": \"Vektorová databáze\",\n    embeds: \"Vložený chat\",\n    security: \"Zabezpečení\",\n    \"event-logs\": \"Protokoly událostí\",\n    privacy: \"Soukromí a data\",\n    \"ai-providers\": \"Poskytovatelé AI\",\n    \"agent-skills\": \"Dovednosti agenta\",\n    admin: \"Správce\",\n    tools: \"Nástroje\",\n    \"system-prompt-variables\": \"Proměnné systémové výzvy\",\n    \"experimental-features\": \"Experimentální funkce\",\n    contact: \"Kontaktovat podporu\",\n    \"browser-extension\": \"Rozšíření prohlížeče\",\n    \"mobile-app\": \"AnythingLLM Mobile\",\n    \"community-hub\": {\n      title: \"Centrální místo pro komunitu\",\n      trending: \"Prozkoumejte aktuální trendy\",\n      \"your-account\": \"Váš účet\",\n      \"import-item\": \"Importovat položku\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Vítejte v\",\n      \"placeholder-username\": \"Uživatelské jméno\",\n      \"placeholder-password\": \"Heslo\",\n      login: \"Přihlásit\",\n      validating: \"Ověřování...\",\n      \"forgot-pass\": \"Zapomněli jste heslo\",\n      reset: \"Resetovat\",\n    },\n    \"sign-in\": \"Přihlaste se do svého {{appName}} účtu.\",\n    \"password-reset\": {\n      title: \"Reset hesla\",\n      description: \"Níže uveďte potřebné informace pro resetování hesla.\",\n      \"recovery-codes\": \"Záchranné kódy\",\n      \"back-to-login\": \"Zpět k přihlášení\",\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Vytvořte agenta\",\n      editWorkspace: \"Upravit pracovní prostor\",\n      uploadDocument: \"Nahrajte dokument\",\n    },\n    greeting: \"Jak vám mohu dnes pomoci?\",\n  },\n  \"new-workspace\": {\n    title: \"Nový pracovní prostor\",\n    placeholder: \"Můj pracovní prostor\",\n  },\n  \"workspaces—settings\": {\n    general: \"Obecná nastavení\",\n    chat: \"Nastavení chatu\",\n    vector: \"Vektorová databáze\",\n    members: \"Členové\",\n    agent: \"Konfigurace agenta\",\n  },\n  general: {\n    vector: {\n      title: \"Počet vektorů\",\n      description: \"Celkový počet vektorů ve vaší vektorové databázi.\",\n    },\n    names: {\n      description:\n        \"Tímto se změní pouze zobrazovaný název vašeho pracovního prostoru.\",\n    },\n    message: {\n      title: \"Navrhované zprávy chatu\",\n      description:\n        \"Přizpůsobte zprávy, které budou navrhovány uživatelům vašeho pracovního prostoru.\",\n      add: \"Přidat novou zprávu\",\n      save: \"Uložit zprávy\",\n      heading: \"Vysvětlit mi\",\n      body: \"výhody AnythingLLM\",\n    },\n    delete: {\n      title: \"Smazat pracovní prostor\",\n      description:\n        \"Smažte tento pracovní prostor a všechna jeho data. Toto smaže pracovní prostor pro všechny uživatele.\",\n      delete: \"Smazat pracovní prostor\",\n      deleting: \"Mazání pracovního prostoru...\",\n      \"confirm-start\": \"Chystáte se smazat celý\",\n      \"confirm-end\":\n        \"pracovní prostor. Toto odstraní všechny vektorové embeddingy ve vaší vektorové databázi.\\n\\nPůvodní zdrojové soubory zůstanou nedotčeny. Tato akce je nevratná.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Poskytovatel LLM pracovního prostoru\",\n      description:\n        \"Konkrétní poskytovatel LLM a model, který bude použit pro tento pracovní prostor. Ve výchozím nastavení používá systémového poskytovatele LLM a nastavení.\",\n      search: \"Hledat všechny poskytovatele LLM\",\n    },\n    model: {\n      title: \"Chatovací model pracovního prostoru\",\n      description:\n        \"Konkrétní chatovací model, který bude použit pro tento pracovní prostor. Pokud je prázdné, použije se systémová preference LLM.\",\n    },\n    mode: {\n      title: \"Režim chatu\",\n      chat: {\n        title: \"Chat\",\n        description:\n          \"poskytne odpovědi založené na obecných znalostech LLM a kontextu dokumentu, který je k dispozici.<br />Pro použití nástrojů budete muset použít příkaz @agent.\",\n      },\n      query: {\n        title: \"Dotaz\",\n        description:\n          \"budou poskytovat odpovědi <b>pouze</b>, pokud je nalezen kontext dokumentu.<br />Pro použití nástrojů budete muset použít příkaz @agent.\",\n      },\n      automatic: {\n        title: \"Auto\",\n        description:\n          \"automaticky použije nástroje, pokud to podporují jak model, tak poskytovatel. Pokud není podporováno nativní volání nástrojů, budete muset použít příkaz `@agent` pro použití nástrojů.\",\n      },\n    },\n    history: {\n      title: \"Historie chatu\",\n      \"desc-start\":\n        \"Počet předchozích chatů, které budou zahrnuty do krátkodobé paměti odpovědi.\",\n      recommend: \"Doporučeno 20. \",\n      \"desc-end\":\n        \"Více než 45 pravděpodobně povede k trvalým selháním chatu v závislosti na velikosti zprávy.\",\n    },\n    prompt: {\n      title: \"Systémová výzva\",\n      description:\n        \"Výzva, která bude použita v tomto pracovním prostoru. Definujte kontext a pokyny pro AI k vygenerování odpovědi. Měli byste poskytnout pečlivě vytvořenou výzvu, aby AI mohla generovat relevantní a přesnou odpověď.\",\n      history: {\n        title: \"Historie systémových výzev\",\n        clearAll: \"Vymazat vše\",\n        noHistory: \"Žádná historie systémových výzev není k dispozici\",\n        restore: \"Obnovit\",\n        delete: \"Smazat\",\n        publish: \"Publikovat do komunitního centra\",\n        deleteConfirm: \"Jste si jisti, že chcete smazat tuto položku historie?\",\n        clearAllConfirm:\n          \"Jste si jisti, že chcete vymazat celou historii? Tato akce nelze vrátit zpět.\",\n        expand: \"Rozbalit\",\n      },\n    },\n    refusal: {\n      title: \"Odpověď na odmítnutí v režimu dotazu\",\n      \"desc-start\": \"V režimu\",\n      query: \"dotazu\",\n      \"desc-end\":\n        \"možná budete chtít vrátit vlastní odpověď na odmítnutí, pokud není nalezen kontext.\",\n      \"tooltip-title\": \"Proč to vidím?\",\n      \"tooltip-description\":\n        \"Jste v režimu dotazu, který používá pouze informace z vašich dokumentů. Přepněte na režim chatu pro flexibilnější konverzace, nebo klikněte sem a navštivte naši dokumentaci pro další informace o režimech chatu.\",\n    },\n    temperature: {\n      title: \"Teplota LLM\",\n      \"desc-start\":\n        'Toto nastavení řídí, jak \"kreativní\" budou odpovědi vašeho LLM.',\n      \"desc-end\":\n        \"Vyšší číslo znamená kreativnější. U některých modelů to může vést k nesourodým odpovědím při nastavení příliš vysoko.\",\n      hint: \"Většina LLM má různé přijatelné rozsahy platných hodnot. Poradťe se se svým poskytovatelem LLM pro tyto informace.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Identifikátor vektorové databáze\",\n    snippets: {\n      title: \"Maximum kontextových úryvků\",\n      description:\n        \"Toto nastavení řídí maximální množství kontextových úryvků, které budou odeslány do LLM pro každý chat nebo dotaz.\",\n      recommend: \"Doporučeno: 4\",\n    },\n    doc: {\n      title: \"Práh podobnosti dokumentů\",\n      description:\n        \"Minimální skóre podobnosti požadované pro zdroj, aby byl považován za související s chatem. Vyšší číslo znamená, že zdroj musí být více podobný chatu.\",\n      zero: \"Žádné omezení\",\n      low: \"Nízké (skóre podobnosti ≥ .25)\",\n      medium: \"Střední (skóre podobnosti ≥ .50)\",\n      high: \"Vysoké (skóre podobnosti ≥ .75)\",\n    },\n    reset: {\n      reset: \"Resetovat vektorovou databázi\",\n      resetting: \"Mazání vektorů...\",\n      confirm:\n        \"Chystáte se resetovat vektorovou databázi tohoto pracovního prostoru. Toto odstraní všechny vektorové embeddingy, které jsou momentálně vloženy.\\n\\nPůvodní zdrojové soubory zůstanou nedotčeny. Tato akce je nevratná.\",\n      error: \"Vektorovou databázi pracovního prostoru se nepodařilo resetovat!\",\n      success: \"Vektorová databáze pracovního prostoru byla resetována!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"Výkon LLM, které explicitně nepodporují volání nástrojů, je silně závislý na schopnostech a přesnosti modelu. Některé schopnosti mohou být omezené nebo nefunkční.\",\n    provider: {\n      title: \"Poskytovatel LLM agenta pracovního prostoru\",\n      description:\n        \"Konkrétní poskytovatel LLM a model, který bude použit pro @agenta agenta tohoto pracovního prostoru.\",\n    },\n    mode: {\n      chat: {\n        title: \"Chatovací model agenta pracovního prostoru\",\n        description:\n          \"Konkrétní chatovací model, který bude použit pro @agenta agenta tohoto pracovního prostoru.\",\n      },\n      title: \"Model agenta pracovního prostoru\",\n      description:\n        \"Konkrétní model LLM, který bude použit pro @agenta agenta tohoto pracovního prostoru.\",\n      wait: \"-- čekání na modely --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG a dlouhodobá paměť\",\n        description:\n          'Umožněte agentovi využívat vaše místní dokumenty k odpovědi na dotaz nebo požádejte agenta, aby si \"zapamatoval\" části obsahu pro dlouhodobé načítání.',\n      },\n      view: {\n        title: \"Zobrazit a shrnout dokumenty\",\n        description:\n          \"Umožněte agentovi vypsat a shrnout obsah souborů pracovního prostoru, které jsou momentálně vloženy.\",\n      },\n      scrape: {\n        title: \"Stahovat webové stránky\",\n        description:\n          \"Umožněte agentovi navštěvovat a stahovat obsah webových stránek.\",\n      },\n      generate: {\n        title: \"Generovat grafy\",\n        description:\n          \"Umožněte výchozímu agentovi generovat různé typy grafů z dat poskytnutých nebo uvedených v chatu.\",\n      },\n      save: {\n        title: \"Generovat a ukládat soubory\",\n        description:\n          \"Umožněte výchozímu agentovi generovat a zapisovat do souborů, které lze uložit do počítače.\",\n      },\n      web: {\n        title: \"Živé webové vyhledávání a prohlížení\",\n        description:\n          \"Umožněte svému agentovi, aby prohledával internet a odpovídal na vaše otázky, propojením se poskytovatelem vyhledávacího servisu (SERP).\",\n      },\n      sql: {\n        title: \"Připojení k databázi SQL\",\n        description:\n          \"Umožněte svému agentovi, aby mohl využívat SQL k zodpovězení vašich otázek, a to prostřednictvím připojení k různým poskytovatelům databází.\",\n      },\n      default_skill:\n        \"Výchozí nastavení je, že tato schopnost je aktivní, ale můžete ji vypnout, pokud nechcete, aby ji mohl využít zástupce.\",\n    },\n    mcp: {\n      title: \"Servery společnosti MCP\",\n      \"loading-from-config\": \"Načítání serverů MCP z konfiguračního souboru\",\n      \"learn-more\": \"Zjistěte více o serverech MCP.\",\n      \"no-servers-found\": \"Nebyl nalezen žádný server pro správu MCP.\",\n      \"tool-warning\":\n        \"Pro optimální výkon zvažte vypnutí nepoužívaných nástrojů, abyste ušetřili zdroje.\",\n      \"stop-server\": \"Zastavte server MCP\",\n      \"start-server\": \"Spustit server MCP\",\n      \"delete-server\": \"Odstranit server MCP\",\n      \"tool-count-warning\":\n        \"Tento server pro správu chatů má povolené nástroje <b>{{count}}, které spotřebovávají kontext v každém chatu. </b> Zvažte vypnutí nepotřebných nástrojů, abyste ušetřili kontext.\",\n      \"startup-command\": \"Příkaz pro spuštění\",\n      command: \"Příkaz\",\n      arguments: \"Argumenty\",\n      \"not-running-warning\":\n        \"Tento server pro správu MCP není aktivní – buď byl vypnut, nebo se při spuštění vyskytuje chyba.\",\n      \"tool-call-arguments\": \"Argumenty pro volání nástroje\",\n      \"tools-enabled\": \"nástroje jsou aktivovány\",\n    },\n    settings: {\n      title: \"Nastavení dovedností agenta\",\n      \"max-tool-calls\": {\n        title: \"Maximální počet volání nástrojů na jednu odpověď\",\n        description:\n          \"Maximální počet nástrojů, které může agent spouštět v řetězci za účelem generování jedné odpovědi. To zabraňuje nekontrolovanému spouštění nástrojů a vytváření nekonečných smyček.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Inteligentní výběr dovedností\",\n        \"beta-badge\": \"Beta\",\n        description:\n          \"Umožněte použití libovolného počtu nástrojů a snížit využití tokenů až o 80 % pro každou dotaz — AnythingLLM automaticky vybírá vhodné dovednosti pro každou žádost.\",\n        \"max-tools\": {\n          title: \"Nástroje Max\",\n          description:\n            \"Maximální počet nástrojů, které lze vybrat pro každou dotaz. Doporučujeme nastavit tuto hodnotu na vyšší, pro modely s větším kontextem.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Chaty pracovních prostorů\",\n    description:\n      \"Toto jsou všechny zaznamenané chaty a zprávy, které odeslali uživatelé, seřazené podle data vytvoření.\",\n    export: \"Exportovat\",\n    table: {\n      id: \"ID\",\n      by: \"Odeslal\",\n      workspace: \"Pracovní prostor\",\n      prompt: \"Výzva\",\n      response: \"Odpověď\",\n      at: \"Odesláno v\",\n    },\n  },\n  customization: {\n    interface: {\n      title: \"Předvolby rozhraní\",\n      description: \"Nastavte své předvolby rozhraní pro AnythingLLM.\",\n    },\n    branding: {\n      title: \"Značení a bílé označení\",\n      description:\n        \"Bílé označení instance AnythingLLM pomocí vlastního značení.\",\n    },\n    chat: {\n      title: \"Chat\",\n      description: \"Nastavte své předvolby chatu pro AnythingLLM.\",\n      auto_submit: {\n        title: \"Automatické odeslání hlasového vstupu\",\n        description: \"Automaticky odeslat hlasový vstup po období ticha\",\n      },\n      auto_speak: {\n        title: \"Automatické čtení odpovědí\",\n        description: \"Automaticky číst odpovědi z AI\",\n      },\n      spellcheck: {\n        title: \"Povolit kontrolu pravopisu\",\n        description:\n          \"Povolit nebo zakázat kontrolu pravopisu v poli vstupu chatu\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Motiv\",\n        description: \"Vyberte preferovaný barevný motiv pro aplikaci.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Zobrazit posuvník\",\n        description: \"Povolit nebo zakázat posuvník v okně chatu.\",\n      },\n      \"support-email\": {\n        title: \"E-mail podpory\",\n        description:\n          \"Nastavte e-mailovou adresu podpory, která má být přístupná uživatelům, když potřebují pomoc.\",\n      },\n      \"app-name\": {\n        title: \"Název\",\n        description:\n          \"Nastavte název, který je zobrazen na přihlašovací stránce všem uživatelům.\",\n      },\n      \"display-language\": {\n        title: \"Zobrazovací jazyk\",\n        description:\n          \"Vyberte preferovaný jazyk pro vykreslení rozhraní AnythingLLM - pokud jsou k dispozici překlady.\",\n      },\n      logo: {\n        title: \"Logo značky\",\n        description:\n          \"Nahrajte své vlastní logo k zobrazení na všech stránkách.\",\n        add: \"Přidat vlastní logo\",\n        recommended: \"Doporučená velikost: 800 x 200\",\n        remove: \"Odebrat\",\n        replace: \"Nahradit\",\n      },\n      \"browser-appearance\": {\n        title: \"Vzhled prohlížeče\",\n        description:\n          \"Přizpůsobte vzhled karty prohlížeče a názvu, když je aplikace otevřena.\",\n        tab: {\n          title: \"Název\",\n          description:\n            \"Nastavte vlastní název karty, když je aplikace otevřena v prohlížeči.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description: \"Použít vlastní favicon pro kartu prohlížeče.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Položky zápatí postranního panelu\",\n        description:\n          \"Přizpůsobte položky zápatí zobrazené na spodní části postranního panelu.\",\n        icon: \"Ikona\",\n        link: \"Odkaz\",\n      },\n      \"render-html\": {\n        title: \"Vykreslit HTML v chatu\",\n        description:\n          \"Vykreslit HTML odpovědi v odpovědích asistenta.\\nTo může vést k mnohem vyšší věrnosti kvality odpovědi, ale může také vést k potenciálním bezpečnostním rizikům.\",\n      },\n    },\n  },\n  api: {\n    title: \"API klíče\",\n    description:\n      \"API klíče umožňují držiteli programově přistupovat a spravovat tuto instanci AnythingLLM.\",\n    link: \"Přečíst dokumentaci API\",\n    generate: \"Generovat nový API klíč\",\n    table: {\n      key: \"API klíč\",\n      by: \"Vytvořil\",\n      created: \"Vytvořeno\",\n    },\n  },\n  llm: {\n    title: \"Preferovaný LLM\",\n    description:\n      \"Toto jsou přihlašovací údaje a nastavení pro vašeho preferovaného poskytovatele chatu a embeddingu LLM. Je důležité, aby tyto klíče byly aktuální a správné, jinak AnythingLLM nebude fungovat správně.\",\n    provider: \"Poskytovatel LLM\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Koncový bod služby Azure\",\n        api_key: \"API klíč\",\n        chat_deployment_name: \"Název nasazení chatu\",\n        chat_model_token_limit: \"Limit tokenů chatovacího modelu\",\n        model_type: \"Typ modelu\",\n        model_type_tooltip:\n          \"Pokud vaše nasazení používá model uvažování (o1, o1-mini, o3-mini atd.), nastavte to na 'Uvažování'. Jinak se vaše požadavky chatu mohou selhat.\",\n        default: \"Výchozí\",\n        reasoning: \"Uvažování\",\n      },\n    },\n  },\n  transcription: {\n    title: \"Preferovaný model přepisu\",\n    description:\n      \"Toto jsou přihlašovací údaje a nastavení pro vašeho preferovaného poskytovatele modelu přepisu. Je důležité, aby tyto klíče byly aktuální a správné, jinak mediální soubory a audio nebudou přepisovány.\",\n    provider: \"Poskytovatel přepisu\",\n    \"warn-start\":\n      \"Použití místního modelu whisper na strojích s omezenou RAM nebo CPU může zastavit AnythingLLM při zpracování mediálních souborů.\",\n    \"warn-recommend\": \"Doporučujeme alespoň 2GB RAM a nahrávat soubory <10Mb.\",\n    \"warn-end\": \"Vestavěný model se automaticky stáhne při prvním použití.\",\n  },\n  embedding: {\n    title: \"Preferovaný embedding\",\n    \"desc-start\":\n      \"Při použití LLM, který nativně nepodporuje engine embeddingu - možná budete muset additionally uvést přihlašovací údaje pro embeddingování textu.\",\n    \"desc-end\":\n      \"Embedding je proces převodu textu na vektory. Tyto přihlašovací údaje jsou nutné k převodu vašich souborů a výzev do formátu, který AnythingLLM může použít ke zpracování.\",\n    provider: {\n      title: \"Poskytovatel embeddingu\",\n    },\n  },\n  text: {\n    title: \"Předvolby rozdělení a chunkování textu\",\n    \"desc-start\":\n      \"Někdy můžete chtít změnit výchozí způsob, jakým jsou nové dokumenty děleny a chunkovány před vložením do vaší vektorové databáze.\",\n    \"desc-end\":\n      \"Měli byste toto nastavení měnit pouze tehdy, pokud rozumíte, jak funguje rozdělení textu a jeho vedlejší účinky.\",\n    size: {\n      title: \"Velikost chunku textu\",\n      description:\n        \"Toto je maximální délka znaků, která může být přítomna v jednom vektoru.\",\n      recommend: \"Maximální délka embeddingového modelu je\",\n    },\n    overlap: {\n      title: \"Překrytí chunků textu\",\n      description:\n        \"Toto je maximální překrytí znaků, ke které dochází během chunkování mezi dvěma sousedními chunky textu.\",\n    },\n  },\n  vector: {\n    title: \"Vektorová databáze\",\n    description:\n      \"Toto jsou přihlašovací údaje a nastavení, jak bude vaše instance AnythingLLM fungovat. Je důležité, aby tyto klíče byly aktuální a správné.\",\n    provider: {\n      title: \"Poskytovatel vektorové databáze\",\n      description: \"Pro LanceDB není potřeba žádná konfigurace.\",\n    },\n  },\n  embeddable: {\n    title: \"Vložitelné widgety chatu\",\n    description:\n      \"Vložitelné widgety chatu jsou veřejně orientovaná rozhraní chatu spojená s jedním pracovním prostorem. Tyto vám umožňují vytvářet pracovní prostory, které pak můžete zveřejnit světu.\",\n    create: \"Vytvořit vložení\",\n    table: {\n      workspace: \"Pracovní prostor\",\n      chats: \"Odeslané chaty\",\n      active: \"Aktivní domény\",\n      created: \"Vytvořeno\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Historie vložených chatů\",\n    export: \"Exportovat\",\n    description:\n      \"Toto jsou všechny zaznamenané chaty a zprávy z jakéhokoli vložení, které jste zveřejnili.\",\n    table: {\n      embed: \"Vložení\",\n      sender: \"Odesílatel\",\n      message: \"Zpráva\",\n      response: \"Odpověď\",\n      at: \"Odesláno v\",\n    },\n  },\n  security: {\n    title: \"Zabezpečení\",\n    multiuser: {\n      title: \"Režim více uživatelů\",\n      description:\n        \"Nastavte svou instanci pro podporu týmu aktivováním režimu více uživatelů.\",\n      enable: {\n        \"is-enable\": \"Režim více uživatelů je povolen\",\n        enable: \"Povolit režim více uživatelů\",\n        description:\n          \"Ve výchozím nastavení budete jediným správcem. Jako správce budete muset vytvářet účty pro všechny nové uživatele nebo správce. Neztrácejte své heslo, protože pouze uživatel typu správce může resetovat hesla.\",\n        username: \"Uživatelské jméno účtu správce\",\n        password: \"Heslo účtu správce\",\n      },\n    },\n    password: {\n      title: \"Ochrana heslem\",\n      description:\n        \"Chraňte svou instanci AnythingLLM heslem. Pokud zapomenete, neexistuje způsob obnovení, proto se ujistěte, že heslo uložíte.\",\n      \"password-label\": \"Heslo instance\",\n    },\n  },\n  event: {\n    title: \"Protokoly událostí\",\n    description:\n      \"Zobrazit všechny akce a události probíhající na této instanci pro sledování.\",\n    clear: \"Vymazat protokoly událostí\",\n    table: {\n      type: \"Typ události\",\n      user: \"Uživatel\",\n      occurred: \"Nastalo v\",\n    },\n  },\n  privacy: {\n    title: \"Soukromí a zpracování dat\",\n    description:\n      \"Toto je vaše konfigurace, jak připojené třetí strany a AnythingLLM zpracovávají vaše data.\",\n    anonymous: \"Anonymní telemetrie je povolena\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Hledat datové konektory\",\n    \"no-connectors\": \"Nebyly nalezeny žádné datové konektory.\",\n    obsidian: {\n      vault_location: \"Umístění trezoru\",\n      vault_description:\n        \"Vyberte složku trezoru Obsidian pro import všech poznámek a jejich spojení.\",\n      selected_files: \"Nalezeno {{count}} souborů markdown\",\n      importing: \"Importování trezoru...\",\n      import_vault: \"Importovat trezor\",\n      processing_time:\n        \"To může chvíli trvat v závislosti na velikosti vašeho trezoru.\",\n      vault_warning:\n        \"Aby se předešlo konfliktům, ujistěte se, že váš trezor Obsidian není momentálně otevřen.\",\n    },\n    github: {\n      name: \"Úložiště GitHub\",\n      description:\n        \"Importovat celé veřejné nebo soukromé úložiště GitHub jedním kliknutím.\",\n      URL: \"URL úložiště GitHub\",\n      URL_explained: \"URL úložiště GitHub, které chcete sbírat.\",\n      token: \"Přístupový token GitHub\",\n      optional: \"volitelné\",\n      token_explained: \"Přístupový token pro prevenci omezení rychlosti.\",\n      token_explained_start: \"Bez \",\n      token_explained_link1: \"Osobního přístupového tokenu\",\n      token_explained_middle:\n        \", API GitHub může omezit počet souborů, které lze sbírat kvůli limitům rychlosti. Můžete \",\n      token_explained_link2: \"vytvořit dočasný přístupový token\",\n      token_explained_end: \" k vyhnutí se tomuto problému.\",\n      ignores: \"Ignorované soubory\",\n      git_ignore:\n        \"Seznam ve formátu .gitignore k ignorování specifických souborů během sbírání. Stiskněte Enter po každé položce, kterou chcete uložit.\",\n      task_explained:\n        \"Po dokončení budou všechny soubory k dispozici pro vložení do pracovních prostorů ve výběru dokumentů.\",\n      branch: \"Větev, ze které chcete sbírat soubory\",\n      branch_loading: \"-- načítání dostupných větví --\",\n      branch_explained: \"Větev, ze které chcete sbírat soubory.\",\n      token_information:\n        \"Bez vyplnění <b>Přístupového tokenu GitHub</b> bude tento datový konektor schopen sbírat pouze <b>nejvyšší úrovňové</b> soubory úložiště kvůli omezením veřejného API GitHub.\",\n      token_personal:\n        \"Získejte bezplatný osobní přístupový token s účtem GitHub zde.\",\n    },\n    gitlab: {\n      name: \"Úložiště GitLab\",\n      description:\n        \"Importovat celé veřejné nebo soukromé úložiště GitLab jedním kliknutím.\",\n      URL: \"URL úložiště GitLab\",\n      URL_explained: \"URL úložiště GitLab, které chcete sbírat.\",\n      token: \"Přístupový token GitLab\",\n      optional: \"volitelné\",\n      token_description: \"Vyberte další entity k načtení z API GitLab.\",\n      token_explained_start: \"Bez \",\n      token_explained_link1: \"Osobního přístupového tokenu\",\n      token_explained_middle:\n        \", API GitLab může omezit počet souborů, které lze sbírat kvůli limitům rychlosti. Můžete \",\n      token_explained_link2: \"vytvořit dočasný přístupový token\",\n      token_explained_end: \" k vyhnutí se tomuto problému.\",\n      fetch_issues: \"Načíst problémy jako dokumenty\",\n      ignores: \"Ignorované soubory\",\n      git_ignore:\n        \"Seznam ve formátu .gitignore k ignorování specifických souborů během sbírání. Stiskněte Enter po každé položce, kterou chcete uložit.\",\n      task_explained:\n        \"Po dokončení budou všechny soubory k dispozici pro vložení do pracovních prostorů ve výběru dokumentů.\",\n      branch: \"Větev, ze které chcete sbírat soubory\",\n      branch_loading: \"-- načítání dostupných větví --\",\n      branch_explained: \"Větev, ze které chcete sbírat soubory.\",\n      token_information:\n        \"Bez vyplnění <b>Přístupového tokenu GitLab</b> bude tento datový konektor schopen sbírat pouze <b>nejvyšší úrovňové</b> soubory úložiště kvůli omezením veřejného API GitLab.\",\n      token_personal:\n        \"Získejte bezplatný osobní přístupový token s účtem GitLab zde.\",\n    },\n    youtube: {\n      name: \"Přepis YouTube\",\n      description: \"Importovat přepis celého videa YouTube z odkazu.\",\n      URL: \"URL videa YouTube\",\n      URL_explained_start:\n        \"Zadejte URL jakéhokoli videa YouTube pro stažení jeho přepisu. Video musí mít \",\n      URL_explained_link: \"uzavřené titulky\",\n      URL_explained_end: \" k dispozici.\",\n      task_explained:\n        \"Po dokončení bude přepis k dispozici pro vložení do pracovních prostorů ve výběru dokumentů.\",\n    },\n    \"website-depth\": {\n      name: \"Hromadný stahovač odkazů\",\n      description:\n        \"Stáhnout webovou stránku a její pododkazy až do určité hloubky.\",\n      URL: \"URL webové stránky\",\n      URL_explained: \"URL webové stránky, kterou chcete stáhnout.\",\n      depth: \"Hloubka stahování\",\n      depth_explained:\n        \"Toto je počet pododkazů, které má pracovník následovat z původní URL.\",\n      max_pages: \"Maximum stránek\",\n      max_pages_explained: \"Maximální počet odkazů ke stažení.\",\n      task_explained:\n        \"Po dokončení bude veškerý stažený obsah k dispozici pro vložení do pracovních prostorů ve výběru dokumentů.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Importovat celou stránku Confluence jedním kliknutím.\",\n      deployment_type: \"Typ nasazení Confluence\",\n      deployment_type_explained:\n        \"Určete, zda je vaše instance Conference hostována na cloudu Atlassian nebo sama hostovaná.\",\n      base_url: \"Základní URL Confluence\",\n      base_url_explained: \"Toto je základní URL vašeho prostoru Confluence.\",\n      space_key: \"Klíč prostoru Confluence\",\n      space_key_explained:\n        \"Toto je klíč prostoru vaší instance Confluence, který bude použit. Obvykle začíná s ~\",\n      username: \"Uživatelské jméno Confluence\",\n      username_explained: \"Vaše uživatelské jméno Confluence\",\n      auth_type: \"Typ ověření Confluence\",\n      auth_type_explained:\n        \"Vyberte typ ověření, který chcete použít pro přístup ke svým stránkám Confluence.\",\n      auth_type_username: \"Uživatelské jméno a přístupový token\",\n      auth_type_personal: \"Osobní přístupový token\",\n      token: \"Přístupový token Confluence\",\n      token_explained_start:\n        \"Musíte poskytnout přístupový token pro ověření. Můžete vygenerovat přístupový token\",\n      token_explained_link: \"zde\",\n      token_desc: \"Přístupový token pro ověření\",\n      pat_token: \"Osobní přístupový token Confluence\",\n      pat_token_explained: \"Váš osobní přístupový token Confluence.\",\n      bypass_ssl: \"Obejití ověření certifikátu SSL\",\n      bypass_ssl_explained:\n        \"Povolte tuto možnost k obejití ověření certifikátu SSL pro samo-hostované instance Confluence s vlastnoručně podepsaným certifikátem\",\n      task_explained:\n        \"Po dokončení bude obsah stránky k dispozici pro vložení do pracovních prostorů ve výběru dokumentů.\",\n    },\n    manage: {\n      documents: \"Dokumenty\",\n      \"data-connectors\": \"Datové konektory\",\n      \"desktop-only\":\n        \"Úprava těchto nastavení je k dispozici pouze na stolním zařízení. Chcete-li pokračovat, přístupujte na tuto stránku na svém stolním počítači.\",\n      dismiss: \"Odmítnout\",\n      editing: \"Úprava\",\n    },\n    directory: {\n      \"my-documents\": \"Mé dokumenty\",\n      \"new-folder\": \"Nová složka\",\n      \"search-document\": \"Hledat dokument\",\n      \"no-documents\": \"Žádné dokumenty\",\n      \"move-workspace\": \"Přesunout do pracovního prostoru\",\n      \"delete-confirmation\":\n        \"Jste si jisti, že chcete smazat tyto soubory a složky?\\nToto odstraní soubory ze systému a automaticky je odstraní ze všech existujících pracovních prostorů.\\nTato akce je nevratná.\",\n      \"removing-message\":\n        \"Odstraňování {{count}} dokumentů a {{folderCount}} složek. Prosím čekejte.\",\n      \"move-success\": \"Úspěšně přesunuto {{count}} dokumentů.\",\n      no_docs: \"Žádné dokumenty\",\n      select_all: \"Vybrat vše\",\n      deselect_all: \"Zrušit výběr všeho\",\n      remove_selected: \"Odebrat vybrané\",\n      costs: \"*Jednorázové náklady pro embeddingy\",\n      save_embed: \"Uložit a vložit\",\n      \"total-documents_one\": \"{{count}} dokument\",\n      \"total-documents_other\": \"{{count}} dokumenty\",\n    },\n    upload: {\n      \"processor-offline\": \"Procesor dokumentů nedostupný\",\n      \"processor-offline-desc\":\n        \"Nemůžeme nahrát vaše soubory právě teď, protože procesor dokumentů je offline. Zkuste to prosím později.\",\n      \"click-upload\": \"Klikněte pro nahrání nebo přetažení a upuštění\",\n      \"file-types\":\n        \"podporuje textové soubory, csv, tabulky, zvukové soubory a další!\",\n      \"or-submit-link\": \"nebo odeslat odkaz\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"Načítání...\",\n      \"fetch-website\": \"Stáhnout webovou stránku\",\n      \"privacy-notice\":\n        \"Tyto soubory budou nahrány na procesor dokumentů běžící na této instanci AnythingLLM. Tyto soubory nejsou odesílány nebo sdíleny s třetí stranou.\",\n    },\n    pinning: {\n      what_pinning: \"Co je připínání dokumentů?\",\n      pin_explained_block1:\n        \"Když <b>připnete</b> dokument v AnythingLLM, vložíme celý obsah dokumentu do vašeho okna výzvy, aby ho LLM plně pochopil.\",\n      pin_explained_block2:\n        \"To funguje nejlépe s <b>modely s velkým kontextem</b> nebo malými soubory, které jsou kritické pro jejich znalostní základ.\",\n      pin_explained_block3:\n        \"Pokud nedostáváte odpovědi, které si přejete od AnythingLLM ve výchozím nastavení, pak připínání je skvělý způsob získání kvalitnějších odpovědí jedním kliknutím.\",\n      accept: \"OK, rozumím\",\n    },\n    watching: {\n      what_watching: \"Co dělá sledování dokumentu?\",\n      watch_explained_block1:\n        \"Když <b>sledujete</b> dokument v AnythingLLM, <i>automaticky</i> synchronizujeme obsah dokumentu z jeho původního zdroje v pravidelných intervalech. Tím se automaticky aktualizuje obsah v každém pracovním prostoru, kde je tento soubor spravován.\",\n      watch_explained_block2:\n        \"Tato funkce v současné době podporuje onlineový obsah a nebude k dispozici pro ručně nahrané dokumenty.\",\n      watch_explained_block3_start:\n        \"Můžete spravovat, které dokumenty jsou sledovány z \",\n      watch_explained_block3_link: \"Správce souborů\",\n      watch_explained_block3_end: \" zobrazení správce.\",\n      accept: \"OK, rozumím\",\n    },\n  },\n  chat_window: {\n    attachments_processing: \"Přílohy se zpracovávají. Prosím čekejte...\",\n    send_message: \"Odeslat zprávu\",\n    attach_file: \"Přiložit soubor k tomuto chatu\",\n    text_size: \"Změnit velikost textu.\",\n    microphone: \"Mluvit svou výzvu.\",\n    send: \"Odeslat zprávu výzvy do pracovního prostoru\",\n    tts_speak_message: \"TTS Číst zprávu\",\n    copy: \"Kopírovat\",\n    regenerate: \"Regenerovat\",\n    regenerate_response: \"Regenerovat odpověď\",\n    good_response: \"Dobrá odpověď\",\n    more_actions: \"Další akce\",\n    fork: \"Rozdělit\",\n    delete: \"Smazat\",\n    cancel: \"Zrušit\",\n    edit_prompt: \"Upravit výzvu\",\n    edit_response: \"Upravit odpověď\",\n    preset_reset_description: \"Vymazat historii chatu a začít nový chat\",\n    add_new_preset: \" Přidat novou předvolbu\",\n    command: \"Příkaz\",\n    your_command: \"váš-příkaz\",\n    placeholder_prompt: \"Toto je obsah, který bude vložen před vaší výzvou.\",\n    description: \"Popis\",\n    placeholder_description: \"Odpovídá básní o LLM.\",\n    save: \"Uložit\",\n    small: \"Malé\",\n    normal: \"Normální\",\n    large: \"Velké\",\n    workspace_llm_manager: {\n      search: \"Hledat poskytovatele LLM\",\n      loading_workspace_settings: \"Načítání nastavení pracovního prostoru...\",\n      available_models: \"Dostupné modely pro {{provider}}\",\n      available_models_description:\n        \"Vyberte model k použití pro tento pracovní prostor.\",\n      save: \"Použít tento model\",\n      saving: \"Nastavování modelu jako výchozího pro pracovní prostor...\",\n      missing_credentials: \"Tomuto poskytovateli chybí přihlašovací údaje!\",\n      missing_credentials_description:\n        \"Klikněte pro nastavení přihlašovacích údajů\",\n    },\n    submit: \"Odeslat\",\n    edit_info_user:\n      \"„Odeslat“ znovu vygeneruje odpověď od AI. „Uložit“ aktualizuje pouze vaši zprávu.\",\n    edit_info_assistant: \"Vaše změny budou uloženy přímo v tomto odpovědi.\",\n    see_less: \"Zobrazit méně\",\n    see_more: \"Více\",\n    tools: \"Nářadí\",\n    browse: \"Prohlédněte si\",\n    text_size_label: \"Velikost písma\",\n    select_model: \"Vyberte model\",\n    sources: \"Zdroje\",\n    document: \"Dokument\",\n    similarity_match: \"zápas\",\n    source_count_one: \"{{count}} – odkaz\",\n    source_count_other: \"{{count}} – odkazy\",\n    preset_exit_description: \"Zastavte aktuální relaci s agentem\",\n    add_new: \"Přidat nové\",\n    edit: \"Upravit\",\n    publish: \"Publikovat\",\n    stop_generating: \"Zastavte generování odpovědi\",\n    pause_tts_speech_message:\n      \"Zastavte čtení textu pomocí syntetické řeči z tohoto zprávy.\",\n    slash_commands: \"Příkazy v řádku\",\n    agent_skills: \"Dovednosti agenta\",\n    manage_agent_skills: \"Řízení dovedností agentů\",\n    agent_skills_disabled_in_session:\n      \"Není možné upravovat dovednosti během aktivního sezení s agentem. Nejprve použijte příkaz `/exit` pro ukončení sezení.\",\n    start_agent_session: \"Spustit relaci s agentem\",\n    use_agent_session_to_use_tools:\n      \"Můžete využít nástroje v chatu spuštěním sezení s agentem pomocí příkazu '@agent' na začátku vašeho vstupu.\",\n  },\n  profile_settings: {\n    edit_account: \"Upravit účet\",\n    profile_picture: \"Profilový obrázek\",\n    remove_profile_picture: \"Odebrat profilový obrázek\",\n    username: \"Uživatelské jméno\",\n    new_password: \"Nové heslo\",\n    password_description: \"Heslo musí mít délku alespoň 8 znaků\",\n    cancel: \"Zrušit\",\n    update_account: \"Aktualizovat účet\",\n    theme: \"Preferovaný motiv\",\n    language: \"Preferovaný jazyk\",\n    failed_upload: \"Nepodařilo se nahrát profilový obrázek: {{error}}\",\n    upload_success: \"Profilový obrázek nahrán.\",\n    failed_remove: \"Nepodařilo se odebrat profilový obrázek: {{error}}\",\n    profile_updated: \"Profil aktualizován.\",\n    failed_update_user: \"Nepodařilo se aktualizovat uživatele: {{error}}\",\n    account: \"Účet\",\n    support: \"Podpora\",\n    signout: \"Odhlásit\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Klávesové zkratky\",\n    shortcuts: {\n      settings: \"Otevřít nastavení\",\n      workspaceSettings: \"Otevřít nastavení aktuálního pracovního prostoru\",\n      home: \"Přejít domů\",\n      workspaces: \"Spravovat pracovní prostory\",\n      apiKeys: \"Nastavení API klíčů\",\n      llmPreferences: \"Preference LLM\",\n      chatSettings: \"Nastavení chatu\",\n      help: \"Zobrazit nápovědu klávesových zkratek\",\n      showLLMSelector: \"Zobrazit výběr LLM pracovního prostoru\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Úspěch!\",\n        success_description:\n          \"Vaše systémová výzva byla publikována do komunitního centra!\",\n        success_thank_you: \"Děkujeme za sdílení s komunitou!\",\n        view_on_hub: \"Zobrazit v komunitním centru\",\n        modal_title: \"Publikovat systémovou výzvu\",\n        name_label: \"Název\",\n        name_description: \"Toto je zobrazovaný název vaší systémové výzvy.\",\n        name_placeholder: \"Moje systémová výzva\",\n        description_label: \"Popis\",\n        description_description:\n          \"Toto je popis vaší systémové výzvy. Použijte k popisu účelu vaší systémové výzvy.\",\n        tags_label: \"Štítky\",\n        tags_description:\n          \"Štítky slouží k označení vaší systémové výzvy pro snadnější vyhledávání. Můžete přidat více štítků. Max 5 štítků. Max 20 znaků na štítek.\",\n        tags_placeholder: \"Zadejte a stiskněte Enter pro přidání štítků\",\n        visibility_label: \"Viditelnost\",\n        public_description: \"Veřejné systémové výzvy jsou viditelné všem.\",\n        private_description:\n          \"Soukromé systémové výzvy jsou viditelné pouze vám.\",\n        publish_button: \"Publikovat do komunitního centra\",\n        submitting: \"Publikování...\",\n        prompt_label: \"Výzva\",\n        prompt_description:\n          \"Toto je skutečná systémová výzva, která bude použita k vedení LLM.\",\n        prompt_placeholder: \"Zadejte svou systémovou výzvu zde...\",\n      },\n      agent_flow: {\n        success_title: \"Úspěch!\",\n        success_description:\n          \"Váš tok agenta byl publikován do komunitního centra!\",\n        success_thank_you: \"Děkujeme za sdílení s komunitou!\",\n        view_on_hub: \"Zobrazit v komunitním centru\",\n        modal_title: \"Publikovat tok agenta\",\n        name_label: \"Název\",\n        name_description: \"Toto je zobrazovaný název vašeho toku agenta.\",\n        name_placeholder: \"Můj tok agenta\",\n        description_label: \"Popis\",\n        description_description:\n          \"Toto je popis vašeho toku agenta. Použijte k popisu účelu vašeho toku agenta.\",\n        tags_label: \"Štítky\",\n        tags_description:\n          \"Štítky slouží k označení vašeho toku agenta pro snadnější vyhledávání. Můžete přidat více štítků. Max 5 štítků. Max 20 znaků na štítek.\",\n        tags_placeholder: \"Zadejte a stiskněte Enter pro přidání štítků\",\n        visibility_label: \"Viditelnost\",\n        submitting: \"Publikování...\",\n        submit: \"Publikovat do komunitního centra\",\n        privacy_note:\n          \"Toky agentů jsou vždy nahrávány jako soukromé pro ochranu jakýchkoli citlivých dat. Viditelnost můžete změnit v komunitním centru po publikování. Prosím ověřte, že váš tok neobsahuje žádné citlivé nebo soukromé informace před publikováním.\",\n      },\n      slash_command: {\n        success_title: \"Úspěch!\",\n        success_description:\n          \"Váš lomítkový příkaz byl publikován do komunitního centra!\",\n        success_thank_you: \"Děkujeme za sdílení s komunitou!\",\n        view_on_hub: \"Zobrazit v komunitním centru\",\n        modal_title: \"Publikovat lomítkový příkaz\",\n        name_label: \"Název\",\n        name_description:\n          \"Toto je zobrazovaný název vašeho lomítkového příkazu.\",\n        name_placeholder: \"Můj lomítkový příkaz\",\n        description_label: \"Popis\",\n        description_description:\n          \"Toto je popis vašeho lomítkového příkazu. Použijte k popisu účelu vašeho lomítkového příkazu.\",\n        tags_label: \"Štítky\",\n        tags_description:\n          \"Štítky slouží k označení vašeho lomítkového příkazu pro snadnější vyhledávání. Můžete přidat více štítků. Max 5 štítků. Max 20 znaků na štítek.\",\n        tags_placeholder: \"Zadejte a stiskněte Enter pro přidání štítků\",\n        visibility_label: \"Viditelnost\",\n        public_description: \"Veřejné lomítkové příkazy jsou viditelné všem.\",\n        private_description:\n          \"Soukromé lomítkové příkazy jsou viditelné pouze vám.\",\n        publish_button: \"Publikovat do komunitního centra\",\n        submitting: \"Publikování...\",\n        prompt_label: \"Výzva\",\n        prompt_description:\n          \"Toto je výzva, která bude použita při spuštění lomítkového příkazu.\",\n        prompt_placeholder: \"Zadejte svou výzvu zde...\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Vyžadováno ověření\",\n          description:\n            \"Musíte se ověřit pomocí komunitního centra AnythingLLM před publikováním položek.\",\n          button: \"Připojit se ke komunitnímu centru\",\n        },\n      },\n    },\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/da/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"Velkommen til\",\n      getStarted: \"Kom godt i gang\",\n    },\n    llm: {\n      title: \"LLM-præference\",\n      description:\n        \"AnythingLLM kan arbejde med mange LLM-udbydere. Dette vil være den tjeneste, der håndterer chat.\",\n    },\n    userSetup: {\n      title: \"Brugeropsætning\",\n      description: \"Konfigurer dine brugerindstillinger.\",\n      howManyUsers: \"Hvor mange brugere vil benytte denne instans?\",\n      justMe: \"Kun mig\",\n      myTeam: \"Mit team\",\n      instancePassword: \"Instansadgangskode\",\n      setPassword: \"Vil du oprette en adgangskode?\",\n      passwordReq: \"Adgangskoder skal være på mindst 8 tegn.\",\n      passwordWarn:\n        \"Det er vigtigt at gemme denne adgangskode, da der ikke findes nogen metode til genoprettelse.\",\n      adminUsername: \"Brugernavn til admin-konto\",\n      adminPassword: \"Adgangskode til admin-konto\",\n      adminPasswordReq: \"Adgangskoder skal være på mindst 8 tegn.\",\n      teamHint:\n        \"Som standard vil du være den eneste administrator. Når onboarding er fuldført, kan du oprette og invitere andre til at blive brugere eller administratorer. Glem ikke din adgangskode, da kun administratorer kan nulstille adgangskoder.\",\n    },\n    data: {\n      title: \"Datahåndtering & Privatliv\",\n      description:\n        \"Vi er forpligtet til gennemsigtighed og kontrol, når det gælder dine persondata.\",\n      settingsHint:\n        \"Disse indstillinger kan ændres når som helst under indstillingerne.\",\n    },\n    survey: {\n      title: \"Velkommen til AnythingLLM\",\n      description:\n        \"Hjælp os med at gøre AnythingLLM tilpasset dine behov. Valgfrit.\",\n      email: \"Hvad er din e-mail?\",\n      useCase: \"Hvad vil du bruge AnythingLLM til?\",\n      useCaseWork: \"Til arbejde\",\n      useCasePersonal: \"Til personligt brug\",\n      useCaseOther: \"Andet\",\n      comment: \"Hvordan hørte du om AnythingLLM?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube, etc. - Fortæl os, hvordan du fandt os!\",\n      skip: \"Spring undersøgelsen over\",\n      thankYou: \"Tak for din feedback!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Navn på arbejdsområder\",\n    user: \"Bruger\",\n    selection: \"Modelvalg\",\n    saving: \"Gemmer...\",\n    save: \"Gem ændringer\",\n    previous: \"Forrige side\",\n    next: \"Næste side\",\n    optional: \"Valgfrit\",\n    yes: \"Ja\",\n    no: \"Nej\",\n    search: \"Søg\",\n    username_requirements:\n      \"Brugernavnet skal bestå af 2-32 tegn, starte med et lille bogstav, og kun indeholde små bogstaver, tal, understregninger, bindestreger og punktummer.\",\n    on: \"Om\",\n    none: \"Ingen\",\n    stopped: \"Stoppet\",\n    loading: \"Indlæsning\",\n    refresh: \"Opfrisk\",\n  },\n  settings: {\n    title: \"Instansindstillinger\",\n    invites: \"Invitationer\",\n    users: \"Brugere\",\n    workspaces: \"Arbejdsområder\",\n    \"workspace-chats\": \"Arbejdsområde-chat\",\n    customization: \"Tilpasning\",\n    \"api-keys\": \"Udvikler API\",\n    llm: \"LLM\",\n    transcription: \"Transskription\",\n    embedder: \"Indlejring\",\n    \"text-splitting\": \"Tekst-splitter og opdeling\",\n    \"voice-speech\": \"Stemme & Tale\",\n    \"vector-database\": \"Vektordatabase\",\n    embeds: \"Chat-indlejring\",\n    security: \"Sikkerhed\",\n    \"event-logs\": \"Hændelseslog\",\n    privacy: \"Privatliv & Data\",\n    \"ai-providers\": \"AI-udbydere\",\n    \"agent-skills\": \"Agentfærdigheder\",\n    admin: \"Administrator\",\n    tools: \"Værktøjer\",\n    \"experimental-features\": \"Eksperimentelle funktioner\",\n    contact: \"Kontakt support\",\n    \"browser-extension\": \"Browserudvidelse\",\n    \"system-prompt-variables\":\n      \"System Prompt Variables\\n\\nSystem Prompt Variabler\",\n    interface: \"Brugerpræferencer\",\n    branding: \"Brandstrategi og white-labeling\",\n    chat: \"Chat\",\n    \"mobile-app\": \"AnythingLLM Mobile\",\n    \"community-hub\": {\n      title: \"Fælleshus\",\n      trending: \"Udforsk populære emner\",\n      \"your-account\": \"Dit konti\",\n      \"import-item\": \"Importeret vare\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Velkommen til\",\n      \"placeholder-username\": \"Brugernavn\",\n      \"placeholder-password\": \"Adgangskode\",\n      login: \"Log ind\",\n      validating: \"Validerer...\",\n      \"forgot-pass\": \"Glemt adgangskode\",\n      reset: \"Nulstil\",\n    },\n    \"sign-in\": \"Log ind på din {{appName}} konto.\",\n    \"password-reset\": {\n      title: \"Nulstilling af adgangskode\",\n      description:\n        \"Angiv de nødvendige oplysninger nedenfor for at nulstille din adgangskode.\",\n      \"recovery-codes\": \"Gendannelseskoder\",\n      \"back-to-login\": \"Tilbage til log ind\",\n    },\n  },\n  \"new-workspace\": {\n    title: \"Nyt arbejdsområde\",\n    placeholder: \"Mit arbejdsområde\",\n  },\n  \"workspaces—settings\": {\n    general: \"Generelle indstillinger\",\n    chat: \"Chatindstillinger\",\n    vector: \"Vektordatabase\",\n    members: \"Medlemmer\",\n    agent: \"Agentkonfiguration\",\n  },\n  general: {\n    vector: {\n      title: \"Antal vektorer\",\n      description: \"Samlet antal vektorer i din vektordatabase.\",\n    },\n    names: {\n      description: \"Dette vil kun ændre visningsnavnet på dit arbejdsområde.\",\n    },\n    message: {\n      title: \"Foreslåede chatbeskeder\",\n      description:\n        \"Tilpas de beskeder, der vil blive foreslået til brugerne af dit arbejdsområde.\",\n      add: \"Tilføj ny besked\",\n      save: \"Gem beskeder\",\n      heading: \"Forklar mig\",\n      body: \"fordelene ved AnythingLLM\",\n    },\n    delete: {\n      title: \"Slet arbejdsområde\",\n      description:\n        \"Slet dette arbejdsområde og alle dets data. Dette vil slette arbejdsområdet for alle brugere.\",\n      delete: \"Slet arbejdsområde\",\n      deleting: \"Sletter arbejdsområde...\",\n      \"confirm-start\": \"Du er ved at slette dit hele\",\n      \"confirm-end\":\n        \"arbejdsområde. Dette vil fjerne alle vektor-indlejringer i din vektordatabase.\\n\\nDe oprindelige kildefiler forbliver uberørte. Denne handling kan ikke fortrydes.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Arbejdsområdets LLM-udbyder\",\n      description:\n        \"Den specifikke LLM-udbyder og -model, der vil blive brugt for dette arbejdsområde. Som standard anvendes systemets LLM-udbyder og indstillinger.\",\n      search: \"Søg blandt alle LLM-udbydere\",\n    },\n    model: {\n      title: \"Arbejdsområdets chatmodel\",\n      description:\n        \"Den specifikke chatmodel, der vil blive brugt for dette arbejdsområde. Hvis tom, anvendes systemets LLM-præference.\",\n    },\n    mode: {\n      title: \"Chat-tilstand\",\n      chat: {\n        title: \"Chat\",\n        description:\n          'vil give svar baseret på LLM\\'s generelle viden og den relevante kontekst fra dokumentet. Du skal bruge kommandoen \"@agent\" for at bruge værktøjerne.',\n      },\n      query: {\n        title: \"Forespørgsel\",\n        description:\n          \"vil give svar <b>kun</b>, hvis dokumentets kontekst er fundet.<br />Du skal bruge kommandoen @agent for at bruge værktøjerne.\",\n      },\n      automatic: {\n        title: \"Bil\",\n        description:\n          'vil automatisk bruge værktøjer, hvis modellen og leverandøren understøtter native værktøjskald.<br />Hvis native værktøjskald ikke understøttes, skal du bruge kommandoen \"@agent\" for at bruge værktøjer.',\n      },\n    },\n    history: {\n      title: \"Chat-historik\",\n      \"desc-start\":\n        \"Antallet af tidligere chats, der vil blive inkluderet i svarens korttidshukommelse.\",\n      recommend: \"Anbefal 20. \",\n      \"desc-end\":\n        \"Alt over 45 kan sandsynligvis føre til gentagne chat-fejl afhængigt af beskedstørrelsen.\",\n    },\n    prompt: {\n      title: \"Prompt\",\n      description:\n        \"Prompten, der vil blive brugt i dette arbejdsområde. Definér konteksten og instruktionerne til, at AI'en kan generere et svar. Du bør levere en omhyggeligt udformet prompt, så AI'en kan generere et relevant og præcist svar.\",\n      history: {\n        title:\n          \"System Prompt History\\n\\nHistorikken over system prompts er gemt i en fil, der er placeret i din lokale mappe.\\nDu kan få adgang til historikken ved at åbne filen og læse indholdet.\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\\n<|file_name|>system_prompt_history.txt\",\n        clearAll: \"Ryd alt\",\n        noHistory: \"Ingen historik over systemprompt er tilgængelig.\",\n        restore: \"Genopret\",\n        delete: \"Slet\",\n        deleteConfirm:\n          \"Er du sikker på, at du vil slette dette historikelement?\",\n        clearAllConfirm:\n          \"Er du sikker på, at du vil slette al historik? Denne handling kan ikke fortrydes.\",\n        expand: \"Udvid\",\n        publish: \"Publicer på Community Hub\",\n      },\n    },\n    refusal: {\n      title: \"Afvisningssvar for forespørgsels-tilstand\",\n      \"desc-start\": \"Når du er i\",\n      query: \"forespørgsels-tilstand\",\n      \"desc-end\":\n        \"tilstand, kan du vælge at returnere et brugerdefineret afvisningssvar, når der ikke findes nogen kontekst.\",\n      \"tooltip-title\": \"Hvorfor ser jeg dette?\",\n      \"tooltip-description\":\n        \"Du er i forespørgselsmodus, hvilket kun bruger information fra dine dokumenter. Skift til chat-modus for mere fleksible samtaler, eller klik her for at besøge vores dokumentation og lære mere om chat-moduser.\",\n    },\n    temperature: {\n      title: \"LLM-temperatur\",\n      \"desc-start\":\n        'Denne indstilling styrer, hvor \"kreative\" dine LLM-svar vil være.',\n      \"desc-end\":\n        \"Jo højere tallet er, desto mere kreative bliver svarene. For nogle modeller kan for høje værdier føre til usammenhængende svar.\",\n      hint: \"De fleste LLM'er har forskellige acceptable intervaller for gyldige værdier. Konsulter din LLM-udbyder for den information.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Identifikator for vektordatabase\",\n    snippets: {\n      title: \"Maksimalt antal kontekstuddrag\",\n      description:\n        \"Denne indstilling styrer det maksimale antal kontekstuddrag, der vil blive sendt til LLM'en pr. chat eller forespørgsel.\",\n      recommend: \"Anbefalet: 4\",\n    },\n    doc: {\n      title: \"Tærskel for dokuments lighed\",\n      description:\n        \"Den minimale lighedsscore, der kræves for, at en kilde betragtes som relateret til chatten. Jo højere tallet er, desto mere lig skal kilden være chatten.\",\n      zero: \"Ingen begrænsning\",\n      low: \"Lav (lighedsscore ≥ 0,25)\",\n      medium: \"Middel (lighedsscore ≥ 0,50)\",\n      high: \"Høj (lighedsscore ≥ 0,75)\",\n    },\n    reset: {\n      reset: \"Nulstil vektordatabase\",\n      resetting: \"Rydder vektorer...\",\n      confirm:\n        \"Du er ved at nulstille dette arbejdsområdes vektordatabase. Dette vil fjerne alle vektor-indlejringer, der aktuelt er indlejret.\\n\\nDe oprindelige kildefiler forbliver uberørte. Denne handling kan ikke fortrydes.\",\n      error: \"Kunne ikke nulstille arbejdsområdets vektordatabase!\",\n      success: \"Arbejdsområdets vektordatabase blev nulstillet!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"Ydeevnen for LLM'er, der ikke eksplicit understøtter værktøjskald, er i høj grad afhængig af modellens kapacitet og nøjagtighed. Nogle funktioner kan være begrænsede eller ikke-fungerende.\",\n    provider: {\n      title: \"Arbejdsområdets agent LLM-udbyder\",\n      description:\n        \"Den specifikke LLM-udbyder og -model, der vil blive brugt for dette arbejdsområdes @agent-agent.\",\n    },\n    mode: {\n      chat: {\n        title: \"Arbejdsområdets agent chatmodel\",\n        description:\n          \"Den specifikke chatmodel, der vil blive brugt for dette arbejdsområdes @agent-agent.\",\n      },\n      title: \"Arbejdsområdets agentmodel\",\n      description:\n        \"Den specifikke LLM-model, der vil blive brugt for dette arbejdsområdes @agent-agent.\",\n      wait: \"-- venter på modeller --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG & langtidshukommelse\",\n        description:\n          'Giv agenten mulighed for at udnytte dine lokale dokumenter til at besvare en forespørgsel eller få agenten til at \"huske\" dele af indhold for langtidshukommelse.',\n      },\n      view: {\n        title: \"Se og opsummér dokumenter\",\n        description:\n          \"Giv agenten mulighed for at liste og opsummere indholdet af de filer i arbejdsområdet, der aktuelt er indlejret.\",\n      },\n      scrape: {\n        title: \"Scrape hjemmesider\",\n        description:\n          \"Giv agenten mulighed for at besøge og scrape indholdet fra hjemmesider.\",\n      },\n      generate: {\n        title: \"Generer diagrammer\",\n        description:\n          \"Gør det muligt for standardagenten at generere forskellige typer diagrammer fra data, der leveres eller gives i chat.\",\n      },\n      save: {\n        title: \"Generer og gem filer i browseren\",\n        description:\n          \"Gør det muligt for standardagenten at generere og skrive til filer, der gemmes og kan downloades i din browser.\",\n      },\n      web: {\n        title: \"Live web-søgning og browsing\",\n        description:\n          \"Giv din agent mulighed for at søge på internettet for at besvare dine spørgsmål ved at forbinde den til en web-søgetjeneste (SERP).\",\n      },\n      sql: {\n        title: \"SQL-forbindelse\",\n        description:\n          \"Giv din agent mulighed for at bruge SQL til at besvare dine spørgsmål ved at oprette forbindelse til forskellige SQL-databaseleverandører.\",\n      },\n      default_skill:\n        \"Som standard er denne funktion aktiveret, men du kan deaktivere den, hvis du ikke ønsker, at den skal være tilgængelig for agenten.\",\n    },\n    mcp: {\n      title: \"MCP-servere\",\n      \"loading-from-config\": \"Indlæsning af MCP-servere fra konfigurationsfil\",\n      \"learn-more\": \"Lær mere om MCP-servere.\",\n      \"no-servers-found\": \"Ingen MCP-servere fundet\",\n      \"tool-warning\":\n        \"For den bedste ydeevne, overvej at deaktivere unødvendige værktøjer for at bevare konteksten.\",\n      \"stop-server\": \"Afbryd MCP-serveren\",\n      \"start-server\": \"Start MCP-serveren\",\n      \"delete-server\": \"Slet MCP-serveren\",\n      \"tool-count-warning\":\n        \"Denne MCP-server har <b>aktiverede</b>værktøjer, som vil forbruge kontekst i hvert chat-session.<br />Overvej at deaktivere uønskede værktøjer for at spare på konteksten.\",\n      \"startup-command\": \"Startkommando\",\n      command: \"Instruktion\",\n      arguments: \"Argumenter\",\n      \"not-running-warning\":\n        \"Denne MCP-server kører ikke – den kan være stoppet, eller den kan opleve fejl ved opstart.\",\n      \"tool-call-arguments\": \"Argumenter til værktøjsopkald\",\n      \"tools-enabled\": \"værktøjer er aktiveret\",\n    },\n    settings: {\n      title: \"Indstillinger for agenters færdigheder\",\n      \"max-tool-calls\": {\n        title: \"Maksimalt antal anmodninger pr. svar\",\n        description:\n          \"Det maksimale antal værktøjer, en agent kan kæde sammen for at generere et enkelt svar. Dette forhindrer, at værktøjer kaldes unødvendigt, og undgår uendelige løkker.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Intelligent valg af færdigheder\",\n        \"beta-badge\": \"Beta\",\n        description:\n          \"Aktiver ubegrænsede værktøjer og reducer brugen af cut-tokens med op til 80 % pr. forespørgsel – AnythingLLM vælger automatisk de relevante færdigheder til hver forespørgsel.\",\n        \"max-tools\": {\n          title: \"Max Tools\",\n          description:\n            \"Det maksimale antal værktøjer, der kan vælges for hver forespørgsel. Vi anbefaler at indstille dette til højere værdier for større modeller med mere kontekst.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Arbejdsområde-chat\",\n    description:\n      \"Dette er alle de optagede chats og beskeder, der er blevet sendt af brugere, sorteret efter oprettelsesdato.\",\n    export: \"Eksporter\",\n    table: {\n      id: \"Id\",\n      by: \"Sendt af\",\n      workspace: \"Arbejdsområde\",\n      prompt: \"Prompt\",\n      response: \"Svar\",\n      at: \"Sendt kl.\",\n    },\n  },\n  api: {\n    title: \"API-nøgler\",\n    description:\n      \"API-nøgler giver indehaveren mulighed for programmatisk at få adgang til og administrere denne AnythingLLM-instans.\",\n    link: \"Læs API-dokumentationen\",\n    generate: \"Generér ny API-nøgle\",\n    table: {\n      key: \"API-nøgle\",\n      by: \"Oprettet af\",\n      created: \"Oprettet\",\n    },\n  },\n  llm: {\n    title: \"LLM-præference\",\n    description:\n      \"Disse er legitimationsoplysningerne og indstillingerne for din foretrukne LLM chat- og indlejringsudbyder. Det er vigtigt, at disse nøgler er opdaterede og korrekte, ellers vil AnythingLLM ikke fungere korrekt.\",\n    provider: \"LLM-udbyder\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Azure Service Endpoint\",\n        api_key: \"API-nøgle\",\n        chat_deployment_name: \"Chat Deployment Name\",\n        chat_model_token_limit:\n          \"Chat Model Token Limit\\n\\nBegrænsning af antallet af tokens i en chatmodel.\",\n        model_type: \"Modeltype\",\n        default: \"Standard\",\n        reasoning: \"Begrundelse\",\n        model_type_tooltip:\n          'Hvis din implementering bruger en ræsonnementsmodel (o1, o1-mini, o3-mini osv.), skal du indstille dette til \"Ræsonnement\". Ellers kan dine chat-anmodninger mislykkes.',\n      },\n    },\n  },\n  transcription: {\n    title: \"Foretrukken transskriptionsmodel\",\n    description:\n      \"Disse er legitimationsoplysningerne og indstillingerne for din foretrukne transskriptionsmodeludbyder. Det er vigtigt, at disse nøgler er opdaterede og korrekte, ellers vil mediefiler og lyd ikke blive transskriberet.\",\n    provider: \"Transskriptionsudbyder\",\n    \"warn-start\":\n      \"Brug af den lokale whisper-model på maskiner med begrænset RAM eller CPU kan få AnythingLLM til at gå i stå under behandling af mediefiler.\",\n    \"warn-recommend\": \"Vi anbefaler mindst 2GB RAM og upload af filer <10Mb.\",\n    \"warn-end\":\n      \"Den indbyggede model vil automatisk blive downloadet ved første brug.\",\n  },\n  embedding: {\n    title: \"Foretrukken indlejringsmetode\",\n    \"desc-start\":\n      \"Når du bruger en LLM, der ikke understøtter en indlejringsmotor natively, skal du muligvis yderligere angive legitimationsoplysninger til indlejring af tekst.\",\n    \"desc-end\":\n      \"Indlejring er processen med at omdanne tekst til vektorer. Disse legitimationsoplysninger er nødvendige for at omdanne dine filer og prompts til et format, som AnythingLLM kan bruge til behandling.\",\n    provider: {\n      title: \"Indlejringsudbyder\",\n    },\n  },\n  text: {\n    title: \"Præferencer for tekstopdeling & segmentering\",\n    \"desc-start\":\n      \"Nogle gange vil du måske ændre den standardmåde, som nye dokumenter deles og opdeles i bidder, inden de indsættes i din vektordatabase.\",\n    \"desc-end\":\n      \"Du bør kun ændre denne indstilling, hvis du forstår, hvordan tekstopdeling fungerer og dens bivirkninger.\",\n    size: {\n      title: \"Størrelse på tekstbidder\",\n      description:\n        \"Dette er den maksimale længde af tegn, der kan være i en enkelt vektor.\",\n      recommend: \"Indlejringsmodellens maksimale længde er\",\n    },\n    overlap: {\n      title: \"Overlap mellem tekstbidder\",\n      description:\n        \"Dette er det maksimale overlap af tegn, der forekommer ved opdeling mellem to tilstødende tekstbidder.\",\n    },\n  },\n  vector: {\n    title: \"Vektordatabase\",\n    description:\n      \"Disse er legitimationsoplysningerne og indstillingerne for, hvordan din AnythingLLM-instans vil fungere. Det er vigtigt, at disse nøgler er opdaterede og korrekte.\",\n    provider: {\n      title: \"Vektordatabaseudbyder\",\n      description: \"Ingen konfiguration er nødvendig for LanceDB.\",\n    },\n  },\n  embeddable: {\n    title: \"Indlejrede chatwidgets\",\n    description:\n      \"Indlejrede chatwidgets er offentligt tilgængelige chatgrænseflader, der er knyttet til et enkelt arbejdsområde. Disse giver dig mulighed for at opbygge arbejdsområder, som du derefter kan offentliggøre for verden.\",\n    create: \"Opret indlejring\",\n    table: {\n      workspace: \"Arbejdsområde\",\n      chats: \"Sendte chats\",\n      active: \"Aktive domæner\",\n      created: \"Oprettet\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Indlejrede chats\",\n    export: \"Eksporter\",\n    description:\n      \"Dette er alle de optagede chats og beskeder fra enhver indlejring, du har offentliggjort.\",\n    table: {\n      embed: \"Indlejring\",\n      sender: \"Afsender\",\n      message: \"Besked\",\n      response: \"Svar\",\n      at: \"Sendt kl.\",\n    },\n  },\n  event: {\n    title: \"Hændelseslog\",\n    description:\n      \"Se alle handlinger og hændelser, der sker på denne instans for overvågning.\",\n    clear: \"Ryd hændelseslog\",\n    table: {\n      type: \"Hændelsestype\",\n      user: \"Bruger\",\n      occurred: \"Skete kl.\",\n    },\n  },\n  privacy: {\n    title: \"Privatliv & datahåndtering\",\n    description:\n      \"Dette er din konfiguration for, hvordan tilsluttede tredjepartsudbydere og AnythingLLM håndterer dine data.\",\n    anonymous: \"Anonym telemetri aktiveret\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Søg efter datakonnektorer\",\n    \"no-connectors\": \"Ingen datakonnektorer fundet.\",\n    github: {\n      name: \"GitHub-repository\",\n      description:\n        \"Importer et helt offentligt eller privat GitHub-repository med et enkelt klik.\",\n      URL: \"GitHub-repository URL\",\n      URL_explained: \"URL til det GitHub-repository, du ønsker at indsamle.\",\n      token: \"GitHub-adgangstoken\",\n      optional: \"valgfrit\",\n      token_explained: \"Adgangstoken for at undgå hastighedsbegrænsning.\",\n      token_explained_start: \"Uden en \",\n      token_explained_link1: \"Personlig adgangstoken\",\n      token_explained_middle:\n        \", kan GitHub API'en begrænse antallet af filer, der kan indsamles på grund af ratebegrænsning. Du kan \",\n      token_explained_link2: \"oprette en midlertidig adgangstoken\",\n      token_explained_end: \" for at undgå dette problem.\",\n      ignores: \"Fil-ignoreringer\",\n      git_ignore:\n        \"Liste i .gitignore-format for at ignorere specifikke filer under indsamling. Tryk enter efter hver post, du vil gemme.\",\n      task_explained:\n        \"Når færdig, vil alle filer være tilgængelige for indlejring i arbejdsområder i dokumentvælgeren.\",\n      branch: \"Den gren, du ønsker at indsamle filer fra.\",\n      branch_loading: \"-- indlæser tilgængelige grene --\",\n      branch_explained: \"Den gren, du ønsker at indsamle filer fra.\",\n      token_information:\n        \"Uden at udfylde <b>GitHub-adgangstoken</b> vil denne datakonnektor kun kunne indsamle <b>topniveau</b> filer fra repoet på grund af GitHubs offentlige API-ratebegrænsninger.\",\n      token_personal:\n        \"Få en gratis personlig adgangstoken med en GitHub-konto her.\",\n    },\n    gitlab: {\n      name: \"GitLab-repository\",\n      description:\n        \"Importer et helt offentligt eller privat GitLab-repository med et enkelt klik.\",\n      URL: \"GitLab-repository URL\",\n      URL_explained: \"URL til det GitLab-repository, du ønsker at indsamle.\",\n      token: \"GitLab-adgangstoken\",\n      optional: \"valgfrit\",\n      token_description: \"Vælg yderligere enheder at hente fra GitLab API'en.\",\n      token_explained_start: \"Uden en \",\n      token_explained_link1: \"personlig adgangstoken\",\n      token_explained_middle:\n        \", kan GitLab API'en begrænse antallet af filer, der kan indsamles på grund af ratebegrænsning. Du kan \",\n      token_explained_link2: \"oprette en midlertidig adgangstoken\",\n      token_explained_end: \" for at undgå dette problem.\",\n      fetch_issues: \"Hent issues som dokumenter\",\n      ignores: \"Fil-ignoreringer\",\n      git_ignore:\n        \"Liste i .gitignore-format for at ignorere specifikke filer under indsamling. Tryk enter efter hver post, du vil gemme.\",\n      task_explained:\n        \"Når færdig, vil alle filer være tilgængelige for indlejring i arbejdsområder i dokumentvælgeren.\",\n      branch: \"Den gren, du ønsker at indsamle filer fra\",\n      branch_loading: \"-- indlæser tilgængelige grene --\",\n      branch_explained: \"Den gren, du ønsker at indsamle filer fra.\",\n      token_information:\n        \"Uden at udfylde <b>GitLab-adgangstoken</b> vil denne datakonnektor kun kunne indsamle <b>topniveau</b> filer fra repoet på grund af GitLabs offentlige API-ratebegrænsninger.\",\n      token_personal:\n        \"Få en gratis personlig adgangstoken med en GitLab-konto her.\",\n    },\n    youtube: {\n      name: \"YouTube-transskription\",\n      description:\n        \"Importer transskriptionen af en hel YouTube-video fra et link.\",\n      URL: \"YouTube-video URL\",\n      URL_explained_start:\n        \"Indtast URL'en til en hvilken som helst YouTube-video for at hente dens transskription. Videoen skal have \",\n      URL_explained_link: \"undertekster\",\n      URL_explained_end: \" tilgængelige.\",\n      task_explained:\n        \"Når færdig, vil transskriptionen være tilgængelig for indlejring i arbejdsområder i dokumentvælgeren.\",\n    },\n    \"website-depth\": {\n      name: \"Bulk link-scraper\",\n      description:\n        \"Scrape en hjemmeside og dens under-links op til en vis dybde.\",\n      URL: \"Hjemmeside URL\",\n      URL_explained: \"URL til den hjemmeside, du ønsker at scrape.\",\n      depth: \"Gennemsøgningsdybde\",\n      depth_explained:\n        \"Dette er antallet af under-links, som arbejderen skal følge fra oprindelses-URL'en.\",\n      max_pages: \"Maksimalt antal sider\",\n      max_pages_explained: \"Maksimalt antal links, der skal scrapes.\",\n      task_explained:\n        \"Når færdig, vil alt scraped indhold være tilgængeligt for indlejring i arbejdsområder i dokumentvælgeren.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Importer en hel Confluence-side med et enkelt klik.\",\n      deployment_type: \"Confluence-udrulningstype\",\n      deployment_type_explained:\n        \"Bestem om din Confluence-instans er hostet på Atlassian Cloud eller selvhostet.\",\n      base_url: \"Confluence-basis URL\",\n      base_url_explained: \"Dette er basis-URL'en for dit Confluence-område.\",\n      space_key: \"Confluence-områdenøgle\",\n      space_key_explained:\n        \"Dette er nøglekoden for dit Confluence-område, som vil blive brugt. Begynder typisk med ~\",\n      username: \"Confluence-brugernavn\",\n      username_explained: \"Dit Confluence-brugernavn\",\n      auth_type: \"Confluence godkendelsestype\",\n      auth_type_explained:\n        \"Vælg den godkendelsestype, du ønsker at bruge for at få adgang til dine Confluence-sider.\",\n      auth_type_username: \"Brugernavn og adgangstoken\",\n      auth_type_personal: \"Personlig adgangstoken\",\n      token: \"Confluence-adgangstoken\",\n      token_explained_start:\n        \"Du skal angive en adgangstoken for godkendelse. Du kan generere en adgangstoken\",\n      token_explained_link: \"her\",\n      token_desc: \"Adgangstoken til godkendelse\",\n      pat_token: \"Confluence personlig adgangstoken\",\n      pat_token_explained: \"Din personlige Confluence-adgangstoken.\",\n      task_explained:\n        \"Når færdig, vil sideindholdet være tilgængeligt for indlejring i arbejdsområder i dokumentvælgeren.\",\n      bypass_ssl: \"Omgå SSL-certifikatvalidering\",\n      bypass_ssl_explained:\n        \"Aktiver denne mulighed for at omgå valideringen af SSL-certifikatet for selv-hostede Confluence-instanser med et selv-underskrevet certifikat.\",\n    },\n    manage: {\n      documents: \"Dokumenter\",\n      \"data-connectors\": \"Datakonnektorer\",\n      \"desktop-only\":\n        \"Redigering af disse indstillinger er kun tilgængelig på en stationær enhed. Venligst tilgå denne side fra din stationære computer for at fortsætte.\",\n      dismiss: \"Afvis\",\n      editing: \"Redigerer\",\n    },\n    directory: {\n      \"my-documents\": \"Mine dokumenter\",\n      \"new-folder\": \"Ny mappe\",\n      \"search-document\": \"Søg efter dokument\",\n      \"no-documents\": \"Ingen dokumenter\",\n      \"move-workspace\": \"Flyt til arbejdsområde\",\n      \"delete-confirmation\":\n        \"Er du sikker på, at du vil slette disse filer og mapper?\\nDette vil fjerne filerne fra systemet og automatisk fjerne dem fra alle eksisterende arbejdsområder.\\nDenne handling kan ikke fortrydes.\",\n      \"removing-message\":\n        \"Fjerner {{count}} dokumenter og {{folderCount}} mapper. Vent venligst.\",\n      \"move-success\": \"Flyttede {{count}} dokumenter med succes.\",\n      no_docs: \"Ingen dokumenter\",\n      select_all: \"Vælg alle\",\n      deselect_all: \"Fravælg alle\",\n      remove_selected: \"Fjern valgte\",\n      costs: \"*Engangsomkostning for indlejringer\",\n      save_embed: \"Gem og indlejr\",\n      \"total-documents_one\": \"{{count}} dokument\",\n      \"total-documents_other\": \"{{count}} dokumenter\",\n    },\n    upload: {\n      \"processor-offline\": \"Dokumentbehandler utilgængelig\",\n      \"processor-offline-desc\":\n        \"Vi kan ikke uploade dine filer lige nu, fordi dokumentbehandleren er offline. Prøv igen senere.\",\n      \"click-upload\": \"Klik for at uploade eller træk og slip\",\n      \"file-types\":\n        \"understøtter tekstfiler, CSV-filer, regneark, lydfiler og mere!\",\n      \"or-submit-link\": \"eller indsæt et link\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"Henter...\",\n      \"fetch-website\": \"Hent hjemmeside\",\n      \"privacy-notice\":\n        \"Disse filer vil blive uploadet til dokumentbehandleren, der kører på denne AnythingLLM-instans. Filene sendes ikke eller deles med en tredjepart.\",\n    },\n    pinning: {\n      what_pinning: \"Hvad er dokumentfastlåsning?\",\n      pin_explained_block1:\n        \"Når du <b>fastlåser</b> et dokument i AnythingLLM, vil vi indsætte hele dokumentets indhold i din prompt-vindue, så din LLM kan forstå det fuldt ud.\",\n      pin_explained_block2:\n        \"Dette fungerer bedst med <b>store kontekstmodeller</b> eller små filer, der er kritiske for dens vidensbase.\",\n      pin_explained_block3:\n        \"Hvis du ikke får de svar, du ønsker fra AnythingLLM som standard, er fastlåsning en fremragende måde at få svar af højere kvalitet med et enkelt klik.\",\n      accept: \"Okay, jeg har forstået\",\n    },\n    watching: {\n      what_watching: \"Hvad gør det at overvåge et dokument?\",\n      watch_explained_block1:\n        \"Når du <b>overvåger</b> et dokument i AnythingLLM, vil vi <i>automatisk</i> synkronisere dokumentets indhold fra dets oprindelige kilde med jævne mellemrum. Dette vil automatisk opdatere indholdet i alle arbejdsområder, hvor denne fil administreres.\",\n      watch_explained_block2:\n        \"Denne funktion understøtter i øjeblikket kun onlinebaseret indhold og vil ikke være tilgængelig for manuelt uploadede dokumenter.\",\n      watch_explained_block3_start:\n        \"Du kan administrere, hvilke dokumenter der overvåges fra \",\n      watch_explained_block3_link: \"Filhåndtering\",\n      watch_explained_block3_end: \" adminvisning.\",\n      accept: \"Okay, jeg har forstået\",\n    },\n    obsidian: {\n      vault_location: \"Opbevaringssted\",\n      vault_description:\n        \"Vælg din Obsidian-mappe, som du vil importere alle noter og deres forbindelser til.\",\n      selected_files: \"Fundet {{count}} markdown-filer\",\n      importing: \"Importering af skattekammer...\",\n      import_vault: \"Import Vault\",\n      processing_time:\n        \"Dette kan tage noget tid, afhængigt af størrelsen på din opbevaring.\",\n      vault_warning:\n        \"For at undgå eventuelle konflikter, skal du sørge for, at din Obsidian-mappe ikke er åben i øjeblikket.\",\n    },\n  },\n  chat_window: {\n    send_message: \"Send en besked\",\n    attach_file: \"Vedhæft en fil til denne chat\",\n    text_size: \"Ændr tekststørrelse.\",\n    microphone: \"Tal din prompt.\",\n    send: \"Send promptbesked til arbejdsområdet\",\n    attachments_processing:\n      \"Vedhæftede filer behandles. Vær venligst tålmodig...\",\n    tts_speak_message: \"TTS-besked\",\n    copy: \"Kopier\",\n    regenerate: \"Genopbyg\",\n    regenerate_response: \"Genopbyg svar\",\n    good_response: \"Godt svar\",\n    more_actions: \"Flere handlinger\",\n    fork: \"Fork\",\n    delete: \"Slet\",\n    cancel: \"Annullér\",\n    edit_prompt: \"Redigeringsanmodning\",\n    edit_response: \"Rediger svar\",\n    preset_reset_description:\n      \"Rydd op i din chat-historik og start en ny samtale\",\n    add_new_preset: \"Tilføj ny forudindstilling\",\n    command: \"Kommandér\",\n    your_command: \"dit kommando\",\n    placeholder_prompt:\n      \"Dette er indholdet, der vil blive indsat foran din forespørgsel.\",\n    description: \"Beskrivelse\",\n    placeholder_description: \"Svarer med et digt om LLM'er.\",\n    save: \"Gem\",\n    small: \"Lille\",\n    normal: \"Normal\",\n    large: \"Stor\",\n    workspace_llm_manager: {\n      search: \"Søg efter LLM-udbydere\",\n      loading_workspace_settings: \"Indlæser arbejdsområdets indstillinger...\",\n      available_models: \"Tilgængelige modeller for {{provider}}\",\n      available_models_description:\n        \"Vælg en model, der skal bruges til dette arbejdsområde.\",\n      save: \"Brug denne model\",\n      saving: \"Indstil modellen som standard for arbejdsområdet...\",\n      missing_credentials: \"Denne udbyder har ikke de nødvendige beviser!\",\n      missing_credentials_description:\n        \"Klik for at oprette legitimationsoplysninger\",\n    },\n    submit: \"Indsend\",\n    edit_info_user:\n      '\"Send\" genopretter AI-responsen. \"Gem\" opdaterer kun dit budskab.',\n    edit_info_assistant:\n      \"Ændringerne, du laver, vil blive gemt direkte i dette svar.\",\n    see_less: \"Se mindre\",\n    see_more: \"Se flere\",\n    tools: \"Værktøj\",\n    browse: \"Gennemse\",\n    text_size_label: \"Tekststørrelse\",\n    select_model: \"Vælg model\",\n    sources: \"Kilder\",\n    document: \"Dokument\",\n    similarity_match: \"kamp\",\n    source_count_one: \"{{count}} henvisning\",\n    source_count_other: \"{{count}} referencer\",\n    preset_exit_description: \"Afslut den aktuelle agent-session\",\n    add_new: \"Tilføj nyt\",\n    edit: \"Rediger\",\n    publish: \"Udgive\",\n    stop_generating: \"Stop med at generere svar\",\n    pause_tts_speech_message: \"Pause TTS-læsningen af beskeden\",\n    slash_commands: \"Kommandoer\",\n    agent_skills: \"Agenters kompetencer\",\n    manage_agent_skills: \"Administrer agenters kompetencer\",\n    agent_skills_disabled_in_session:\n      \"Det er ikke muligt at ændre færdigheder under en aktiv agent-session. Brug kommandoen `/exit` for at afslutte sessionen først.\",\n    start_agent_session: \"Start Agent-session\",\n    use_agent_session_to_use_tools:\n      \"Du kan bruge værktøjer i chat ved at starte en agent-session med '@agent' i starten af din forespørgsel.\",\n  },\n  profile_settings: {\n    edit_account: \"Rediger konto\",\n    profile_picture: \"Profilbillede\",\n    remove_profile_picture: \"Fjern profilbillede\",\n    username: \"Brugernavn\",\n    new_password: \"Ny adgangskode\",\n    password_description: \"Adgangskoden skal være mindst 8 tegn lang\",\n    cancel: \"Annuller\",\n    update_account: \"Opdater konto\",\n    theme: \"Tema-præference\",\n    language: \"Foretrukket sprog\",\n    failed_upload: \"Kunne ikke uploade profilbillede: {{error}}\",\n    upload_success: \"Profilbillede er uploadet.\",\n    failed_remove: \"Kunne ikke fjerne profilbilledet: {{error}}\",\n    profile_updated: \"Profil opdateret.\",\n    failed_update_user: \"Mislykket med at opdatere bruger: {{error}}\",\n    account: \"Konto\",\n    support: \"Støtte\",\n    signout: \"Log ud\",\n  },\n  customization: {\n    interface: {\n      title: \"Brugerpræferencer\",\n      description: \"Konfigurer dine præferencer for AnythingLLM.\",\n    },\n    branding: {\n      title: 'Brandstrategi og \"white label\"-løsninger',\n      description: \"Mærk din AnythingLLM-instans med dit eget brand.\",\n    },\n    chat: {\n      title: \"Chat\",\n      description: \"Angiv dine præferencer for chat med AnythingLLM.\",\n      auto_submit: {\n        title: \"Automatisk indtastning af taleinput\",\n        description:\n          \"Automatisk afsendelse af taleinput efter en periode med stilhed\",\n      },\n      auto_speak: {\n        title: \"Auto-Speak Responses\\n\\nAutomatiske svar\",\n        description: \"Automatisk genererede svar fra AI'en\",\n      },\n      spellcheck: {\n        title: \"Aktiver stavekontrol\",\n        description:\n          \"Aktiver eller deaktiver stavekontrollen i indtastningsfeltet\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Tema\",\n        description: \"Vælg dit foretrukne farveskema til applikationen.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Vis afrulningslinje\",\n        description: \"Aktiver eller deaktiver scrollbaren i chatvinduet.\",\n      },\n      \"support-email\": {\n        title: \"Støtte-e-mail\",\n        description:\n          \"Angiv e-mailadressen, der skal være tilgængelig for brugere, når de har brug for hjælp.\",\n      },\n      \"app-name\": {\n        title: \"Navn\",\n        description:\n          \"Angiv et navn, der vises på login-siden for alle brugere.\",\n      },\n      \"display-language\": {\n        title: \"Visningssprog\",\n        description:\n          \"Vælg det foretrukne sprog til at vise AnythingLLM's brugergrænseflade i – når oversættelser er tilgængelige.\",\n      },\n      logo: {\n        title: \"Brand Logo\",\n        description:\n          \"Upload dit brugerdefinerede logo for at vise det på alle sider.\",\n        add: \"Tilføj et brugerdefineret logo\",\n        recommended: \"Anbefalet størrelse: 800 x 200\",\n        remove: \"Fjern\",\n        replace: \"Udskift\",\n      },\n      \"browser-appearance\": {\n        title: \"Browser-udseende\",\n        description:\n          \"Tilpas udseendet af browserens fane og titel, når appen er åben.\",\n        tab: {\n          title:\n            \"**Embracing the Future: A Comprehensive Guide to Sustainable Development**\",\n          description:\n            \"Angiv en brugerdefineret titel for fanen, når appen åbnes i en browser.\",\n        },\n        favicon: {\n          title: \"Favikon\",\n          description: \"Brug et brugerdefineret ikon til browserens fane.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Sidefods-elementer\",\n        description:\n          \"Tilpas de elementer, der vises i fodervirket nederst i sidepanelet.\",\n        icon: \"Ikon\",\n        link: \"Link\",\n      },\n      \"render-html\": {\n        title: \"Vis HTML i chat\",\n        description:\n          \"Generer HTML-svar i hjælperes svar.\\nDette kan resultere i en meget højere kvalitet af svaret, men kan også føre til potentielle sikkerhedsrisici.\",\n      },\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Opret en agent\",\n      editWorkspace: \"Rediger arbejdsområdet\",\n      uploadDocument: \"Upload en fil\",\n    },\n    greeting: \"Hvordan kan jeg hjælpe dig i dag?\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Tastaturgenveje\",\n    shortcuts: {\n      settings: \"Åbn indstillinger\",\n      workspaceSettings: \"Åbn aktuelle arbejdsområdesindstillinger\",\n      home: \"Gå til Hjem\",\n      workspaces: \"Administrer arbejdsområder\",\n      apiKeys: \"API-nøgler: Indstillinger\",\n      llmPreferences: \"LLM-præferencer\",\n      chatSettings: \"Opsætningsindstillinger\",\n      help: \"Vis hjælp til tastaturgenveje\",\n      showLLMSelector: \"Vis arbejdsområde LLM-valg\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Succes!\",\n        success_description:\n          \"Dit systemprompt er nu tilgængeligt i Community Hub!\",\n        success_thank_you: \"Tak for at dele med fællesskabet!\",\n        view_on_hub: \"Se på Community Hub\",\n        modal_title: \"Publikationssystemets prompt\",\n        name_label: \"Navn\",\n        name_description: \"Dette er navnet, der vises for dit systemprompt.\",\n        name_placeholder: \"Mit systemprompt\",\n        description_label: \"Beskrivelse\",\n        description_description:\n          \"Dette er beskrivelsen af dit systemprompt. Brug dette til at beskrive formålet med dit systemprompt.\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Tags bruges til at mærke dine system prompts, så de er nemmere at finde. Du kan tilføje flere tags. Maksimalt 5 tags. Maksimalt 20 tegn per tag.\",\n        tags_placeholder: \"Skriv og tryk på Enter for at tilføje tags\",\n        visibility_label: \"Synlighed\",\n        public_description: \"Offentlige systemmeddelelser er synlige for alle.\",\n        private_description: \"Private system prompts er kun synlige for dig.\",\n        publish_button: \"Publicer på Community Hub\",\n        submitting: \"Uddrag...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Dette er den faktiske systemprompt, der vil blive brugt til at styre LLM'en.\",\n        prompt_placeholder: \"Indtast din systemprompt her...\",\n      },\n      agent_flow: {\n        success_title: \"Succes!\",\n        success_description:\n          \"Dit Agent Flow er nu tilgængeligt i Community Hub!\",\n        success_thank_you: \"Tak for at dele med fællesskabet!\",\n        view_on_hub: \"Se på Community Hub\",\n        modal_title: \"Publicer agentflow\",\n        name_label: \"Navn\",\n        name_description: \"Dette er navnet, der vises for din agentflow.\",\n        name_placeholder: \"Min agent, Flow\",\n        description_label: \"Beskrivelse\",\n        description_description:\n          \"Dette er beskrivelsen af din agentflow. Brug den til at beskrive formålet med dit agentflow.\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Tags bruges til at mærke dine agentflows, så de er nemmere at finde. Du kan tilføje flere tags. Maksimalt 5 tags. Maksimalt 20 tegn per tag.\",\n        tags_placeholder: \"Skriv og tryk på Enter for at tilføje tags\",\n        visibility_label: \"Synlighed\",\n        submitting: \"Uddrag...\",\n        submit: \"Publicer på Community Hub\",\n        privacy_note:\n          \"Agent-strømme uploades altid som private for at beskytte enhver følsom data. Du kan ændre synligheden i Community Hub efter udgivelse. Vær venligst opmærksom på, at din strøm ikke indeholder nogen følsom eller privat information, før du udgiver den.\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Krav om godkendelse\",\n          description:\n            \"Du skal verificere din identitet via AnythingLLM Community Hub, før du kan publicere indhold.\",\n          button: \"Forbind til fællesskabscenter\",\n        },\n      },\n      slash_command: {\n        success_title: \"Succes!\",\n        success_description:\n          \"Din Slash-kommando er blevet offentliggjort i Community Hub!\",\n        success_thank_you: \"Tak for at dele med fællesskabet!\",\n        view_on_hub: \"Se på Community Hub\",\n        modal_title: \"Udsend Slash Command\",\n        name_label: \"Navn\",\n        name_description: \"Dette er navnet, der vises for din kommando.\",\n        name_placeholder: \"Mit Slash-kommando\",\n        description_label: \"Beskrivelse\",\n        description_description:\n          \"Dette er beskrivelsen af din kommando. Brug den til at beskrive formålet med din kommando.\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Tags bruges til at mærke dine kommandoer, så de er nemmere at finde. Du kan tilføje flere tags. Maksimalt 5 tags. Maksimalt 20 tegn pr. tag.\",\n        tags_placeholder: \"Skriv og tryk på Enter for at tilføje tags\",\n        visibility_label: \"Synlighed\",\n        public_description: \"Offentlige kommandoer er synlige for alle.\",\n        private_description: \"Private kommandoer er kun synlige for dig.\",\n        publish_button: \"Publicer på Community Hub\",\n        submitting: \"Uddrag...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Dette er den kommando, der vil blive brugt, når kommandoen med skråstreg aktiveres.\",\n        prompt_placeholder: \"Indtast din forespørgsel her...\",\n      },\n    },\n  },\n  security: {\n    title: \"Sikkerhed\",\n    multiuser: {\n      title: \"Multi-brugertilstand\",\n      description:\n        \"Opsæt din instans til at understøtte dit team ved at aktivere multi-brugertilstand.\",\n      enable: {\n        \"is-enable\": \"Multi-brugertilstand er aktiveret\",\n        enable: \"Aktivér multi-brugertilstand\",\n        description:\n          \"Som standard vil du være den eneste administrator. Som administrator skal du oprette konti til alle nye brugere eller administratorer. Glem ikke din adgangskode, da kun en administrator kan nulstille adgangskoder.\",\n        username: \"Brugernavn til admin-konto\",\n        password: \"Adgangskode til admin-konto\",\n      },\n    },\n    password: {\n      title: \"Adgangskodebeskyttelse\",\n      description:\n        \"Beskyt din AnythingLLM-instans med en adgangskode. Hvis du glemmer den, findes der ingen genoprettelsesmetode, så sørg for at gemme denne adgangskode.\",\n      \"password-label\": \"Instansadgangskode\",\n    },\n  },\n  home: {\n    welcome: \"Velkommen\",\n    chooseWorkspace: \"Vælg et arbejdsområde for at starte at chatte!\",\n    notAssigned:\n      \"Du er ikke tildelt til nogen arbejdsområder.\\nKontakt din administrator for at anmode om adgang til et arbejdsområde.\",\n    goToWorkspace: 'Gå til \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/de/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"Willkommen bei\",\n      getStarted: \"Jetzt starten\",\n    },\n    llm: {\n      title: \"LLM-Einstellung\",\n      description:\n        \"AnythingLLM ist mit vielen LLM-Anbietern kompatibel. Der ausgewählte Dienst wird für die Chats verwendet.\",\n    },\n    userSetup: {\n      title: \"Benutzer Setup\",\n      description: \"Konfigurieren Sie Ihre Benutzereinstellungen.\",\n      howManyUsers: \"Wie viele Benutzer werden diese Instanz verwenden?\",\n      justMe: \"Nur ich\",\n      myTeam: \"Mein Team\",\n      instancePassword: \"Passwort für diese Instanz\",\n      setPassword: \"Möchten Sie ein Passwort einrichten?\",\n      passwordReq: \"Das Passwort muss mindestens 8 Zeichen enthalten.\",\n      passwordWarn:\n        \"Dieses Passwort sollte sicher aufbewahrt werden, da Wiederherstellung nicht möglich ist.\",\n      adminUsername: \"Benutzername des Admin-Accounts\",\n      adminPassword: \"Passwort des Admin-Accounts\",\n      adminPasswordReq: \"Das Passwort muss mindestens 8 Zeichen enthalten.\",\n      teamHint:\n        \"Zu Beginn sind Sie der einzige Admin. Nach der Einrichtung können Sie weitere Benutzer oder Admins einladen. Verlieren Sie Ihr Passwort nicht – nur Admins können Passwörter zurücksetzen.\",\n    },\n    data: {\n      title: \"Datenverarbeitung & Datenschutz\",\n      description:\n        \"Wir setzen uns für Transparenz und Kontrolle im Umgang mit Ihren persönlichen Daten ein.\",\n      settingsHint:\n        \"Diese Einstellungen können jederzeit in den Einstellungen angepasst werden.\",\n    },\n    survey: {\n      title: \"Willkommen bei AnythingLLM\",\n      description:\n        \"Helfen Sie uns, AnythingLLM an Ihre Bedürfnisse anzupassen. (Optional)\",\n      email: \"Wie lautet Ihre E-Mail-Adresse?\",\n      useCase: \"Wofür möchten Sie AnythingLLM verwenden?\",\n      useCaseWork: \"Beruflich\",\n      useCasePersonal: \"Privat\",\n      useCaseOther: \"Sonstiges\",\n      comment: \"Wie haben Sie von AnythingLLM erfahren?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube, etc. – Teilen Sie uns mit, wie Sie uns entdeckt haben!\",\n      skip: \"Umfrage überspringen\",\n      thankYou: \"Vielen Dank für Ihr Feedback!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Namen der Workspaces\",\n    user: \"Benutzer\",\n    selection: \"Modellauswahl\",\n    saving: \"Speichern...\",\n    save: \"Änderungen speichern\",\n    previous: \"Vorherige Seite\",\n    next: \"Nächste Seite\",\n    optional: \"Optional\",\n    yes: \"Ja\",\n    no: \"Nein\",\n    search: \"Suchen\",\n    username_requirements:\n      \"Der Benutzername muss 2-32 Zeichen lang sein, mit einem Kleinbuchstaben beginnen und darf nur Kleinbuchstaben, Zahlen, Unterstriche, Bindestriche und Punkte enthalten.\",\n    on: \"Über\",\n    none: \"Keine\",\n    stopped: \"Gestoppt\",\n    loading: \"Laden\",\n    refresh: \"Erfrischen\",\n  },\n  settings: {\n    title: \"Instanzeinstellungen\",\n    invites: \"Einladungen\",\n    users: \"Benutzer\",\n    workspaces: \"Workspaces\",\n    \"workspace-chats\": \"Workspace-Chats\",\n    customization: \"Personalisierung\",\n    interface: \"UI-Einstellungen\",\n    branding: \"Branding & Whitelabeling\",\n    chat: \"Chat\",\n    \"api-keys\": \"Entwickler-API\",\n    llm: \"LLM\",\n    transcription: \"Transkription\",\n    embedder: \"Einbettung\",\n    \"text-splitting\": \"Textsplitting & Chunking\",\n    \"voice-speech\": \"Sprache & Sprachausgabe\",\n    \"vector-database\": \"Vektordatenbank\",\n    embeds: \"Chat-Einbettung\",\n    security: \"Sicherheit\",\n    \"event-logs\": \"Ereignisprotokolle\",\n    privacy: \"Datenschutz & Datenverarbeitung\",\n    \"ai-providers\": \"KI-Anbieter\",\n    \"agent-skills\": \"Agentenfähigkeiten\",\n    admin: \"Administrator\",\n    tools: \"Werkzeuge\",\n    \"experimental-features\": \"Experimentelle Funktionen\",\n    contact: \"Support kontaktieren\",\n    \"browser-extension\": \"Browser-Extension\",\n    \"system-prompt-variables\": \"Systempromptvariablen\",\n    \"mobile-app\": \"AnythingLLM Mobile\",\n    \"community-hub\": {\n      title: \"Gemeindezentrum\",\n      trending: \"Entdecken Sie die aktuell beliebtesten Themen\",\n      \"your-account\": \"Ihr Konto\",\n      \"import-item\": \"Artikel importieren\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Willkommen bei\",\n      \"placeholder-username\": \"Benutzername\",\n      \"placeholder-password\": \"Passwort\",\n      login: \"Anmelden\",\n      validating: \"Überprüfung...\",\n      \"forgot-pass\": \"Passwort vergessen\",\n      reset: \"Zurücksetzen\",\n    },\n    \"sign-in\": \"Melden Sie sich bei Ihrem {{appName}} Konto an.\",\n    \"password-reset\": {\n      title: \"Passwort zurücksetzen\",\n      description:\n        \"Geben Sie die erforderlichen Informationen unten ein, um Ihr Passwort zurückzusetzen.\",\n      \"recovery-codes\": \"Wiederherstellungscodes\",\n      \"back-to-login\": \"Zurück zur Anmeldung\",\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Erstelle einen Agenten\",\n      editWorkspace: \"Arbeitsbereich bearbeiten\",\n      uploadDocument: \"Ein Dokument hochladen\",\n    },\n    greeting: \"Wie kann ich Ihnen heute helfen?\",\n  },\n  \"new-workspace\": {\n    title: \"Neuer Workspace\",\n    placeholder: \"Mein Workspace\",\n  },\n  \"workspaces—settings\": {\n    general: \"Allgemeine Einstellungen\",\n    chat: \"Chat-Einstellungen\",\n    vector: \"Vektordatenbank\",\n    members: \"Mitglieder\",\n    agent: \"Agentenkonfiguration\",\n  },\n  general: {\n    vector: {\n      title: \"Vektoranzahl\",\n      description: \"Gesamtanzahl der Vektoren in Ihrer Vektordatenbank.\",\n    },\n    names: {\n      description: \"Dies ändert nur den Anzeigenamen Ihres Workspace.\",\n    },\n    message: {\n      title: \"Vorgeschlagene Chat-Nachrichten\",\n      description:\n        \"Passen Sie die Nachrichten an, die Ihren Workspace-Benutzern vorgeschlagen werden.\",\n      add: \"Neue Nachricht hinzufügen\",\n      save: \"Nachrichten speichern\",\n      heading: \"Erkläre mir\",\n      body: \"die Vorteile von AnythingLLM\",\n    },\n    delete: {\n      title: \"Workspace löschen\",\n      description:\n        \"Löschen Sie diesen Workspace und alle seine Daten. Dies löscht den Workspace für alle Benutzer.\",\n      delete: \"Workspace löschen\",\n      deleting: \"Workspace wird gelöscht...\",\n      \"confirm-start\": \"Sie sind dabei, Ihren gesamten\",\n      \"confirm-end\":\n        \"Workspace zu löschen. Dies entfernt alle Vektoreinbettungen in Ihrer Vektordatenbank.\\n\\nDie ursprünglichen Quelldateien bleiben unberührt. Diese Aktion ist irreversibel.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Workspace-LLM-Anbieter\",\n      description:\n        \"Der spezifische LLM-Anbieter und das Modell, das für diesen Workspace verwendet wird. Standardmäßig wird der System-LLM-Anbieter und dessen Einstellungen verwendet.\",\n      search: \"Durchsuchen Sie alle LLM-Anbieter\",\n    },\n    model: {\n      title: \"Workspace-Chat-Modell\",\n      description:\n        \"Das spezifische Chat-Modell, das für diesen Workspace verwendet wird. Wenn leer, wird die System-LLM-Präferenz verwendet.\",\n    },\n    mode: {\n      title: \"Chat-Modus\",\n      chat: {\n        title: \"Chat\",\n        description:\n          \"wird Antworten basierend auf dem allgemeinen Wissen des LLM und dem relevanten Kontext aus den Dokumenten <b> und </b> liefern. <br /> Sie benötigen den Befehl `@agent`, um die Tools zu nutzen.\",\n      },\n      query: {\n        title: \"Abfrage\",\n        description:\n          'wird nur Antworten <b> und </b> bereitstellen, falls der Kontext des Dokuments gefunden wurde.<br />Sie müssen den Befehl \"@agent\" verwenden, um die Tools zu nutzen.',\n      },\n      automatic: {\n        title: \"Auto\",\n        description:\n          'wird automatisch Werkzeuge verwenden, wenn das Modell und der Anbieter native Werkzeugaufrufe unterstützen. <br />Wenn native Werkzeugaufrufe nicht unterstützt werden, müssen Sie den Befehl \"@agent\" verwenden, um Werkzeuge zu nutzen.',\n      },\n    },\n    history: {\n      title: \"Chat-Verlauf\",\n      \"desc-start\":\n        \"Die Anzahl der vorherigen Chats, die in das Kurzzeitgedächtnis der Antwort einbezogen werden.\",\n      recommend: \"Empfohlen 20. \",\n      \"desc-end\":\n        \"Alles über 45 führt wahrscheinlich zu kontinuierlichen Chat-Ausfällen, abhängig von der Nachrichtengröße.\",\n    },\n    prompt: {\n      title: \"Prompt\",\n      description:\n        \"Der Prompt, der in diesem Workspace verwendet wird. Definieren Sie den Kontext und die Anweisungen für die KI, um eine Antwort zu generieren. Sie sollten einen sorgfältig formulierten Prompt bereitstellen, damit die KI eine relevante und genaue Antwort generieren kann.\",\n      history: {\n        title: \"Systemprompt-Historie\",\n        clearAll: \"Alles löschen\",\n        noHistory: \"Keine Einträge im Verlauf vorhanden\",\n        restore: \"Wiederherstellen\",\n        delete: \"Löschen\",\n        publish: \"Im Community Hub veröffentlichen\",\n        deleteConfirm: \"Möchten Sie diesen Eintrag wirklich löschen?\",\n        clearAllConfirm:\n          \"Möchten Sie wirklich alle Einträge löschen? Diese Aktion ist unwiderruflich.\",\n        expand: \"Ausklappen\",\n      },\n    },\n    refusal: {\n      title: \"Abfragemodus-Ablehnungsantwort\",\n      \"desc-start\": \"Wenn im\",\n      query: \"Abfrage\",\n      \"desc-end\":\n        \"modus, möchten Sie vielleicht eine benutzerdefinierte Ablehnungsantwort zurückgeben, wenn kein Kontext gefunden wird.\",\n      \"tooltip-title\": \"Warum sehe ich das?\",\n      \"tooltip-description\":\n        \"Sie befinden sich im Abfragemodus, der nur Informationen aus Ihren Dokumenten verwendet. Wechseln Sie in den Chat-Modus für flexiblere Gespräche oder klicken Sie hier, um unsere Dokumentation zu besuchen und mehr über Chat-Modi zu erfahren.\",\n    },\n    temperature: {\n      title: \"LLM-Temperatur\",\n      \"desc-start\":\n        'Diese Einstellung steuert, wie \"kreativ\" Ihre LLM-Antworten sein werden.',\n      \"desc-end\":\n        \"Je höher die Zahl, desto kreativer. Bei einigen Modellen kann dies zu unverständlichen Antworten führen, wenn sie zu hoch eingestellt ist.\",\n      hint: \"Die meisten LLMs haben verschiedene akzeptable Bereiche gültiger Werte. Konsultieren Sie Ihren LLM-Anbieter für diese Informationen.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Vektordatenbank-Identifikator\",\n    snippets: {\n      title: \"Maximale Kontext-Snippets\",\n      description:\n        \"Diese Einstellung steuert die maximale Anzahl von Kontext-Snippets, die pro Chat oder Abfrage an das LLM gesendet werden.\",\n      recommend: \"Empfohlen: 4\",\n    },\n    doc: {\n      title: \"Dokumentähnlichkeitsschwelle\",\n      description:\n        \"Der minimale Ähnlichkeitswert, der erforderlich ist, damit eine Quelle als relevant für den Chat betrachtet wird. Je höher die Zahl, desto ähnlicher muss die Quelle dem Chat sein.\",\n      zero: \"Keine Einschränkung\",\n      low: \"Niedrig (Ähnlichkeitswert ≥ .25)\",\n      medium: \"Mittel (Ähnlichkeitswert ≥ .50)\",\n      high: \"Hoch (Ähnlichkeitswert ≥ .75)\",\n    },\n    reset: {\n      reset: \"Vektordatenbank zurücksetzen\",\n      resetting: \"Vektoren werden gelöscht...\",\n      confirm:\n        \"Sie sind dabei, die Vektordatenbank dieses Workspace zurückzusetzen. Dies entfernt alle derzeit eingebetteten Vektoreinbettungen.\\n\\nDie ursprünglichen Quelldateien bleiben unberührt. Diese Aktion ist irreversibel.\",\n      error: \"Die Workspace-Vektordatenbank konnte nicht zurückgesetzt werden!\",\n      success: \"Die Workspace-Vektordatenbank wurde zurückgesetzt!\",\n    },\n  },\n  agent: {\n    provider: {\n      title: \"Workspace-Agent LLM-Anbieter\",\n      description:\n        \"Der spezifische LLM-Anbieter und das Modell, das für den @agent-Agenten dieses Workspace verwendet wird.\",\n    },\n    mode: {\n      chat: {\n        title: \"Workspace-Agent Chat-Modell\",\n        description:\n          \"Das spezifische Chat-Modell, das für den @agent-Agenten dieses Workspace verwendet wird.\",\n      },\n      title: \"Workspace-Agent-Modell\",\n      description:\n        \"Das spezifische LLM-Modell, das für den @agent-Agenten dieses Workspace verwendet wird.\",\n      wait: \"-- warte auf Modelle --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG & Langzeitgedächtnis\",\n        description:\n          'Erlauben Sie dem Agenten, Ihre lokalen Dokumente zu nutzen, um eine Abfrage zu beantworten oder bitten Sie den Agenten, Inhalte für den Langzeitabruf zu \"merken\".',\n      },\n      view: {\n        title: \"Dokumente anzeigen & zusammenfassen\",\n        description:\n          \"Erlauben Sie dem Agenten, den Inhalt der aktuell eingebetteten Workspace-Dateien aufzulisten und zusammenzufassen.\",\n      },\n      scrape: {\n        title: \"Websites durchsuchen\",\n        description:\n          \"Erlauben Sie dem Agenten, Websites zu besuchen und deren Inhalt zu extrahieren.\",\n      },\n      generate: {\n        title: \"Diagramme generieren\",\n        description:\n          \"Aktivieren Sie den Standard-Agenten, um verschiedene Arten von Diagrammen aus bereitgestellten oder im Chat gegebenen Daten zu generieren.\",\n      },\n      save: {\n        title: \"Dateien generieren & im Browser speichern\",\n        description:\n          \"Aktivieren Sie den Standard-Agenten, um Dateien zu generieren und zu schreiben, die gespeichert und in Ihrem Browser heruntergeladen werden können.\",\n      },\n      web: {\n        title: \"Live-Websuche und -Browsing\",\n        description:\n          \"Ermöglichen Sie Ihrem Agenten, das Internet zu durchsuchen, um Ihre Fragen zu beantworten, indem Sie eine Verbindung zu einem Anbieter von Web-Suchdiensten (SERP) herstellen.\",\n      },\n      sql: {\n        title: \"SQL-Verbindung\",\n        description:\n          \"Ermöglichen Sie Ihrem Agenten, SQL zu nutzen, um Ihre Fragen zu beantworten, indem Sie eine Verbindung zu verschiedenen SQL-Datenbankanbietern herstellen.\",\n      },\n      default_skill:\n        \"Standardmäßig ist diese Funktion aktiviert, aber Sie können sie deaktivieren, wenn Sie nicht möchten, dass sie für den Agenten verfügbar ist.\",\n    },\n    \"performance-warning\":\n      \"Die Leistung von LLMs, die keine explizite Unterstützung für das Aufrufen von Tools bieten, hängt stark von den Fähigkeiten und der Genauigkeit des Modells ab. Einige Fähigkeiten können eingeschränkt oder nicht funktionsfähig sein.\",\n    mcp: {\n      title: \"MCP-Servern\",\n      \"loading-from-config\":\n        \"Laden von MCP-Servern aus einer Konfigurationsdatei\",\n      \"learn-more\": \"Erfahren Sie mehr über MCP-Server.\",\n      \"no-servers-found\": \"Keine MCP-Server gefunden\",\n      \"tool-warning\":\n        \"Für die beste Leistung sollten Sie unnötige Werkzeuge deaktivieren, um den Kontext zu schonen.\",\n      \"stop-server\": \"MCP-Server stoppen\",\n      \"start-server\": \"MCP-Server starten\",\n      \"delete-server\": \"MCP-Server löschen\",\n      \"tool-count-warning\":\n        \"Dieser MCP-Server hat <b>{{count}} Tools aktiviert, </b> die Kontext verbrauchen werden, wenn eine Chat-Sitzung stattfindet. <br /> Erwägen Sie, unerwünschte Tools zu deaktivieren, um Kontext zu sparen.\",\n      \"startup-command\": \"Startbefehl\",\n      command: \"Befehl\",\n      arguments: \"Argumente\",\n      \"not-running-warning\":\n        \"Dieser MCP-Server ist nicht aktiv – er kann gestoppt sein oder bei der Startsequenz einen Fehler aufweisen.\",\n      \"tool-call-arguments\": \"Argumente für die Funktionsaufrufe\",\n      \"tools-enabled\": \"Werkzeuge aktiviert\",\n    },\n    settings: {\n      title: \"Einstellungen für Agenten-Fähigkeiten\",\n      \"max-tool-calls\": {\n        title: \"Maximale Anzahl an Tool-Anfragen pro Antwort\",\n        description:\n          \"Die maximale Anzahl an Werkzeugen, die ein Agent verketten kann, um eine einzelne Antwort zu generieren. Dies verhindert, dass Werkzeuge unkontrolliert aufgerufen werden und zu endlosen Schleifen führen.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Intelligente Auswahl von Fähigkeiten\",\n        \"beta-badge\": \"Beta-Version\",\n        description:\n          \"Ermöglichen Sie die uneingeschränkte Nutzung von Werkzeugen und reduzieren Sie die Token-Nutzung pro Anfrage um bis zu 80 % – AnythingLLM wählt automatisch die passenden Fähigkeiten für jede Anfrage aus.\",\n        \"max-tools\": {\n          title: \"Max Tools\",\n          description:\n            \"Die maximale Anzahl der auszuwählenden Werkzeuge für jede Abfrage. Wir empfehlen, diesen Wert für größere Modelle mit größerem Kontext auf einen höheren Wert einzustellen.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Workspace-Chats\",\n    description:\n      \"Dies sind alle aufgezeichneten Chats und Nachrichten, die von Benutzern gesendet wurden, geordnet nach ihrem Erstellungsdatum.\",\n    export: \"Exportieren\",\n    table: {\n      id: \"Id\",\n      by: \"Gesendet von\",\n      workspace: \"Workspace\",\n      prompt: \"Prompt\",\n      response: \"Antwort\",\n      at: \"Gesendet am\",\n    },\n  },\n  customization: {\n    interface: {\n      title: \"UI Einstellungen\",\n      description: \"Passen Sie die Benutzeroberfläche von AnythingLLM an.\",\n    },\n    branding: {\n      title: \"Branding & Whitelabeling\",\n      description:\n        \"Individualisieren Sie Ihre AnythingLLM-Instanz durch eigenes Branding.\",\n    },\n    chat: {\n      title: \"Chat\",\n      description: \"Passen Sie Ihre Chat-Einstellungen für AnythingLLM an.\",\n      auto_submit: {\n        title: \"Spracheingaben automatisch senden\",\n        description:\n          \"Automatische Übermittlung der Spracheingabe nach einer Sprechpause.\",\n      },\n      auto_speak: {\n        title: \"Antworten automatisch vorlesen\",\n        description: \"Antworten der KI automatisch vorlesen lassen\",\n      },\n      spellcheck: {\n        title: \"Rechtschreibprüfung aktivieren\",\n        description:\n          \"Aktivieren oder deaktivieren Sie die Rechtschreibprüfung im Chat-Eingabefeld.\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Farbschema\",\n        description: \"Wählen Sie Ihr bevorzugtes Farbschema für die Anwendung.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Scrollbar anzeigen\",\n        description:\n          \"Aktivieren oder deaktivieren Sie die Scrollbar im Chat-Fenster.\",\n      },\n      \"support-email\": {\n        title: \"Support-E-Mail\",\n        description: \"Legen Sie die E-Mail-Adresse für den Kundensupport fest.\",\n      },\n      \"app-name\": {\n        title: \"Name\",\n        description:\n          \"Geben Sie einen Anwendungsnamen ein, der auf der Login-Seite erscheint.\",\n      },\n      \"display-language\": {\n        title: \"Sprache\",\n        description:\n          \"Wählen Sie die bevorzugte Sprache für die Benutzeroberfläche.\",\n      },\n      logo: {\n        title: \"Eigenes Logo\",\n        description:\n          \"Laden Sie Ihr eigenes Logo hoch, das auf allen Seiten angezeigt wird.\",\n        add: \"Eigenes Logo hinzufügen\",\n        recommended: \"Empfohlene Größe: 800 x 200\",\n        remove: \"Löschen\",\n        replace: \"Ersetzen\",\n      },\n      \"browser-appearance\": {\n        title: \"Browser-Ansicht\",\n        description:\n          \"Individualisieren Sie die Ansicht von Browser-Tab und -Titel, während die App geöffnet ist.\",\n        tab: {\n          title: \"Titel\",\n          description:\n            \"Bestimmen Sie einen individuellen Tab-Titel, wenn die App im Browser geöffnet ist.\",\n        },\n        favicon: {\n          title: \"Tab-Icon\",\n          description: \"Nutzen Sie ein eigenes Icon für den Tab im Browser.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Fußzeilenelemente der Seitenleiste\",\n        description:\n          \"Individualisieren Sie die Elemente in der Fußzeile am unteren Ende der Seitenleiste.\",\n        icon: \"Icon\",\n        link: \"Link\",\n      },\n      \"render-html\": {\n        title: \"HTML-Code in einem Chat anzeigen\",\n        description:\n          \"HTML-Antworten in den Antworten des Assistenten anzeigen.\\nDies kann zu einer viel höheren Qualität der Antwort führen, aber auch zu potenziellen Sicherheitsrisiken führen.\",\n      },\n    },\n  },\n  api: {\n    title: \"API-Schlüssel\",\n    description:\n      \"API-Schlüssel ermöglichen es dem Besitzer, programmatisch auf diese AnythingLLM-Instanz zuzugreifen und sie zu verwalten.\",\n    link: \"Lesen Sie die API-Dokumentation\",\n    generate: \"Neuen API-Schlüssel generieren\",\n    table: {\n      key: \"API-Schlüssel\",\n      by: \"Erstellt von\",\n      created: \"Erstellt\",\n    },\n  },\n  llm: {\n    title: \"LLM-Präferenz\",\n    description:\n      \"Dies sind die Anmeldeinformationen und Einstellungen für Ihren bevorzugten LLM-Chat- und Einbettungsanbieter. Es ist wichtig, dass diese Schlüssel aktuell und korrekt sind, sonst wird AnythingLLM nicht richtig funktionieren.\",\n    provider: \"LLM-Anbieter\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Azure-Service-Endpoint\",\n        api_key: \"API-Schlüssel\",\n        chat_deployment_name: \"Name der Chat-Deployment\",\n        chat_model_token_limit: \"Chat-Modell Token-Begrenzung\",\n        model_type: \"Art des Modells\",\n        default: \"Standard\",\n        reasoning: \"Reasoning\",\n        model_type_tooltip:\n          'Wenn Ihre Bereitstellung ein Reasoning-Modell verwendet (z. B. o1, o1-mini, o3-mini usw.), setzen Sie dies auf \"Reasoning\". Andernfalls können Ihre Chat-Anfragen fehlschlagen.',\n      },\n    },\n  },\n  transcription: {\n    title: \"Transkriptionsmodell-Präferenz\",\n    description:\n      \"Dies sind die Anmeldeinformationen und Einstellungen für Ihren bevorzugten Transkriptionsmodellanbieter. Es ist wichtig, dass diese Schlüssel aktuell und korrekt sind, sonst werden Mediendateien und Audio nicht transkribiert.\",\n    provider: \"Transkriptionsanbieter\",\n    \"warn-start\":\n      \"Die Verwendung des lokalen Whisper-Modells auf Maschinen mit begrenztem RAM oder CPU kann AnythingLLM bei der Verarbeitung von Mediendateien zum Stillstand bringen.\",\n    \"warn-recommend\":\n      \"Wir empfehlen mindestens 2 GB RAM und das Hochladen von Dateien <10 MB.\",\n    \"warn-end\":\n      \"Das eingebaute Modell wird bei der ersten Verwendung automatisch heruntergeladen.\",\n  },\n  embedding: {\n    title: \"Einbettungspräferenz\",\n    \"desc-start\":\n      \"Bei der Verwendung eines LLM, das keine native Unterstützung für eine Einbettungs-Engine bietet, müssen Sie möglicherweise zusätzlich Anmeldeinformationen für die Texteinbettung angeben.\",\n    \"desc-end\":\n      \"Einbettung ist der Prozess, Text in Vektoren umzuwandeln. Diese Anmeldeinformationen sind erforderlich, um Ihre Dateien und Prompts in ein Format umzuwandeln, das AnythingLLM zur Verarbeitung verwenden kann.\",\n    provider: {\n      title: \"Einbettungsanbieter\",\n    },\n  },\n  text: {\n    title: \"Textsplitting & Chunking-Präferenzen\",\n    \"desc-start\":\n      \"Manchmal möchten Sie vielleicht die Standardmethode ändern, wie neue Dokumente gesplittet und gechunkt werden, bevor sie in Ihre Vektordatenbank eingefügt werden.\",\n    \"desc-end\":\n      \"Sie sollten diese Einstellung nur ändern, wenn Sie verstehen, wie Textsplitting funktioniert und welche Nebenwirkungen es hat.\",\n    size: {\n      title: \"Textchunk-Größe\",\n      description:\n        \"Dies ist die maximale Länge der Zeichen, die in einem einzelnen Vektor vorhanden sein können.\",\n      recommend: \"Die maximale Länge des Einbettungsmodells beträgt\",\n    },\n    overlap: {\n      title: \"Textchunk-Überlappung\",\n      description:\n        \"Dies ist die maximale Überlappung von Zeichen, die während des Chunkings zwischen zwei benachbarten Textchunks auftritt.\",\n    },\n  },\n  vector: {\n    title: \"Vektordatenbank\",\n    description:\n      \"Dies sind die Anmeldeinformationen und Einstellungen für die Funktionsweise Ihrer AnythingLLM-Instanz. Es ist wichtig, dass diese Schlüssel aktuell und korrekt sind.\",\n    provider: {\n      title: \"Vektordatenbankanbieter\",\n      description: \"Für LanceDB ist keine Konfiguration erforderlich.\",\n    },\n  },\n  embeddable: {\n    title: \"Einbettbare Chat-Widgets\",\n    description:\n      \"Einbettbare Chat-Widgets sind öffentlich zugängliche Chat-Schnittstellen, die an einen einzelnen Workspace gebunden sind. Diese ermöglichen es Ihnen, Workspaces zu erstellen, die Sie dann weltweit veröffentlichen können.\",\n    create: \"Einbettung erstellen\",\n    table: {\n      workspace: \"Workspace\",\n      chats: \"Gesendete Chats\",\n      active: \"Aktive Domains\",\n      created: \"Erstellt\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Eingebettete Chats\",\n    export: \"Exportieren\",\n    description:\n      \"Dies sind alle aufgezeichneten Chats und Nachrichten von jeder Einbettung, die Sie veröffentlicht haben.\",\n    table: {\n      embed: \"Einbettung\",\n      sender: \"Absender\",\n      message: \"Nachricht\",\n      response: \"Antwort\",\n      at: \"Gesendet am\",\n    },\n  },\n  event: {\n    title: \"Ereignisprotokolle\",\n    description:\n      \"Sehen Sie alle Aktionen und Ereignisse, die auf dieser Instanz zur Überwachung stattfinden.\",\n    clear: \"Ereignisprotokolle löschen\",\n    table: {\n      type: \"Ereignistyp\",\n      user: \"Benutzer\",\n      occurred: \"Aufgetreten am\",\n    },\n  },\n  privacy: {\n    title: \"Datenschutz & Datenverarbeitung\",\n    description:\n      \"Dies ist Ihre Konfiguration dafür, wie verbundene Drittanbieter und AnythingLLM Ihre Daten behandeln.\",\n    anonymous: \"Anonyme Telemetrie aktiviert\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Datenverbindungen durchsuchen\",\n    \"no-connectors\": \"Keine Datenverbindungen gefunden.\",\n    obsidian: {\n      vault_location: \"Ort des Vaults\",\n      vault_description:\n        \"Ordner des Obsidian-Vaults auswählen, um sämtliche Notizen inkl. Verknüpfungen zu importieren.\",\n      selected_files: \"{{count}} Markdown-Dateien gefunden\",\n      importing: \"Vault wird importiert...\",\n      import_vault: \"Vault importieren\",\n      processing_time: \"Dies kann je nach Größe Ihres Vaults etwas dauern\",\n      vault_warning:\n        \"Bitte schließen Sie Ihr Obsidian-Vault, um mögliche Konflikte zu vermeiden.\",\n    },\n    github: {\n      name: \"GitHub Repository\",\n      description:\n        \"Importieren Sie ein öffentliches oder privates GitHub-Repository mit einem einzigen Klick.\",\n      URL: \"GitHub Repo URL\",\n      URL_explained: \"URL des GitHub-Repositories, das Sie sammeln möchten.\",\n      token: \"GitHub Zugriffstoken\",\n      optional: \"optional\",\n      token_explained: \"Zugriffstoken um Ratenlimits zu vermeiden.\",\n      token_explained_start: \"Ohne einen \",\n      token_explained_link1: \"persönlichen Zugriffstoken\",\n      token_explained_middle:\n        \" kann die GitHub-API aufgrund von Ratenlimits die Anzahl der abrufbaren Dateien einschränken. Sie können \",\n      token_explained_link2: \"einen temporären Zugriffstoken erstellen\",\n      token_explained_end: \", um dieses Problem zu vermeiden.\",\n      ignores: \"Datei-Ausschlüsse\",\n      git_ignore:\n        \"Liste im .gitignore-Format, um bestimmte Dateien während der Sammlung zu ignorieren. Drücken Sie Enter nach jedem Eintrag, den Sie speichern möchten.\",\n      task_explained:\n        \"Sobald der Vorgang abgeschlossen ist, sind alle Dateien im Dokumenten-Picker zur Einbettung in Workspaces verfügbar.\",\n      branch: \"Branch, von dem Sie Dateien sammeln möchten.\",\n      branch_loading: \"-- lade verfügbare Branches --\",\n      branch_explained: \"Branch, von dem Sie Dateien sammeln möchten.\",\n      token_information:\n        \"Ohne Angabe des <b>GitHub Zugriffstokens</b> kann dieser Datenkonnektor aufgrund der öffentlichen API-Ratenlimits von GitHub nur die <b>Top-Level</b>-Dateien des Repositories sammeln.\",\n      token_personal:\n        \"Holen Sie sich hier einen kostenlosen persönlichen Zugriffstoken mit einem GitHub-Konto.\",\n    },\n    gitlab: {\n      name: \"GitLab Repository\",\n      description:\n        \"Importieren Sie ein öffentliches oder privates GitLab-Repository mit einem einzigen Klick.\",\n      URL: \"GitLab Repo URL\",\n      URL_explained: \"URL des GitLab-Repositories, das Sie sammeln möchten.\",\n      token: \"GitLab Zugriffstoken\",\n      optional: \"optional\",\n      token_description:\n        \"Wählen Sie zusätzliche Entitäten aus, die von der GitLab-API abgerufen werden sollen.\",\n      token_explained_start: \"Ohne einen \",\n      token_explained_link1: \"persönlichen Zugriffstoken\",\n      token_explained_middle:\n        \" kann die GitLab-API aufgrund von Ratenlimits die Anzahl der abrufbaren Dateien einschränken. Sie können \",\n      token_explained_link2: \"einen temporären Zugriffstoken erstellen\",\n      token_explained_end: \", um dieses Problem zu vermeiden.\",\n      fetch_issues: \"Issues als Dokumente abrufen\",\n      ignores: \"Datei-Ausschlüsse\",\n      git_ignore:\n        \"Liste im .gitignore-Format, um bestimmte Dateien während der Sammlung zu ignorieren. Drücken Sie Enter nach jedem Eintrag, den Sie speichern möchten.\",\n      task_explained:\n        \"Sobald der Vorgang abgeschlossen ist, sind alle Dateien im Dokumenten-Picker zur Einbettung in Workspaces verfügbar.\",\n      branch: \"Branch, von dem Sie Dateien sammeln möchten\",\n      branch_loading: \"-- lade verfügbare Branches --\",\n      branch_explained: \"Branch, von dem Sie Dateien sammeln möchten.\",\n      token_information:\n        \"Ohne Angabe des <b>GitLab Zugriffstokens</b> kann dieser Datenkonnektor aufgrund der öffentlichen API-Ratenlimits von GitLab nur die <b>Top-Level</b>-Dateien des Repositories sammeln.\",\n      token_personal:\n        \"Holen Sie sich hier einen kostenlosen persönlichen Zugriffstoken mit einem GitLab-Konto.\",\n    },\n    youtube: {\n      name: \"YouTube Transkript\",\n      description:\n        \"Importieren Sie die Transkription eines YouTube-Videos über einen Link.\",\n      URL: \"YouTube Video URL\",\n      URL_explained_start:\n        \"Geben Sie die URL eines beliebigen YouTube-Videos ein, um dessen Transkript abzurufen. Das Video muss über \",\n      URL_explained_link: \"Untertitel\",\n      URL_explained_end: \" verfügen.\",\n      task_explained:\n        \"Sobald der Vorgang abgeschlossen ist, ist das Transkript im Dokumenten-Picker zur Einbettung in Workspaces verfügbar.\",\n    },\n    \"website-depth\": {\n      name: \"Massen-Link-Scraper\",\n      description:\n        \"Durchsuchen Sie eine Website und ihre Unterlinks bis zu einer bestimmten Tiefe.\",\n      URL: \"Website URL\",\n      URL_explained:\n        \"Geben Sie die Start-URL der Website ein, die Sie durchsuchen möchten.\",\n      depth: \"Durchsuchungstiefe\",\n      depth_explained:\n        \"Das ist die Menge an Unterseiten, die abhängig der originalen URL durchsucht werden sollen.\",\n      max_pages: \"Maximale Seitenanzahl\",\n      max_pages_explained: \"Maximale Anzahl der zu durchsuchenden Seiten.\",\n      task_explained:\n        \"Sobald der Vorgang abgeschlossen ist, sind alle gesammelten Inhalte im Dokumenten-Picker zur Einbettung in Workspaces verfügbar.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description:\n        \"Importieren Sie eine komplette Confluence-Seite mit einem einzigen Klick.\",\n      deployment_type: \"Confluence Bereitstellungstyp\",\n      deployment_type_explained:\n        \"Bestimmen Sie, ob Ihre Confluence-Instanz in der Atlassian Cloud oder selbst gehostet ist.\",\n      base_url: \"Confluence Basis-URL\",\n      base_url_explained: \"Dies ist die Basis-URL Ihres Confluence-Bereichs.\",\n      space_key: \"Confluence Space-Key\",\n      space_key_explained:\n        \"Dies ist der Space-Key Ihrer Confluence-Instanz, der verwendet wird. Beginnt normalerweise mit ~\",\n      username: \"Confluence Benutzername\",\n      username_explained: \"Ihr Confluence Benutzername.\",\n      auth_type: \"Confluence Authentifizierungstyp\",\n      auth_type_explained:\n        \"Wählen Sie den Authentifizierungstyp, den Sie verwenden möchten, um auf Ihre Confluence-Seiten zuzugreifen.\",\n      auth_type_username: \"Benutzername und Zugriffstoken\",\n      auth_type_personal: \"Persönliches Zugriffstoken\",\n      token: \"Confluence API-Token\",\n      token_explained_start:\n        \"Sie müssen ein Zugriffstoken für die Authentifizierung bereitstellen. Sie können ein Zugriffstoken\",\n      token_explained_link: \"hier\",\n      token_desc: \"Zugriffstoken für die Authentifizierung.\",\n      pat_token: \"Confluence persönliches Zugriffstoken\",\n      pat_token_explained: \"Ihr Confluence persönliches Zugriffstoken.\",\n      task_explained:\n        \"Sobald der Vorgang abgeschlossen ist, ist der Seiteninhalt im Dokumenten-Picker zur Einbettung in Workspaces verfügbar.\",\n      bypass_ssl: \"SSL-Zertifikatsvalidierung umgehen\",\n      bypass_ssl_explained:\n        \"Aktivieren Sie diese Option, um die SSL-Zertifikatsvalidierung für selbst gehostete Confluence-Instanzen mit selbstsignierten Zertifikaten zu umgehen.\",\n    },\n    manage: {\n      documents: \"Dokumente\",\n      \"data-connectors\": \"Datenverbindungen\",\n      \"desktop-only\":\n        \"Diese Einstellungen können nur auf einem Desktop-Gerät bearbeitet werden. Bitte rufen Sie diese Seite auf Ihrem Desktop auf, um fortzufahren.\",\n      dismiss: \"Schließen\",\n      editing: \"Bearbeite\",\n    },\n    directory: {\n      \"my-documents\": \"Meine Dokumente\",\n      \"new-folder\": \"Neuer Ordner\",\n      \"search-document\": \"Dokument suchen\",\n      \"no-documents\": \"Keine Dokumente\",\n      \"move-workspace\": \"In Workspace verschieben\",\n      \"delete-confirmation\":\n        \"Sind Sie sicher, dass Sie diese Dateien und Ordner löschen möchten?\\nDies wird die Dateien vom System entfernen und sie automatisch aus allen vorhandenen Workspaces entfernen.\\nDiese Aktion kann nicht rückgängig gemacht werden.\",\n      \"removing-message\":\n        \"Entferne {{count}} Dokumente und {{folderCount}} Ordner. Bitte warten.\",\n      \"move-success\": \"{{count}} Dokumente erfolgreich verschoben.\",\n      select_all: \"Alle auswählen\",\n      deselect_all: \"Auswahl abbrechen\",\n      no_docs: \"Keine Dokumente vorhanden.\",\n      remove_selected: \"Ausgewähltes entfernen\",\n      costs: \"*Einmalige Kosten für das Einbetten\",\n      save_embed: \"Speichern und Einbetten\",\n      \"total-documents_one\": \"{{count}} Dokument\",\n      \"total-documents_other\": \"{{count}} Dokumente\",\n    },\n    upload: {\n      \"processor-offline\": \"Dokumentenprozessor nicht verfügbar\",\n      \"processor-offline-desc\":\n        \"Wir können Ihre Dateien momentan nicht hochladen, da der Dokumentenprozessor offline ist. Bitte versuchen Sie es später erneut.\",\n      \"click-upload\":\n        \"Klicken Sie zum Hochladen oder ziehen Sie Dateien per Drag & Drop\",\n      \"file-types\":\n        \"unterstützt Textdateien, CSVs, Tabellenkalkulationen, Audiodateien und mehr!\",\n      \"or-submit-link\": \"oder einen Link einreichen\",\n      \"placeholder-link\": \"https://beispiel.de\",\n      fetching: \"Wird abgerufen...\",\n      \"fetch-website\": \"Website abrufen\",\n      \"privacy-notice\":\n        \"Diese Dateien werden zum Dokumentenprozessor hochgeladen, der auf dieser AnythingLLM-Instanz läuft. Diese Dateien werden nicht an Dritte gesendet oder geteilt.\",\n    },\n    pinning: {\n      what_pinning: \"Was bedeutet es Dokumente anzuheften?\",\n      pin_explained_block1:\n        \"Wenn du ein Dokument <b>anheftest</b>, wird den kompletten Inhalt des Dokuments mit deinem Prompt versendet, wodurch das LLM den vollen Kontext besitzt\",\n      pin_explained_block2:\n        \"Das funktioniert am besten bei <b>sehr großen Dokumenten</b> sowie für kleine Dokumenten, dessen Inhalt für die Wissensbasis absolut wichtig sind.\",\n      pin_explained_block3:\n        \"Wenn du nicht standardmäßig die erwünschten Ergebnisse bekommst, kann das anheften eine gute Methode sein, um Antworten mit einer besseren Qualität mit nur einem Klick zu erhalten.\",\n      accept: \"Alles klar, ich habe es verstanden.\",\n    },\n    watching: {\n      what_watching: \"Was bedeutet es ein Dokument zu beobachten?\",\n      watch_explained_block1:\n        \"Wenn du ein Dokument <b>beobachtest,</b> werden wir <i>automatisch</i> das Dokument von der Datenquelle in regelmäßigen Abständen aktualisieren. Dadurch wird der Inhalt automatisch in allen Workspaces aktualisiert, wo sich das Dokument befindet.\",\n      watch_explained_block2:\n        \"Diese Funktion unterstützt aktuell nur Online-Quellen und ist somit nicht verfügbar für selbst hochgeladene Dokumente\",\n      watch_explained_block3_start: \"Du kannst im \",\n      watch_explained_block3_link: \"Dateimanager\",\n      watch_explained_block3_end:\n        \" entscheiden, welche Dokumente du beobachten möchtest.\",\n      accept: \"Alles klar, ich habe es verstanden.\",\n    },\n  },\n  chat_window: {\n    attachments_processing: \"Anhänge werden verarbeitet. Bitte warten...\",\n    send_message: \"Schreibe eine Nachricht\",\n    attach_file: \"Füge eine Datei zum Chat hinzu\",\n    text_size: \"Ändere die Größe des Textes.\",\n    microphone: \"Spreche deinen Prompt ein.\",\n    send: \"Versende den Prompt an den Workspace.\",\n    tts_speak_message: \"Nachricht vorlesen (TTS)\",\n    copy: \"Kopieren\",\n    regenerate: \"Neu generieren\",\n    regenerate_response: \"Antwort neu generieren\",\n    good_response: \"Gute Antwort\",\n    more_actions: \"Weitere Aktionen\",\n    fork: \"Abzweigen\",\n    delete: \"Löschen\",\n    cancel: \"Abbrechen\",\n    edit_prompt: \"Prompt bearbeiten\",\n    edit_response: \"Antwort bearbeiten\",\n    preset_reset_description: \"Chatverlauf löschen und neuen Chat starten\",\n    add_new_preset: \"Neues Preset anlegen\",\n    command: \"Befehl\",\n    your_command: \"dein-befehl\",\n    placeholder_prompt: \"Dieser Text wird vor deinem Prompt eingefügt.\",\n    description: \"Beschreibung\",\n    placeholder_description: \"Antwortet mit einem Gedicht über LLMs.\",\n    save: \"Speichern\",\n    small: \"Klein\",\n    normal: \"Standard\",\n    large: \"Groß\",\n    workspace_llm_manager: {\n      search: \"LLM-Provider durchsuchen\",\n      loading_workspace_settings: \"Workspace-Einstellungen werden geladen\",\n      available_models: \"Verfügbare Modelle von {{provider}}\",\n      available_models_description:\n        \"Wählen Sie ein Modell für diesen Workspace\",\n      save: \"Modell verwenden\",\n      saving: \"Standardmodell für Workspace wird eingestellt...\",\n      missing_credentials: \"Für diesen Anbieter fehlen Anmeldedaten!\",\n      missing_credentials_description: \"Klicken, um Zugangsdaten einzurichten\",\n    },\n    submit: \"Absenden\",\n    edit_info_user:\n      '\"Absenden\" generiert die Antwort des KI-Systems neu. \"Speichern\" aktualisiert lediglich Ihre Nachricht.',\n    edit_info_assistant:\n      \"Ihre Änderungen werden direkt in diese Antwort gespeichert.\",\n    see_less: \"Weniger anzeigen\",\n    see_more: \"Mehr anzeigen\",\n    tools: \"Werkzeuge\",\n    browse: \"Durchsuchen\",\n    text_size_label: \"Schriftgröße\",\n    select_model: \"Modell auswählen\",\n    sources: \"Quellen\",\n    document: \"Dokument\",\n    similarity_match: \"Spiel\",\n    source_count_one: \"{{count}} Referenz\",\n    source_count_other: \"{{count}} Verweise\",\n    preset_exit_description: \"Behalte die aktuelle Agentensitzung\",\n    add_new: \"Neu hinzufügen\",\n    edit: \"Bearbeiten\",\n    publish: \"Veröffentlichen\",\n    stop_generating: \"Stoppen Sie die Generierung von Antworten\",\n    pause_tts_speech_message: \"Pause die Text-to-Speech-Funktion der Nachricht\",\n    slash_commands: \"Befehlszeilen\",\n    agent_skills: \"Fähigkeiten von Agenten\",\n    manage_agent_skills: \"Verwalten Sie die Fähigkeiten von Agenten\",\n    agent_skills_disabled_in_session:\n      \"Es ist nicht möglich, während einer aktiven Sitzung die Fähigkeiten zu ändern. Verwenden Sie zuerst den Befehl `/exit`, um die Sitzung zu beenden.\",\n    start_agent_session: \"Starte eine Agent-Sitzung\",\n    use_agent_session_to_use_tools:\n      'Sie können Tools im Chat nutzen, indem Sie eine Agentensitzung mit \"@agent\" am Anfang Ihrer Anfrage starten.',\n  },\n  profile_settings: {\n    edit_account: \"Account bearbeiten\",\n    profile_picture: \"Profilbild\",\n    remove_profile_picture: \"Profilbild entfernen\",\n    username: \"Nutzername\",\n    new_password: \"Neues Passwort\",\n    password_description: \"Das Passwort muss mindestens 8 Zeichen haben.\",\n    cancel: \"Abbrechen\",\n    update_account: \"Account updaten\",\n    theme: \"Bevorzugtes Design\",\n    language: \"Bevorzugte Sprache\",\n    failed_upload: \"Profilbild konnte nicht hochgeladen werden: {{error}}\",\n    upload_success: \"Profilbild hochgeladen.\",\n    failed_remove: \"Profilbild konnte nicht entfernt werden: {{error}}\",\n    profile_updated: \"Profil wurde aktualisiert.\",\n    failed_update_user: \"Benutzer konnte nicht aktualisiert werden: {{error}}\",\n    account: \"Account\",\n    support: \"Support\",\n    signout: \"Abmelden\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Tastaturkürzel\",\n    shortcuts: {\n      settings: \"Einstellungen öffnen\",\n      workspaceSettings: \"Workspace Einstellungen öffnen\",\n      home: \"Zur Startseite\",\n      workspaces: \"Workspaces verwalten\",\n      apiKeys: \"API-Schlüssel Einstellungen\",\n      llmPreferences: \"LLM-Einstellungen\",\n      chatSettings: \"Chat Einstellungen\",\n      help: \"Tastenkürzel Hilfe anzeigen\",\n      showLLMSelector: \"LLM-Auswahl für Workspace zeigen\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Erfolg!\",\n        success_description:\n          \"Ihre System-Anweisung wurde im Community Hub veröffentlicht!\",\n        success_thank_you: \"Vielen Dank für die Weitergabe an die Community!\",\n        view_on_hub: \"Ansicht im Community Hub\",\n        modal_title:\n          \"Veröffentlichen Sie das System, um die Benutzer zu informieren, dass das System nicht mehr verfügbar ist.\",\n        name_label: \"Name\",\n        visibility_label: \"Sichtbarkeit\",\n        public_description: \"Öffentliche Anweisungen sind für alle sichtbar.\",\n        private_description:\n          \"Private System-Nachrichten sind nur für Sie sichtbar.\",\n        publish_button: \"Veröffentlichen Sie im Community Hub\",\n        submitting: \"Veröffentlichung...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Dies ist der eigentliche Systemprompt, der verwendet wird, um das LLM zu steuern.\",\n        prompt_placeholder: \"Bitte geben Sie Ihren Systemprompt hier ein...\",\n        name_description: \"Dies ist der Anzeigename für Ihren Systemprompt.\",\n        name_placeholder: \"Mein System-Prompt\",\n        description_label: \"Beschreibung\",\n        description_description:\n          \"Dies ist die Beschreibung Ihres System-Prompts. Verwenden Sie dies, um den Zweck Ihres System-Prompts zu beschreiben.\",\n        tags_label: \"Schlüsselwörter\",\n        tags_description:\n          \"Die Tags werden verwendet, um Ihre Systemanweisung für eine einfachere Suche zu kennzeichnen. Sie können mehrere Tags hinzufügen. Maximal 5 Tags. Maximal 20 Zeichen pro Tag.\",\n        tags_placeholder:\n          \"Geben Sie den Text ein und drücken Sie die Eingabetaste, um Tags hinzuzufügen.\",\n      },\n      agent_flow: {\n        success_title: \"Erfolg!\",\n        success_description:\n          \"Ihr Agent Flow wurde auf dem Community Hub veröffentlicht!\",\n        success_thank_you: \"Vielen Dank für die Weitergabe an die Community!\",\n        view_on_hub: \"Ansicht im Community Hub\",\n        modal_title: \"Veröffentlichen Sie den Agentenfluss.\",\n        name_label: \"Name\",\n        name_description: \"Dies ist der Anzeigename für Ihren Agentenablauf.\",\n        name_placeholder: \"Mein Agent Flow\",\n        description_label: \"Beschreibung\",\n        description_description:\n          \"Dies ist die Beschreibung Ihres Agentenflusses. Verwenden Sie diese, um den Zweck Ihres Agentenflusses zu beschreiben.\",\n        tags_label: \"Schlüsselwörter\",\n        tags_description:\n          \"Die Tags werden verwendet, um Ihren Agentenfluss leichter durchsuchbar zu machen. Sie können mehrere Tags hinzufügen. Maximal 5 Tags. Maximal 20 Zeichen pro Tag.\",\n        tags_placeholder:\n          \"Geben Sie Tags ein und drücken Sie die Eingabetaste, um sie hinzuzufügen.\",\n        visibility_label: \"Sichtbarkeit\",\n        submitting: \"Veröffentlichung...\",\n        submit: \"Veröffentlichen Sie im Community Hub\",\n        privacy_note:\n          \"Agent-Prozesse werden immer privat hochgeladen, um sensible Daten zu schützen. Sie können die Sichtbarkeit im Community Hub nach der Veröffentlichung ändern. Bitte überprüfen Sie, ob Ihr Prozess keine sensiblen oder privaten Informationen enthält, bevor Sie ihn veröffentlichen.\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Benötigte Authentifizierung\",\n          description:\n            \"Sie müssen sich vor der Veröffentlichung von Inhalten über den AnythingLLM Community Hub authentifizieren.\",\n          button: \"Verbinden Sie sich mit dem Community Hub\",\n        },\n      },\n      slash_command: {\n        success_title: \"Erfolg!\",\n        success_description:\n          \"Ihre Slash-Befehle wurden im Community Hub veröffentlicht!\",\n        success_thank_you: \"Vielen Dank für die Weitergabe an die Community!\",\n        view_on_hub: \"Ansicht im Community Hub\",\n        modal_title: \"Slash-Befehle veröffentlichen\",\n        name_label: \"Name\",\n        name_description: \"Dies ist der Anzeigename für Ihren Slash-Befehl.\",\n        name_placeholder: \"Meine Slash-Befehle\",\n        description_label: \"Beschreibung\",\n        description_description:\n          \"Dies ist die Beschreibung für Ihren Slash-Befehl. Verwenden Sie diese, um den Zweck Ihres Slash-Befehls zu beschreiben.\",\n        tags_label: \"Schlüsselwörter\",\n        tags_description:\n          \"Die Tags werden verwendet, um Ihren Slash-Befehl zu kennzeichnen und die Suche zu erleichtern. Sie können mehrere Tags hinzufügen. Maximal 5 Tags. Maximal 20 Zeichen pro Tag.\",\n        tags_placeholder:\n          \"Geben Sie Tags ein und drücken Sie die Eingabetaste, um sie hinzuzufügen.\",\n        visibility_label: \"Sichtbarkeit\",\n        public_description:\n          \"Öffentliche Slash-Befehle sind für jeden sichtbar.\",\n        private_description: \"Private Slash-Befehle sind nur für Sie sichtbar.\",\n        publish_button: \"Veröffentlichen Sie im Community Hub\",\n        submitting: \"Veröffentlichung...\",\n        prompt_label:\n          \"Bitte geben Sie den Namen des Produkts an, das Sie verkaufen möchten.\",\n        prompt_description:\n          \"Dies ist der Befehl, der verwendet wird, wenn der Slash-Befehl ausgelöst wird.\",\n        prompt_placeholder: \"Bitte geben Sie Ihre Anfrage hier ein...\",\n      },\n    },\n  },\n  security: {\n    title: \"Sicherheit\",\n    multiuser: {\n      title: \"Mehrbenutzer-Modus\",\n      description:\n        \"Richten Sie Ihre Instanz ein, um Ihr Team zu unterstützen, indem Sie den Mehrbenutzer-Modus aktivieren.\",\n      enable: {\n        \"is-enable\": \"Mehrbenutzer-Modus ist aktiviert\",\n        enable: \"Mehrbenutzer-Modus aktivieren\",\n        description:\n          \"Standardmäßig sind Sie der einzige Administrator. Als Administrator müssen Sie Konten für alle neuen Benutzer oder Administratoren erstellen. Verlieren Sie Ihr Passwort nicht, da nur ein Administrator-Benutzer Passwörter zurücksetzen kann.\",\n        username: \"Administrator-Kontoname\",\n        password: \"Administrator-Kontopasswort\",\n      },\n    },\n    password: {\n      title: \"Passwortschutz\",\n      description:\n        \"Schützen Sie Ihre AnythingLLM-Instanz mit einem Passwort. Wenn Sie dieses vergessen, gibt es keine Wiederherstellungsmethode, also stellen Sie sicher, dass Sie dieses Passwort speichern.\",\n      \"password-label\": \"Instanzpasswort\",\n    },\n  },\n  home: {\n    welcome: \"Willkommen\",\n    chooseWorkspace: \"Wählen Sie ein Arbeitsbereich, um zu beginnen!\",\n    notAssigned:\n      \"Sie sind nicht zugewiesen zu einem Arbeitsbereich.\\nBitte kontaktieren Sie Ihren Administrator, um Zugriff auf einen Arbeitsbereich zu erhalten.\",\n    goToWorkspace: 'Zurück zum Arbeitsbereich \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/dynamicKeyAllowlist.js",
    "content": "// Keys listed here are used dynamically (e.g. t(variable)) and should never\n// be flagged as unused or deleted by findUnusedTranslations.mjs.\n//\n// When you add a dynamic t() call, add the affected key(s) here so the\n// pruning script knows they are intentionally referenced at runtime.\nconst DYNAMIC_KEY_ALLOWLIST = [\n  // Used dynamically in KeyboardShortcutsHelp via t(`keyboard-shortcuts.shortcuts.${shortcut.translationKey}`)\n  \"keyboard-shortcuts.shortcuts.settings\",\n  \"keyboard-shortcuts.shortcuts.home\",\n  \"keyboard-shortcuts.shortcuts.workspaces\",\n  \"keyboard-shortcuts.shortcuts.apiKeys\",\n  \"keyboard-shortcuts.shortcuts.llmPreferences\",\n  \"keyboard-shortcuts.shortcuts.chatSettings\",\n  \"keyboard-shortcuts.shortcuts.help\",\n  \"keyboard-shortcuts.shortcuts.showLLMSelector\",\n  \"keyboard-shortcuts.shortcuts.workspaceSettings\",\n];\n\nexport default DYNAMIC_KEY_ALLOWLIST;\n"
  },
  {
    "path": "frontend/src/locales/en/common.js",
    "content": "const TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"Welcome to\",\n      getStarted: \"Get Started\",\n    },\n    llm: {\n      title: \"LLM Preference\",\n      description:\n        \"AnythingLLM can work with many LLM providers. This will be the service which handles chatting.\",\n    },\n    userSetup: {\n      title: \"User Setup\",\n      description: \"Configure your user settings.\",\n      howManyUsers: \"How many users will be using this instance?\",\n      justMe: \"Just me\",\n      myTeam: \"My team\",\n      instancePassword: \"Instance Password\",\n      setPassword: \"Would you like to set up a password?\",\n      passwordReq: \"Passwords must be at least 8 characters.\",\n      passwordWarn:\n        \"It's important to save this password because there is no recovery method.\",\n      adminUsername: \"Admin account username\",\n      adminPassword: \"Admin account password\",\n      adminPasswordReq: \"Passwords must be at least 8 characters.\",\n      teamHint:\n        \"By default, you will be the only admin. Once onboarding is completed you can create and invite others to be users or admins. Do not lose your password as only admins can reset passwords.\",\n    },\n    data: {\n      title: \"Data Handling & Privacy\",\n      description:\n        \"We are committed to transparency and control when it comes to your personal data.\",\n      settingsHint:\n        \"These settings can be reconfigured at any time in the settings.\",\n    },\n    survey: {\n      title: \"Welcome to AnythingLLM\",\n      description: \"Help us make AnythingLLM built for your needs. Optional.\",\n      email: \"What's your email?\",\n      useCase: \"What will you use AnythingLLM for?\",\n      useCaseWork: \"For work\",\n      useCasePersonal: \"For personal use\",\n      useCaseOther: \"Other\",\n      comment: \"How did you hear about AnythingLLM?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube, etc. - Let us know how you found us!\",\n      skip: \"Skip Survey\",\n      thankYou: \"Thank you for your feedback!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Workspace Name\",\n    user: \"User\",\n    selection: \"Model Selection\",\n    saving: \"Saving...\",\n    save: \"Save changes\",\n    previous: \"Previous Page\",\n    next: \"Next Page\",\n    optional: \"Optional\",\n    yes: \"Yes\",\n    no: \"No\",\n    on: \"On\",\n    none: \"None\",\n    stopped: \"Stopped\",\n    search: \"Search\",\n    username_requirements:\n      \"Username must be 2-32 characters, start with a lowercase letter, and only contain lowercase letters, numbers, underscores, hyphens, and periods.\",\n    loading: \"Loading\",\n    refresh: \"Refresh\",\n  },\n  home: {\n    welcome: \"Welcome\",\n    chooseWorkspace: \"Choose a workspace to start chatting!\",\n    notAssigned:\n      \"You currently aren't assigned to any workspaces.\\nPlease contact your administrator to request access to a workspace.\",\n    goToWorkspace: 'Go to \"{{workspace}}\"',\n  },\n  settings: {\n    title: \"Instance Settings\",\n    invites: \"Invites\",\n    users: \"Users\",\n    workspaces: \"Workspaces\",\n    \"workspace-chats\": \"Workspace Chats\",\n    customization: \"Customization\",\n    interface: \"UI Preferences\",\n    branding: \"Branding & Whitelabeling\",\n    chat: \"Chat\",\n    \"api-keys\": \"Developer API\",\n    llm: \"LLM\",\n    transcription: \"Transcription\",\n    embedder: \"Embedder\",\n    \"text-splitting\": \"Text Splitter & Chunking\",\n    \"voice-speech\": \"Voice & Speech\",\n    \"vector-database\": \"Vector Database\",\n    embeds: \"Chat Embed\",\n    security: \"Security\",\n    \"event-logs\": \"Event Logs\",\n    privacy: \"Privacy & Data\",\n    \"ai-providers\": \"AI Providers\",\n    \"agent-skills\": \"Agent Skills\",\n    \"community-hub\": {\n      title: \"Community Hub\",\n      trending: \"Explore Trending\",\n      \"your-account\": \"Your Account\",\n      \"import-item\": \"Import Item\",\n    },\n    admin: \"Admin\",\n    tools: \"Tools\",\n    \"system-prompt-variables\": \"System Prompt Variables\",\n    \"experimental-features\": \"Experimental Features\",\n    contact: \"Contact Support\",\n    \"browser-extension\": \"Browser Extension\",\n    \"mobile-app\": \"AnythingLLM Mobile\",\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Welcome\",\n      \"placeholder-username\": \"Username\",\n      \"placeholder-password\": \"Password\",\n      login: \"Login\",\n      validating: \"Validating...\",\n      \"forgot-pass\": \"Forgot password\",\n      reset: \"Reset\",\n    },\n    \"sign-in\":\n      \"Enter your username and password to access your {{appName}} instance.\",\n    \"password-reset\": {\n      title: \"Password Reset\",\n      description:\n        \"Provide the necessary information below to reset your password.\",\n      \"recovery-codes\": \"Recovery Codes\",\n      \"back-to-login\": \"Back to Login\",\n    },\n  },\n  \"main-page\": {\n    greeting: \"How can I help you today?\",\n    quickActions: {\n      createAgent: \"Create an Agent\",\n      editWorkspace: \"Edit Workspace\",\n      uploadDocument: \"Upload a Document\",\n    },\n  },\n  \"new-workspace\": {\n    title: \"New Workspace\",\n    placeholder: \"My Workspace\",\n  },\n  \"workspaces—settings\": {\n    general: \"General Settings\",\n    chat: \"Chat Settings\",\n    vector: \"Vector Database\",\n    members: \"Members\",\n    agent: \"Agent Configuration\",\n  },\n  general: {\n    vector: {\n      title: \"Vector Count\",\n      description: \"Total number of vectors in your vector database.\",\n    },\n    names: {\n      description: \"This will only change the display name of your workspace.\",\n    },\n    message: {\n      title: \"Suggested Chat Messages\",\n      description:\n        \"Customize the messages that will be suggested to your workspace users.\",\n      add: \"Add new message\",\n      save: \"Save Messages\",\n      heading: \"Explain to me\",\n      body: \"the benefits of AnythingLLM\",\n    },\n    delete: {\n      title: \"Delete Workspace\",\n      description:\n        \"Delete this workspace and all of its data. This will delete the workspace for all users.\",\n      delete: \"Delete Workspace\",\n      deleting: \"Deleting Workspace...\",\n      \"confirm-start\": \"You are about to delete your entire\",\n      \"confirm-end\":\n        \"workspace. This will remove all vector embeddings in your vector database.\\n\\nThe original source files will remain untouched. This action is irreversible.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Workspace LLM Provider\",\n      description:\n        \"The specific LLM provider & model that will be used for this workspace. By default, it uses the system LLM provider and settings.\",\n      search: \"Search all LLM providers\",\n    },\n    model: {\n      title: \"Workspace Chat model\",\n      description:\n        \"The specific chat model that will be used for this workspace. If empty, will use the system LLM preference.\",\n    },\n    mode: {\n      title: \"Chat mode\",\n      automatic: {\n        title: \"Auto\",\n        description:\n          \"will automatically use tools if the model and provider support native tool calling.<br />If native tooling is not supported, you will need to use the @agent command to use tools.\",\n      },\n      chat: {\n        title: \"Chat\",\n        description:\n          \"will provide answers with the LLM's general knowledge <b>and</b> document context that is found.<br />You will need to use the @agent command to use tools.\",\n      },\n      query: {\n        title: \"Query\",\n        description:\n          \"will provide answers <b>only</b> if document context is found.<br />You will need to use the @agent command to use tools.\",\n      },\n    },\n    history: {\n      title: \"Chat History\",\n      \"desc-start\":\n        \"The number of previous chats that will be included in the response's short-term memory.\",\n      recommend: \"Recommend 20. \",\n      \"desc-end\":\n        \"Anything more than 45 is likely to lead to continuous chat failures depending on message size.\",\n    },\n    prompt: {\n      title: \"System Prompt\",\n      description:\n        \"The prompt that will be used on this workspace. Define the context and instructions for the AI to generate a response. You should provide a carefully crafted prompt so the AI can generate a relevant and accurate response.\",\n      history: {\n        title: \"System Prompt History\",\n        clearAll: \"Clear All\",\n        noHistory: \"No system prompt history available\",\n        restore: \"Restore\",\n        delete: \"Delete\",\n        publish: \"Publish to Community Hub\",\n        deleteConfirm: \"Are you sure you want to delete this history item?\",\n        clearAllConfirm:\n          \"Are you sure you want to clear all history? This action cannot be undone.\",\n        expand: \"Expand\",\n      },\n    },\n    refusal: {\n      title: \"Query mode refusal response\",\n      \"desc-start\": \"When in\",\n      query: \"query\",\n      \"desc-end\":\n        \"mode, you may want to return a custom refusal response when no context is found.\",\n      \"tooltip-title\": \"Why am I seeing this?\",\n      \"tooltip-description\":\n        \"You are in query mode, which only uses information from your documents. Switch to chat mode for more flexible conversations, or click here to visit our documentation to learn more about chat modes.\",\n    },\n    temperature: {\n      title: \"LLM Temperature\",\n      \"desc-start\":\n        'This setting controls how \"creative\" your LLM responses will be.',\n      \"desc-end\":\n        \"The higher the number the more creative. For some models this can lead to incoherent responses when set too high.\",\n      hint: \"Most LLMs have various acceptable ranges of valid values. Consult your LLM provider for that information.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Vector database identifier\",\n    snippets: {\n      title: \"Max Context Snippets\",\n      description:\n        \"This setting controls the maximum amount of context snippets that will be sent to the LLM for per chat or query.\",\n      recommend: \"Recommended: 4\",\n    },\n    doc: {\n      title: \"Document similarity threshold\",\n      description:\n        \"The minimum similarity score required for a source to be considered related to the chat. The higher the number, the more similar the source must be to the chat.\",\n      zero: \"No restriction\",\n      low: \"Low (similarity score ≥ .25)\",\n      medium: \"Medium (similarity score ≥ .50)\",\n      high: \"High (similarity score ≥ .75)\",\n    },\n    reset: {\n      reset: \"Reset Vector Database\",\n      resetting: \"Clearing vectors...\",\n      confirm:\n        \"You are about to reset this workspace's vector database. This will remove all vector embeddings currently embedded.\\n\\nThe original source files will remain untouched. This action is irreversible.\",\n      error: \"Workspace vector database could not be reset!\",\n      success: \"Workspace vector database was reset!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"Performance of LLMs that do not explicitly support tool-calling is highly dependent on the model's capabilities and accuracy. Some abilities may be limited or non-functional.\",\n    provider: {\n      title: \"Workspace Agent LLM Provider\",\n      description:\n        \"The specific LLM provider & model that will be used for this workspace's @agent agent.\",\n    },\n    mode: {\n      chat: {\n        title: \"Workspace Agent Chat model\",\n        description:\n          \"The specific chat model that will be used for this workspace's @agent agent.\",\n      },\n      title: \"Workspace Agent model\",\n      description:\n        \"The specific LLM model that will be used for this workspace's @agent agent.\",\n      wait: \"-- waiting for models --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG & long-term memory\",\n        description:\n          'Allow the agent to leverage your local documents to answer a query or ask the agent to \"remember\" pieces of content for long-term memory retrieval.',\n      },\n      view: {\n        title: \"View & summarize documents\",\n        description:\n          \"Allow the agent to list and summarize the content of workspace files currently embedded.\",\n      },\n      scrape: {\n        title: \"Scrape websites\",\n        description:\n          \"Allow the agent to visit and scrape the content of websites.\",\n      },\n      generate: {\n        title: \"Generate charts\",\n        description:\n          \"Enable the default agent to generate various types of charts from data provided or given in chat.\",\n      },\n      save: {\n        title: \"Generate & save files\",\n        description:\n          \"Enable the default agent to generate and write to files that can be saved to your computer.\",\n      },\n      web: {\n        title: \"Web Search\",\n        description:\n          \"Enable your agent to search the web to answer your questions by connecting to a web-search (SERP) provider.\",\n      },\n      sql: {\n        title: \"SQL Connector\",\n        description:\n          \"Enable your agent to be able to leverage SQL to answer you questions by connecting to various SQL database providers.\",\n      },\n      default_skill:\n        \"By default, this skill is enabled, but you can disable it if you don't want it to be available to the agent.\",\n    },\n    mcp: {\n      title: \"MCP Servers\",\n      \"loading-from-config\": \"Loading MCP Servers from configuration file\",\n      \"learn-more\": \"Learn more about MCP Servers.\",\n      \"no-servers-found\": \"No MCP servers found\",\n      \"tool-warning\":\n        \"For the best performance, consider disabling unwanted tools to conserve context.\",\n      \"tools-enabled\": \"tools enabled\",\n      \"stop-server\": \"Stop MCP Server\",\n      \"start-server\": \"Start MCP Server\",\n      \"delete-server\": \"Delete MCP Server\",\n      \"tool-count-warning\":\n        \"This MCP server has <b>{{count}} tools enabled</b> that will consume context in every chat.<br />Consider disabling unwanted tools to conserve context.\",\n      \"startup-command\": \"Startup Command\",\n      command: \"Command\",\n      arguments: \"Arguments\",\n      \"not-running-warning\":\n        \"This MCP server is not running - it may be stopped or experiencing an error on startup.\",\n      \"tool-call-arguments\": \"Tool call arguments\",\n    },\n    settings: {\n      title: \"Agent Skill Settings\",\n      \"max-tool-calls\": {\n        title: \"Max Tool Calls Per Response\",\n        description:\n          \"The maximum number of tools an agent can chain to generate a single response. This prevents runaway tool calls and infinite loops.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Intelligent Skill Selection\",\n        \"beta-badge\": \"Beta\",\n        description:\n          \"Enable unlimited tools and cut token usage by up to 80% per query — AnythingLLM automatically selects the right skills for every prompt.\",\n        \"max-tools\": {\n          title: \"Max Tools\",\n          description:\n            \"The maximum number of tools to select for each query. We recommend setting this to higher values for larger context models.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Workspace Chats\",\n    description:\n      \"These are all the recorded chats and messages that have been sent by users ordered by their creation date.\",\n    export: \"Export\",\n    table: {\n      id: \"ID\",\n      by: \"Sent By\",\n      workspace: \"Workspace\",\n      prompt: \"Prompt\",\n      response: \"Response\",\n      at: \"Sent At\",\n    },\n  },\n  customization: {\n    interface: {\n      title: \"UI Preferences\",\n      description: \"Set your UI preferences for AnythingLLM.\",\n    },\n    branding: {\n      title: \"Branding & Whitelabeling\",\n      description:\n        \"White-label your AnythingLLM instance with custom branding.\",\n    },\n    chat: {\n      title: \"Chat\",\n      description: \"Set your chat preferences for AnythingLLM.\",\n      auto_submit: {\n        title: \"Auto-Submit Speech Input\",\n        description:\n          \"Automatically submit speech input after a period of silence\",\n      },\n      auto_speak: {\n        title: \"Auto-Speak Responses\",\n        description: \"Automatically speak responses from the AI\",\n      },\n      spellcheck: {\n        title: \"Enable Spellcheck\",\n        description: \"Enable or disable spellcheck in the chat input field\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Theme\",\n        description: \"Select your preferred color theme for the application.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Show Scrollbar\",\n        description: \"Enable or disable the scrollbar in the chat window.\",\n      },\n      \"support-email\": {\n        title: \"Support Email\",\n        description:\n          \"Set the support email address that should be accessible by users when they need help.\",\n      },\n      \"app-name\": {\n        title: \"Name\",\n        description:\n          \"Set a name that is displayed on the login page to all users.\",\n      },\n      \"display-language\": {\n        title: \"Display Language\",\n        description:\n          \"Select the preferred language to render AnythingLLM's UI in - when translations are available.\",\n      },\n      logo: {\n        title: \"Brand Logo\",\n        description: \"Upload your custom logo to showcase on all pages.\",\n        add: \"Add a custom logo\",\n        recommended: \"Recommended size: 800 x 200\",\n        remove: \"Remove\",\n        replace: \"Replace\",\n      },\n      \"browser-appearance\": {\n        title: \"Browser Appearance\",\n        description:\n          \"Customize the appearance of the browser tab and title when the app is open.\",\n        tab: {\n          title: \"Title\",\n          description:\n            \"Set a custom tab title when the app is open in a browser.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description: \"Use a custom favicon for the browser tab.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Sidebar Footer Items\",\n        description:\n          \"Customize the footer items displayed on the bottom of the sidebar.\",\n        icon: \"Icon\",\n        link: \"Link\",\n      },\n      \"render-html\": {\n        title: \"Render HTML in chat\",\n        description:\n          \"Render HTML responses in assistant responses.\\nThis can result in a much higher fidelity of response quality, but can also lead to potential security risks.\",\n      },\n    },\n  },\n  api: {\n    title: \"API Keys\",\n    description:\n      \"API keys allow the holder to programmatically access and manage this AnythingLLM instance.\",\n    link: \"Read the API documentation\",\n    generate: \"Generate New API Key\",\n    table: {\n      key: \"API Key\",\n      by: \"Created By\",\n      created: \"Created\",\n    },\n  },\n  llm: {\n    title: \"LLM Preference\",\n    description:\n      \"These are the credentials and settings for your preferred LLM chat & embedding provider. It is important that these keys are current and correct, or else AnythingLLM will not function properly.\",\n    provider: \"LLM Provider\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Azure Service Endpoint\",\n        api_key: \"API Key\",\n        chat_deployment_name: \"Chat Deployment Name\",\n        chat_model_token_limit: \"Chat Model Token Limit\",\n        model_type: \"Model Type\",\n        model_type_tooltip:\n          \"If your deployment uses a reasoning model (o1, o1-mini, o3-mini, etc.), set this to “Reasoning”. Otherwise, your chat requests may fail.\",\n        default: \"Default\",\n        reasoning: \"Reasoning\",\n      },\n    },\n  },\n  transcription: {\n    title: \"Transcription Model Preference\",\n    description:\n      \"These are the credentials and settings for your preferred transcription model provider. Its important these keys are current and correct or else media files and audio will not transcribe.\",\n    provider: \"Transcription Provider\",\n    \"warn-start\":\n      \"Using the local whisper model on machines with limited RAM or CPU can stall AnythingLLM when processing media files.\",\n    \"warn-recommend\":\n      \"We recommend at least 2GB of RAM and upload files <10Mb.\",\n    \"warn-end\":\n      \"The built-in model will automatically download on the first use.\",\n  },\n  embedding: {\n    title: \"Embedding Preference\",\n    \"desc-start\":\n      \"When using an LLM that does not natively support an embedding engine - you may need to additionally specify credentials for embedding text.\",\n    \"desc-end\":\n      \"Embedding is the process of turning text into vectors. These credentials are required to turn your files and prompts into a format which AnythingLLM can use to process.\",\n    provider: {\n      title: \"Embedding Provider\",\n    },\n  },\n  text: {\n    title: \"Text splitting & Chunking Preferences\",\n    \"desc-start\":\n      \"Sometimes, you may want to change the default way that new documents are split and chunked before being inserted into your vector database.\",\n    \"desc-end\":\n      \"You should only modify this setting if you understand how text splitting works and it's side effects.\",\n    size: {\n      title: \"Text Chunk Size\",\n      description:\n        \"This is the maximum length of characters that can be present in a single vector.\",\n      recommend: \"Embed model maximum length is\",\n    },\n    overlap: {\n      title: \"Text Chunk Overlap\",\n      description:\n        \"This is the maximum overlap of characters that occurs during chunking between two adjacent text chunks.\",\n    },\n  },\n  vector: {\n    title: \"Vector Database\",\n    description:\n      \"These are the credentials and settings for how your AnythingLLM instance will function. It's important these keys are current and correct.\",\n    provider: {\n      title: \"Vector Database Provider\",\n      description: \"There is no configuration needed for LanceDB.\",\n    },\n  },\n  embeddable: {\n    title: \"Embeddable Chat Widgets\",\n    description:\n      \"Embeddable chat widgets are public facing chat interfaces that are tied to a single workspace. These allow you to build workspaces that then you can publish to the world.\",\n    create: \"Create embed\",\n    table: {\n      workspace: \"Workspace\",\n      chats: \"Sent Chats\",\n      active: \"Active Domains\",\n      created: \"Created\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Embed Chat History\",\n    export: \"Export\",\n    description:\n      \"These are all the recorded chats and messages from any embed that you have published.\",\n    table: {\n      embed: \"Embed\",\n      sender: \"Sender\",\n      message: \"Message\",\n      response: \"Response\",\n      at: \"Sent At\",\n    },\n  },\n  security: {\n    title: \"Security\",\n    multiuser: {\n      title: \"Multi-User Mode\",\n      description:\n        \"Set up your instance to support your team by activating Multi-User Mode.\",\n      enable: {\n        \"is-enable\": \"Multi-User Mode is Enabled\",\n        enable: \"Enable Multi-User Mode\",\n        description:\n          \"By default, you will be the only admin. As an admin you will need to create accounts for all new users or admins. Do not lose your password as only an Admin user can reset passwords.\",\n        username: \"Admin account username\",\n        password: \"Admin account password\",\n      },\n    },\n    password: {\n      title: \"Password Protection\",\n      description:\n        \"Protect your AnythingLLM instance with a password. If you forget this there is no recovery method so ensure you save this password.\",\n      \"password-label\": \"Instance Password\",\n    },\n  },\n  event: {\n    title: \"Event Logs\",\n    description:\n      \"View all actions and events happening on this instance for monitoring.\",\n    clear: \"Clear Event Logs\",\n    table: {\n      type: \"Event Type\",\n      user: \"User\",\n      occurred: \"Occurred At\",\n    },\n  },\n  privacy: {\n    title: \"Privacy & Data-Handling\",\n    description:\n      \"This is your configuration for how connected third party providers and AnythingLLM handle your data.\",\n    anonymous: \"Anonymous Telemetry Enabled\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Search data connectors\",\n    \"no-connectors\": \"No data connectors found.\",\n    obsidian: {\n      vault_location: \"Vault Location\",\n      vault_description:\n        \"Select your Obsidian vault folder to import all notes and their connections.\",\n      selected_files: \"Found {{count}} markdown files\",\n      importing: \"Importing vault...\",\n      import_vault: \"Import Vault\",\n      processing_time:\n        \"This may take a while depending on the size of your vault.\",\n      vault_warning:\n        \"To avoid any conflicts, make sure your Obsidian vault is not currently open.\",\n    },\n    github: {\n      name: \"GitHub Repo\",\n      description:\n        \"Import an entire public or private GitHub repository in a single click.\",\n      URL: \"GitHub Repo URL\",\n      URL_explained: \"Url of the GitHub repo you wish to collect.\",\n      token: \"GitHub Access Token\",\n      optional: \"optional\",\n      token_explained: \"Access Token to prevent rate limiting.\",\n      token_explained_start: \"Without a \",\n      token_explained_link1: \"Personal Access Token\",\n      token_explained_middle:\n        \", the GitHub API may limit the number of files that can be collected due to rate limits. You can \",\n      token_explained_link2: \"create a temporary Access Token\",\n      token_explained_end: \" to avoid this issue.\",\n      ignores: \"File Ignores\",\n      git_ignore:\n        \"List in .gitignore format to ignore specific files during collection. Press enter after each entry you want to save.\",\n      task_explained:\n        \"Once complete, all files will be available for embedding into workspaces in the document picker.\",\n      branch: \"Branch you wish to collect files from.\",\n      branch_loading: \"-- loading available branches --\",\n      branch_explained: \"Branch you wish to collect files from.\",\n      token_information:\n        \"Without filling out the <b>GitHub Access Token</b> this data connector will only be able to collect the <b>top-level</b> files of the repo due to GitHub's public API rate-limits.\",\n      token_personal:\n        \"Get a free Personal Access Token with a GitHub account here.\",\n    },\n    gitlab: {\n      name: \"GitLab Repo\",\n      description:\n        \"Import an entire public or private GitLab repository in a single click.\",\n      URL: \"GitLab Repo URL\",\n      URL_explained: \"URL of the GitLab repo you wish to collect.\",\n      token: \"GitLab Access Token\",\n      optional: \"optional\",\n      token_description:\n        \"Select additional entities to fetch from the GitLab API.\",\n      token_explained_start: \"Without a \",\n      token_explained_link1: \"Personal Access Token\",\n      token_explained_middle:\n        \", the GitLab API may limit the number of files that can be collected due to rate limits. You can \",\n      token_explained_link2: \"create a temporary Access Token\",\n      token_explained_end: \" to avoid this issue.\",\n      fetch_issues: \"Fetch Issues as Documents\",\n      ignores: \"File Ignores\",\n      git_ignore:\n        \"List in .gitignore format to ignore specific files during collection. Press enter after each entry you want to save.\",\n      task_explained:\n        \"Once complete, all files will be available for embedding into workspaces in the document picker.\",\n      branch: \"Branch you wish to collect files from\",\n      branch_loading: \"-- loading available branches --\",\n      branch_explained: \"Branch you wish to collect files from.\",\n      token_information:\n        \"Without filling out the <b>GitLab Access Token</b> this data connector will only be able to collect the <b>top-level</b> files of the repo due to GitLab's public API rate-limits.\",\n      token_personal:\n        \"Get a free Personal Access Token with a GitLab account here.\",\n    },\n    youtube: {\n      name: \"YouTube Transcript\",\n      description:\n        \"Import the transcription of an entire YouTube video from a link.\",\n      URL: \"YouTube Video URL\",\n      URL_explained_start:\n        \"Enter the URL of any YouTube video to fetch its transcript. The video must have \",\n      URL_explained_link: \"closed captions\",\n      URL_explained_end: \" available.\",\n      task_explained:\n        \"Once complete, the transcript will be available for embedding into workspaces in the document picker.\",\n    },\n    \"website-depth\": {\n      name: \"Bulk Link Scraper\",\n      description: \"Scrape a website and its sub-links up to a certain depth.\",\n      URL: \"Website URL\",\n      URL_explained: \"URL of the website you want to scrape.\",\n      depth: \"Crawl Depth\",\n      depth_explained:\n        \"This is the number of child-links that the worker should follow from the origin URL.\",\n      max_pages: \"Maximum Pages\",\n      max_pages_explained: \"Maximum number of links to scrape.\",\n      task_explained:\n        \"Once complete, all scraped content will be available for embedding into workspaces in the document picker.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Import an entire Confluence page in a single click.\",\n      deployment_type: \"Confluence deployment type\",\n      deployment_type_explained:\n        \"Determine if your Confluence instance is hosted on Atlassian cloud or self-hosted.\",\n      base_url: \"Confluence base URL\",\n      base_url_explained: \"This is the base URL of your Confluence space.\",\n      space_key: \"Confluence space key\",\n      space_key_explained:\n        \"This is the spaces key of your confluence instance that will be used. Usually begins with ~\",\n      username: \"Confluence Username\",\n      username_explained: \"Your Confluence username\",\n      auth_type: \"Confluence Auth Type\",\n      auth_type_explained:\n        \"Select the authentication type you want to use to access your Confluence pages.\",\n      auth_type_username: \"Username and Access Token\",\n      auth_type_personal: \"Personal Access Token\",\n      token: \"Confluence Access Token\",\n      token_explained_start:\n        \"You need to provide an access token for authentication. You can generate an access token\",\n      token_explained_link: \"here\",\n      token_desc: \"Access token for authentication\",\n      pat_token: \"Confluence Personal Access Token\",\n      pat_token_explained: \"Your Confluence personal access token.\",\n      bypass_ssl: \"Bypass SSL Certificate Validation\",\n      bypass_ssl_explained:\n        \"Enable this option to bypass SSL certificate validation for self-hosted confluence instances with self-signed certificate\",\n      task_explained:\n        \"Once complete, the page content will be available for embedding into workspaces in the document picker.\",\n    },\n    manage: {\n      documents: \"Documents\",\n      \"data-connectors\": \"Data Connectors\",\n      \"desktop-only\":\n        \"Editing these settings are only available on a desktop device. Please access this page on your desktop to continue.\",\n      dismiss: \"Dismiss\",\n      editing: \"Editing\",\n    },\n    directory: {\n      \"my-documents\": \"My Documents\",\n      \"new-folder\": \"New Folder\",\n      \"total-documents_one\": \"{{count}} document\",\n      \"total-documents_other\": \"{{count}} documents\",\n      \"search-document\": \"Search for document\",\n      \"no-documents\": \"No Documents\",\n      \"move-workspace\": \"Move to Workspace\",\n      \"delete-confirmation\":\n        \"Are you sure you want to delete these files and folders?\\nThis will remove the files from the system and remove them from any existing workspaces automatically.\\nThis action is not reversible.\",\n      \"removing-message\":\n        \"Removing {{count}} documents and {{folderCount}} folders. Please wait.\",\n      \"move-success\": \"Successfully moved {{count}} documents.\",\n      no_docs: \"No Documents\",\n      select_all: \"Select All\",\n      deselect_all: \"Deselect All\",\n      remove_selected: \"Remove Selected\",\n      costs: \"*One time cost for embeddings\",\n      save_embed: \"Save and Embed\",\n    },\n    upload: {\n      \"processor-offline\": \"Document Processor Unavailable\",\n      \"processor-offline-desc\":\n        \"We can't upload your files right now because the document processor is offline. Please try again later.\",\n      \"click-upload\": \"Click to upload or drag and drop\",\n      \"file-types\":\n        \"supports text files, csv's, spreadsheets, audio files, and more!\",\n      \"or-submit-link\": \"or submit a link\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"Fetching...\",\n      \"fetch-website\": \"Fetch website\",\n      \"privacy-notice\":\n        \"These files will be uploaded to the document processor running on this AnythingLLM instance. These files are not sent or shared with a third party.\",\n    },\n    pinning: {\n      what_pinning: \"What is document pinning?\",\n      pin_explained_block1:\n        \"When you <b>pin</b> a document in AnythingLLM we will inject the entire content of the document into your prompt window for your LLM to fully comprehend.\",\n      pin_explained_block2:\n        \"This works best with <b>large-context models</b> or small files that are critical to its knowledge-base.\",\n      pin_explained_block3:\n        \"If you are not getting the answers you desire from AnythingLLM by default then pinning is a great way to get higher quality answers in a click.\",\n      accept: \"Okay, got it\",\n    },\n    watching: {\n      what_watching: \"What does watching a document do?\",\n      watch_explained_block1:\n        \"When you <b>watch</b> a document in AnythingLLM we will <i>automatically</i> sync your document content from it's original source on regular intervals. This will automatically update the content in every workspace where this file is managed.\",\n      watch_explained_block2:\n        \"This feature currently supports online-based content and will not be available for manually uploaded documents.\",\n      watch_explained_block3_start:\n        \"You can manage what documents are watched from the \",\n      watch_explained_block3_link: \"File manager\",\n      watch_explained_block3_end: \" admin view.\",\n      accept: \"Okay, got it\",\n    },\n  },\n  chat_window: {\n    attachments_processing: \"Attachments are processing. Please wait...\",\n    send_message: \"Send a message\",\n    attach_file: \"Attach a file to this chat\",\n    text_size: \"Change text size.\",\n    microphone: \"Speak your prompt.\",\n    send: \"Send prompt message to workspace\",\n    tts_speak_message: \"TTS Speak message\",\n    copy: \"Copy\",\n    regenerate: \"Regenerate\",\n    regenerate_response: \"Regenerate response\",\n    good_response: \"Good response\",\n    more_actions: \"More actions\",\n    sources: \"Sources\",\n    source_count_one: \"{{count}} reference\",\n    source_count_other: \"{{count}} references\",\n    document: \"Document\",\n    similarity_match: \"match\",\n    pause_tts_speech_message: \"Pause TTS speech of message\",\n    fork: \"Fork\",\n    delete: \"Delete\",\n    cancel: \"Cancel\",\n    submit: \"Submit\",\n    edit_prompt: \"Edit prompt\",\n    edit_response: \"Edit response\",\n    edit_info_user:\n      '\"Submit\" regenerates the AI response. \"Save\" updates your message only.',\n    edit_info_assistant:\n      \"Your changes will be saved directly to this response.\",\n    see_less: \"See Less\",\n    see_more: \"See More\",\n    preset_reset_description: \"Clear your chat history and begin a new chat\",\n    preset_exit_description: \"Halt the current agent session\",\n    add_new_preset: \" Add New Preset\",\n    add_new: \"Add new\",\n    edit: \"Edit\",\n    publish: \"Publish\",\n    stop_generating: \"Stop generating response\",\n    command: \"Command\",\n    your_command: \"your-command\",\n    placeholder_prompt:\n      \"This is the content that will be injected in front of your prompt.\",\n    description: \"Description\",\n    placeholder_description: \"Responds with a poem about LLMs.\",\n    save: \"Save\",\n    small: \"Small\",\n    normal: \"Normal\",\n    large: \"Large\",\n    tools: \"Tools\",\n    browse: \"Browse\",\n    text_size_label: \"Text Size\",\n    select_model: \"Select Model\",\n    slash_commands: \"Slash Commands\",\n    agent_skills: \"Agent Skills\",\n    manage_agent_skills: \"Manage Agent Skills\",\n    start_agent_session: \"Start Agent Session\",\n    agent_skills_disabled_in_session:\n      \"Can't modify skills during an active agent session. Use /exit to end the session first.\",\n    use_agent_session_to_use_tools:\n      \"You can use tools in chat by starting an agent session with '@agent' at the beginning of your prompt.\",\n    workspace_llm_manager: {\n      search: \"Search\",\n      loading_workspace_settings: \"Loading workspace settings...\",\n      available_models: \"Available Models for {{provider}}\",\n      available_models_description: \"Select a model to use for this workspace.\",\n      save: \"Use this model\",\n      saving: \"Setting model as workspace default...\",\n      missing_credentials: \"This provider is missing credentials!\",\n      missing_credentials_description: \"Set up now\",\n    },\n  },\n  profile_settings: {\n    edit_account: \"Edit Account\",\n    profile_picture: \"Profile Picture\",\n    remove_profile_picture: \"Remove Profile Picture\",\n    username: \"Username\",\n    new_password: \"New Password\",\n    password_description: \"Password must be at least 8 characters long\",\n    cancel: \"Cancel\",\n    update_account: \"Update Account\",\n    theme: \"Theme Preference\",\n    language: \"Preferred language\",\n    failed_upload: \"Failed to upload profile picture: {{error}}\",\n    upload_success: \"Profile picture uploaded.\",\n    failed_remove: \"Failed to remove profile picture: {{error}}\",\n    profile_updated: \"Profile updated.\",\n    failed_update_user: \"Failed to update user: {{error}}\",\n    account: \"Account\",\n    support: \"Support\",\n    signout: \"Sign out\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Keyboard Shortcuts\",\n    shortcuts: {\n      settings: \"Open Settings\",\n      workspaceSettings: \"Open Current Workspace Settings\",\n      home: \"Go to Home\",\n      workspaces: \"Manage Workspaces\",\n      apiKeys: \"API Keys Settings\",\n      llmPreferences: \"LLM Preferences\",\n      chatSettings: \"Chat Settings\",\n      help: \"Show keyboard shortcuts help\",\n      showLLMSelector: \"Show workspace LLM Selector\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Success!\",\n        success_description:\n          \"Your System Prompt has been published to the Community Hub!\",\n        success_thank_you: \"Thank you for sharing to the Community!\",\n        view_on_hub: \"View on Community Hub\",\n        modal_title: \"Publish System Prompt\",\n        name_label: \"Name\",\n        name_description: \"This is the display name of your system prompt.\",\n        name_placeholder: \"My System Prompt\",\n        description_label: \"Description\",\n        description_description:\n          \"This is the description of your system prompt. Use this to describe the purpose of your system prompt.\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Tags are used to label your system prompt for easier searching. You can add multiple tags. Max 5 tags. Max 20 characters per tag.\",\n        tags_placeholder: \"Type and press Enter to add tags\",\n        visibility_label: \"Visibility\",\n        public_description: \"Public system prompts are visible to everyone.\",\n        private_description: \"Private system prompts are only visible to you.\",\n        publish_button: \"Publish to Community Hub\",\n        submitting: \"Publishing...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"This is the actual system prompt that will be used to guide the LLM.\",\n        prompt_placeholder: \"Enter your system prompt here...\",\n      },\n      agent_flow: {\n        success_title: \"Success!\",\n        success_description:\n          \"Your Agent Flow has been published to the Community Hub!\",\n        success_thank_you: \"Thank you for sharing to the Community!\",\n        view_on_hub: \"View on Community Hub\",\n        modal_title: \"Publish Agent Flow\",\n        name_label: \"Name\",\n        name_description: \"This is the display name of your agent flow.\",\n        name_placeholder: \"My Agent Flow\",\n        description_label: \"Description\",\n        description_description:\n          \"This is the description of your agent flow. Use this to describe the purpose of your agent flow.\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Tags are used to label your agent flow for easier searching. You can add multiple tags. Max 5 tags. Max 20 characters per tag.\",\n        tags_placeholder: \"Type and press Enter to add tags\",\n        visibility_label: \"Visibility\",\n        submitting: \"Publishing...\",\n        submit: \"Publish to Community Hub\",\n        privacy_note:\n          \"Agent flows are always uploaded as private to protect any sensitive data. You can change the visibility in the Community Hub after publishing. Please verify your flow does not contain any sensitive or private information before publishing.\",\n      },\n      slash_command: {\n        success_title: \"Success!\",\n        success_description:\n          \"Your Slash Command has been published to the Community Hub!\",\n        success_thank_you: \"Thank you for sharing to the Community!\",\n        view_on_hub: \"View on Community Hub\",\n        modal_title: \"Publish Slash Command\",\n        name_label: \"Name\",\n        name_description: \"This is the display name of your slash command.\",\n        name_placeholder: \"My Slash Command\",\n        description_label: \"Description\",\n        description_description:\n          \"This is the description of your slash command. Use this to describe the purpose of your slash command.\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Tags are used to label your slash command for easier searching. You can add multiple tags. Max 5 tags. Max 20 characters per tag.\",\n        tags_placeholder: \"Type and press Enter to add tags\",\n        visibility_label: \"Visibility\",\n        public_description: \"Public slash commands are visible to everyone.\",\n        private_description: \"Private slash commands are only visible to you.\",\n        publish_button: \"Publish to Community Hub\",\n        submitting: \"Publishing...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"This is the prompt that will be used when the slash command is triggered.\",\n        prompt_placeholder: \"Enter your prompt here...\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Authentication Required\",\n          description:\n            \"You need to authenticate with the AnythingLLM Community Hub before publishing items.\",\n          button: \"Connect to Community Hub\",\n        },\n      },\n    },\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/es/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"Bienvenido a\",\n      getStarted: \"Comenzar\",\n    },\n    llm: {\n      title: \"Preferencia de LLM\",\n      description:\n        \"AnythingLLM puede funcionar con muchos proveedores de LLM. Este será el servicio que gestionará el chat.\",\n    },\n    userSetup: {\n      title: \"Configuración de usuario\",\n      description: \"Configura los ajustes de tu usuario.\",\n      howManyUsers: \"¿Cuántos usuarios utilizarán esta instancia?\",\n      justMe: \"Solo yo\",\n      myTeam: \"Mi equipo\",\n      instancePassword: \"Contraseña de la instancia\",\n      setPassword: \"¿Deseas establecer una contraseña?\",\n      passwordReq: \"Las contraseñas deben tener al menos 8 caracteres.\",\n      passwordWarn:\n        \"Es importante guardar esta contraseña porque no hay método de recuperación.\",\n      adminUsername: \"Nombre de usuario del administrador\",\n      adminPassword: \"Contraseña de la cuenta de administrador\",\n      adminPasswordReq: \"Las contraseñas deben tener al menos 8 caracteres.\",\n      teamHint:\n        \"Por defecto, serás el único administrador. Una vez completada la incorporación, puedes crear e invitar a otros a ser usuarios o administradores. No pierdas tu contraseña, ya que solo los administradores pueden restablecer las contraseñas.\",\n    },\n    data: {\n      title: \"Manejo de datos y privacidad\",\n      description:\n        \"Estamos comprometidos con la transparencia y el control en lo que respecta a tus datos personales.\",\n      settingsHint:\n        \"Estos ajustes se pueden reconfigurar en cualquier momento en la configuración.\",\n    },\n    survey: {\n      title: \"Bienvenido a AnythingLLM\",\n      description:\n        \"Ayúdanos a hacer que AnythingLLM se adapte a tus necesidades. Opcional.\",\n      email: \"¿Cuál es tu correo electrónico?\",\n      useCase: \"¿Para qué usarás AnythingLLM?\",\n      useCaseWork: \"Para el trabajo\",\n      useCasePersonal: \"Para uso personal\",\n      useCaseOther: \"Otro\",\n      comment: \"¿Cómo te enteraste de AnythingLLM?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube, etc. - ¡Haznos saber cómo nos encontraste!\",\n      skip: \"Omitir encuesta\",\n      thankYou: \"¡Gracias por tus comentarios!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Nombre de los espacios de trabajo\",\n    user: \"Usuario\",\n    selection: \"Selección de modelo\",\n    saving: \"Guardando...\",\n    save: \"Guardar cambios\",\n    previous: \"Página anterior\",\n    next: \"Página siguiente\",\n    optional: \"Opcional\",\n    yes: \"Sí\",\n    no: \"No\",\n    search: \"Buscar\",\n    username_requirements:\n      \"El nombre de usuario debe tener entre 2 y 32 caracteres, comenzar con una letra minúscula y solo contener letras minúsculas, números, guiones bajos, guiones y puntos.\",\n    on: \"Sobre\",\n    none: \"Ninguno\",\n    stopped: \"Parado\",\n    loading: \"Cargando\",\n    refresh: \"Renovar; revitalizar\",\n  },\n  settings: {\n    title: \"Ajustes de la instancia\",\n    invites: \"Invitaciones\",\n    users: \"Usuarios\",\n    workspaces: \"Espacios de trabajo\",\n    \"workspace-chats\": \"Chats del espacio de trabajo\",\n    customization: \"Personalización\",\n    interface: \"Preferencias de la interfaz de usuario\",\n    branding: \"Marca y marca blanca\",\n    chat: \"Chat\",\n    \"api-keys\": \"API de desarrollador\",\n    llm: \"LLM\",\n    transcription: \"Transcripción\",\n    embedder: \"Incrustador (Embedder)\",\n    \"text-splitting\": \"División de texto y fragmentación\",\n    \"voice-speech\": \"Voz y habla\",\n    \"vector-database\": \"Base de datos vectorial\",\n    embeds: \"Incrustaciones de chat\",\n    security: \"Seguridad\",\n    \"event-logs\": \"Registros de eventos\",\n    privacy: \"Privacidad y datos\",\n    \"ai-providers\": \"Proveedores de IA\",\n    \"agent-skills\": \"Habilidades del agente\",\n    admin: \"Administrador\",\n    tools: \"Herramientas\",\n    \"system-prompt-variables\": \"Variables de prompt del sistema\",\n    \"experimental-features\": \"Funciones experimentales\",\n    contact: \"Contactar con soporte\",\n    \"browser-extension\": \"Extensión del navegador\",\n    \"mobile-app\": \"AnythingLLM Móvil\",\n    \"community-hub\": {\n      title: \"Centro comunitario\",\n      trending: \"Explora las tendencias más populares\",\n      \"your-account\": \"Su cuenta\",\n      \"import-item\": \"Importar artículo\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Bienvenido a\",\n      \"placeholder-username\": \"Nombre de usuario\",\n      \"placeholder-password\": \"Contraseña\",\n      login: \"Iniciar sesión\",\n      validating: \"Validando...\",\n      \"forgot-pass\": \"Olvidé mi contraseña\",\n      reset: \"Restablecer\",\n    },\n    \"sign-in\": \"Inicia sesión en tu cuenta de {{appName}}.\",\n    \"password-reset\": {\n      title: \"Restablecimiento de contraseña\",\n      description:\n        \"Proporciona la información necesaria a continuación para restablecer tu contraseña.\",\n      \"recovery-codes\": \"Códigos de recuperación\",\n      \"back-to-login\": \"Volver al inicio de sesión\",\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Crear un agente\",\n      editWorkspace: \"Editar espacio de trabajo\",\n      uploadDocument: \"Cargar un documento\",\n    },\n    greeting: \"¿Cómo puedo ayudarte hoy?\",\n  },\n  \"new-workspace\": {\n    title: \"Nuevo espacio de trabajo\",\n    placeholder: \"Mi espacio de trabajo\",\n  },\n  \"workspaces—settings\": {\n    general: \"Ajustes generales\",\n    chat: \"Ajustes de chat\",\n    vector: \"Base de datos vectorial\",\n    members: \"Miembros\",\n    agent: \"Configuración del agente\",\n  },\n  general: {\n    vector: {\n      title: \"Recuento de vectores\",\n      description: \"Número total de vectores en tu base de datos vectorial.\",\n    },\n    names: {\n      description:\n        \"Esto solo cambiará el nombre para mostrar de tu espacio de trabajo.\",\n    },\n    message: {\n      title: \"Mensajes de chat sugeridos\",\n      description:\n        \"Personaliza los mensajes que se sugerirán a los usuarios de tu espacio de trabajo.\",\n      add: \"Agregar nuevo mensaje\",\n      save: \"Guardar mensajes\",\n      heading: \"Explícame\",\n      body: \"los beneficios de AnythingLLM\",\n    },\n    delete: {\n      title: \"Eliminar espacio de trabajo\",\n      description:\n        \"Elimina este espacio de trabajo y todos sus datos. Esto eliminará el espacio de trabajo para todos los usuarios.\",\n      delete: \"Eliminar espacio de trabajo\",\n      deleting: \"Eliminando espacio de trabajo...\",\n      \"confirm-start\": \"Estás a punto de eliminar todo tu\",\n      \"confirm-end\":\n        \"espacio de trabajo. Esto eliminará todas las incrustaciones de vectores en tu base de datos vectorial.\\n\\nLos archivos fuente originales permanecerán intactos. Esta acción es irreversible.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Proveedor de LLM del espacio de trabajo\",\n      description:\n        \"El proveedor y modelo de LLM específico que se utilizará para este espacio de trabajo. Por defecto, utiliza el proveedor y la configuración de LLM del sistema.\",\n      search: \"Buscar todos los proveedores de LLM\",\n    },\n    model: {\n      title: \"Modelo de chat del espacio de trabajo\",\n      description:\n        \"El modelo de chat específico que se utilizará para este espacio de trabajo. Si está vacío, utilizará la preferencia de LLM del sistema.\",\n    },\n    mode: {\n      title: \"Modo de chat\",\n      chat: {\n        title: \"Chat\",\n        description:\n          'proporcionará respuestas basándose en el conocimiento general del LLM y en el contexto del documento que se encuentre disponible. Para utilizar las herramientas, deberá utilizar el comando \"@agent\".',\n      },\n      query: {\n        title: \"Consulta\",\n        description:\n          'proporcionará respuestas <b>solo</b> si se encuentra el contexto del documento.<br />Deberá utilizar el comando \"@agent\" para utilizar las herramientas.',\n      },\n      automatic: {\n        title: \"Coche\",\n        description:\n          'Utilizará automáticamente las herramientas si el modelo y el proveedor admiten la llamada a herramientas nativas. Si no se admiten las herramientas nativas, deberá utilizar el comando \"@agent\" para utilizar las herramientas.',\n      },\n    },\n    history: {\n      title: \"Historial de chat\",\n      \"desc-start\":\n        \"El número de chats anteriores que se incluirán en la memoria a corto plazo de la respuesta.\",\n      recommend: \"Recomendado 20.\",\n      \"desc-end\":\n        \"Cualquier valor superior a 45 es probable que provoque fallos continuos en el chat dependiendo del tamaño del mensaje.\",\n    },\n    prompt: {\n      title: \"Prompt del sistema\",\n      description:\n        \"El prompt que se utilizará en este espacio de trabajo. Define el contexto y las instrucciones para que la IA genere una respuesta. Debes proporcionar un prompt cuidadosamente elaborado para que la IA pueda generar una respuesta relevante y precisa.\",\n      history: {\n        title: \"Historial de prompts del sistema\",\n        clearAll: \"Borrar todo\",\n        noHistory: \"No hay historial de prompts del sistema disponible\",\n        restore: \"Restaurar\",\n        delete: \"Eliminar\",\n        publish: \"Publicar en el Centro de la Comunidad\",\n        deleteConfirm:\n          \"¿Estás seguro de que quieres eliminar este elemento del historial?\",\n        clearAllConfirm:\n          \"¿Estás seguro de que quieres borrar todo el historial? Esta acción no se puede deshacer.\",\n        expand: \"Expandir\",\n      },\n    },\n    refusal: {\n      title: \"Respuesta de rechazo en modo de consulta\",\n      \"desc-start\": \"Cuando estés en modo de\",\n      query: \"consulta\",\n      \"desc-end\":\n        \", es posible que desees devolver una respuesta de rechazo personalizada cuando no se encuentre contexto.\",\n      \"tooltip-title\": \"¿Por qué veo esto?\",\n      \"tooltip-description\":\n        \"Estás en modo de consulta, que solo utiliza información de tus documentos. Cambia al modo de chat para conversaciones más flexibles, o haz clic aquí para visitar nuestra documentación y obtener más información sobre los modos de chat.\",\n    },\n    temperature: {\n      title: \"Temperatura del LLM\",\n      \"desc-start\":\n        'Esta configuración controla cuán \"creativas\" serán tus respuestas del LLM.',\n      \"desc-end\":\n        \"Cuanto mayor sea el número, más creativo. Para algunos modelos, esto puede llevar a respuestas incoherentes si se establece un valor demasiado alto.\",\n      hint: \"La mayoría de los LLM tienen varios rangos aceptables de valores válidos. Consulta a tu proveedor de LLM para obtener esa información.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Identificador de la base de datos vectorial\",\n    snippets: {\n      title: \"Fragmentos de contexto máximos\",\n      description:\n        \"Esta configuración controla la cantidad máxima de fragmentos de contexto que se enviarán al LLM por chat o consulta.\",\n      recommend: \"Recomendado: 4\",\n    },\n    doc: {\n      title: \"Umbral de similitud de documentos\",\n      description:\n        \"La puntuación de similitud mínima requerida para que una fuente se considere relacionada con el chat. Cuanto mayor sea el número, más similar debe ser la fuente al chat.\",\n      zero: \"Sin restricción\",\n      low: \"Bajo (puntuación de similitud ≥ .25)\",\n      medium: \"Medio (puntuación de similitud ≥ .50)\",\n      high: \"Alto (puntuación de similitud ≥ .75)\",\n    },\n    reset: {\n      reset: \"Restablecer base de datos vectorial\",\n      resetting: \"Borrando vectores...\",\n      confirm:\n        \"Estás a punto de restablecer la base de datos vectorial de este espacio de trabajo. Esto eliminará todas las incrustaciones de vectores actualmente incrustadas.\\n\\nLos archivos fuente originales permanecerán intactos. Esta acción es irreversible.\",\n      error:\n        \"¡No se pudo restablecer la base de datos vectorial del espacio de trabajo!\",\n      success:\n        \"¡La base de datos vectorial del espacio de trabajo se restableció!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"El rendimiento de los LLM que no admiten explícitamente la llamada a herramientas depende en gran medida de las capacidades y la precisión del modelo. Algunas habilidades pueden ser limitadas o no funcionales.\",\n    provider: {\n      title: \"Proveedor de LLM del agente del espacio de trabajo\",\n      description:\n        \"El proveedor y modelo de LLM específico que se utilizará para el agente @agent de este espacio de trabajo.\",\n    },\n    mode: {\n      chat: {\n        title: \"Modelo de chat del agente del espacio de trabajo\",\n        description:\n          \"El modelo de chat específico que se utilizará para el agente @agent de este espacio de trabajo.\",\n      },\n      title: \"Modelo de agente del espacio de trabajo\",\n      description:\n        \"El modelo de LLM específico que se utilizará para el agente @agent de este espacio de trabajo.\",\n      wait: \"-- esperando modelos --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG y memoria a largo plazo\",\n        description:\n          'Permite que el agente aproveche tus documentos locales para responder una consulta o pídele al agente que \"recuerde\" fragmentos de contenido para la recuperación de memoria a largo plazo.',\n      },\n      view: {\n        title: \"Ver y resumir documentos\",\n        description:\n          \"Permite que el agente liste y resuma el contenido de los archivos del espacio de trabajo actualmente incrustados.\",\n      },\n      scrape: {\n        title: \"Extraer sitios web\",\n        description:\n          \"Permite que el agente visite y extraiga el contenido de los sitios web.\",\n      },\n      generate: {\n        title: \"Generar gráficos\",\n        description:\n          \"Habilita al agente predeterminado para generar varios tipos de gráficos a partir de datos proporcionados o dados en el chat.\",\n      },\n      save: {\n        title: \"Generar y guardar archivos en el navegador\",\n        description:\n          \"Habilita al agente predeterminado para generar y escribir en archivos que se guardan y se pueden descargar en tu navegador.\",\n      },\n      web: {\n        title: \"Búsqueda y navegación web en vivo\",\n        description:\n          \"Permita que su agente acceda a internet para responder a sus preguntas, conectándolo a un proveedor de búsqueda web (SERP).\",\n      },\n      sql: {\n        title: \"Conector SQL\",\n        description:\n          \"Permita que su agente pueda utilizar SQL para responder a sus preguntas, conectándose con diferentes proveedores de bases de datos SQL.\",\n      },\n      default_skill:\n        \"Por defecto, esta función está activada, pero puede desactivarla si no desea que esté disponible para el agente.\",\n    },\n    mcp: {\n      title: \"Servidores MCP\",\n      \"loading-from-config\":\n        \"Cargar servidores MCP desde el archivo de configuración\",\n      \"learn-more\": \"Aprenda más sobre los servidores MCP.\",\n      \"no-servers-found\": \"No se encontraron servidores MCP.\",\n      \"tool-warning\":\n        \"Para obtener el mejor rendimiento, considere desactivar las herramientas innecesarias para conservar el contexto.\",\n      \"stop-server\": \"Detener el servidor MCP\",\n      \"start-server\": \"Iniciar el servidor MCP\",\n      \"delete-server\": \"Eliminar el servidor MCP\",\n      \"tool-count-warning\":\n        \"Este servidor de MCP tiene <b> herramientas habilitadas</b> que consumirán contexto en cada conversación.<br /> Considere desactivar las herramientas no deseadas para ahorrar contexto.\",\n      \"startup-command\": \"Comando inicial\",\n      command: \"Órden\",\n      arguments: \"Argumentos\",\n      \"not-running-warning\":\n        \"Este servidor de MCP no está funcionando; podría estar detenido o estar experimentando un error al iniciarse.\",\n      \"tool-call-arguments\": \"Argumentos de llamada de función\",\n      \"tools-enabled\": \"herramientas habilitadas\",\n    },\n    settings: {\n      title: \"Configuración de habilidades del agente\",\n      \"max-tool-calls\": {\n        title: \"Número máximo de llamadas a funciones Max Tool por respuesta\",\n        description:\n          \"El número máximo de herramientas que un agente puede encadenar para generar una única respuesta. Esto evita que se realicen llamadas a herramientas de forma descontrolada y que se produzcan bucles infinitos.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Selección inteligente de habilidades\",\n        \"beta-badge\": \"Versión preliminar\",\n        description:\n          \"Permite el uso ilimitado de herramientas y reduce el consumo de tokens hasta en un 80% por consulta: AnythingLLM selecciona automáticamente las habilidades adecuadas para cada solicitud.\",\n        \"max-tools\": {\n          title: \"Herramientas Max\",\n          description:\n            \"El número máximo de herramientas que se pueden seleccionar para cada consulta. Recomendamos establecer este valor en un número más alto para modelos con un contexto más amplio.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Chats del espacio de trabajo\",\n    description:\n      \"Estos son todos los chats y mensajes grabados que han sido enviados por los usuarios, ordenados por su fecha de creación.\",\n    export: \"Exportar\",\n    table: {\n      id: \"ID\",\n      by: \"Enviado por\",\n      workspace: \"Espacio de trabajo\",\n      prompt: \"Prompt\",\n      response: \"Respuesta\",\n      at: \"Enviado el\",\n    },\n  },\n  customization: {\n    interface: {\n      title: \"Preferencias de la interfaz de usuario\",\n      description:\n        \"Establece tus preferencias de la interfaz de usuario para AnythingLLM.\",\n    },\n    branding: {\n      title: \"Marca y marca blanca\",\n      description:\n        \"Personaliza tu instancia de AnythingLLM con tu propia marca.\",\n    },\n    chat: {\n      title: \"Chat\",\n      description: \"Establece tus preferencias de chat para AnythingLLM.\",\n      auto_submit: {\n        title: \"Envío automático de entrada de voz\",\n        description:\n          \"Enviar automáticamente la entrada de voz después de un período de silencio\",\n      },\n      auto_speak: {\n        title: \"Hablar respuestas automáticamente\",\n        description: \"Hablar automáticamente las respuestas de la IA\",\n      },\n      spellcheck: {\n        title: \"Habilitar corrector ortográfico\",\n        description:\n          \"Habilitar o deshabilitar el corrector ortográfico en el campo de entrada del chat\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Tema\",\n        description:\n          \"Selecciona tu tema de color preferido para la aplicación.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Mostrar barra de desplazamiento\",\n        description:\n          \"Habilitar o deshabilitar la barra de desplazamiento en la ventana de chat.\",\n      },\n      \"support-email\": {\n        title: \"Correo electrónico de soporte\",\n        description:\n          \"Establece la dirección de correo electrónico de soporte a la que los usuarios pueden acceder cuando necesiten ayuda.\",\n      },\n      \"app-name\": {\n        title: \"Nombre\",\n        description:\n          \"Establece un nombre que se mostrará en la página de inicio de sesión para todos los usuarios.\",\n      },\n      \"display-language\": {\n        title: \"Idioma de visualización\",\n        description:\n          \"Selecciona el idioma preferido para renderizar la interfaz de usuario de AnythingLLM, cuando las traducciones estén disponibles.\",\n      },\n      logo: {\n        title: \"Logotipo de la marca\",\n        description:\n          \"Sube tu logotipo personalizado para mostrarlo en todas las páginas.\",\n        add: \"Agregar un logotipo personalizado\",\n        recommended: \"Tamaño recomendado: 800 x 200\",\n        remove: \"Eliminar\",\n        replace: \"Reemplazar\",\n      },\n      \"browser-appearance\": {\n        title: \"Apariencia del navegador\",\n        description:\n          \"Personaliza la apariencia de la pestaña y el título del navegador cuando la aplicación está abierta.\",\n        tab: {\n          title: \"Título\",\n          description:\n            \"Establece un título de pestaña personalizado cuando la aplicación está abierta en un navegador.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description:\n            \"Usa un favicon personalizado para la pestaña del navegador.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Elementos del pie de página de la barra lateral\",\n        description:\n          \"Personaliza los elementos del pie de página que se muestran en la parte inferior de la barra lateral.\",\n        icon: \"Icono\",\n        link: \"Enlace\",\n      },\n      \"render-html\": {\n        title: \"Renderizar HTML en el chat\",\n        description:\n          \"Generar respuestas en HTML en las respuestas del asistente.\\nEsto puede resultar en una mayor calidad de las respuestas, pero también puede generar posibles riesgos de seguridad.\",\n      },\n    },\n  },\n  api: {\n    title: \"Claves de API\",\n    description:\n      \"Las claves de API permiten al titular acceder y administrar programáticamente esta instancia de AnythingLLM.\",\n    link: \"Leer la documentación de la API\",\n    generate: \"Generar nueva clave de API\",\n    table: {\n      key: \"Clave de API\",\n      by: \"Creado por\",\n      created: \"Creado\",\n    },\n  },\n  llm: {\n    title: \"Preferencia de LLM\",\n    description:\n      \"Estas son las credenciales y la configuración de tu proveedor preferido de chat e incrustación de LLM. Es importante que estas claves estén actualizadas y sean correctas, de lo contrario, AnythingLLM no funcionará correctamente.\",\n    provider: \"Proveedor de LLM\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Punto de conexión del servicio de Azure\",\n        api_key: \"Clave de API\",\n        chat_deployment_name: \"Nombre de la implementación del chat\",\n        chat_model_token_limit: \"Límite de tokens del modelo de chat\",\n        model_type: \"Tipo de modelo\",\n        default: \"Predeterminado\",\n        reasoning: \"Razonamiento\",\n        model_type_tooltip:\n          'Si su implementación utiliza un modelo de razonamiento (o1, o1-mini, o3-mini, etc.), configure esto como \"Razonamiento\". De lo contrario, sus solicitudes de chat podrían fallar.',\n      },\n    },\n  },\n  transcription: {\n    title: \"Preferencia del modelo de transcripción\",\n    description:\n      \"Estas son las credenciales y la configuración de tu proveedor de modelo de transcripción preferido. Es importante que estas claves estén actualizadas y sean correctas, de lo contrario, los archivos multimedia y el audio no se transcribirán.\",\n    provider: \"Proveedor de transcripción\",\n    \"warn-start\":\n      \"El uso del modelo local de Whisper en máquinas con RAM o CPU limitadas puede detener AnythingLLM al procesar archivos multimedia.\",\n    \"warn-recommend\":\n      \"Recomendamos al menos 2 GB de RAM y subir archivos de menos de 10 MB.\",\n    \"warn-end\":\n      \"El modelo integrado se descargará automáticamente en el primer uso.\",\n  },\n  embedding: {\n    title: \"Preferencia de incrustación\",\n    \"desc-start\":\n      \"Cuando se utiliza un LLM que no admite de forma nativa un motor de incrustación, es posible que debas especificar credenciales adicionales para la incrustación de texto.\",\n    \"desc-end\":\n      \"La incrustación es el proceso de convertir texto en vectores. Estas credenciales son necesarias para convertir tus archivos y prompts en un formato que AnythingLLM pueda usar para procesar.\",\n    provider: {\n      title: \"Proveedor de incrustación\",\n    },\n  },\n  text: {\n    title: \"Preferencias de división de texto y fragmentación\",\n    \"desc-start\":\n      \"A veces, es posible que desees cambiar la forma predeterminada en que se dividen y fragmentan los nuevos documentos antes de insertarlos en tu base de datos vectorial.\",\n    \"desc-end\":\n      \"Solo debes modificar esta configuración si comprendes cómo funciona la división de texto y sus efectos secundarios.\",\n    size: {\n      title: \"Tamaño del fragmento de texto\",\n      description:\n        \"Esta es la longitud máxima de caracteres que puede estar presente en un solo vector.\",\n      recommend: \"La longitud máxima del modelo de incrustación es\",\n    },\n    overlap: {\n      title: \"Superposición de fragmentos de texto\",\n      description:\n        \"Esta es la superposición máxima de caracteres que se produce durante la fragmentación entre dos fragmentos de texto adyacentes.\",\n    },\n  },\n  vector: {\n    title: \"Base de datos vectorial\",\n    description:\n      \"Estas son las credenciales y la configuración de cómo funcionará tu instancia de AnythingLLM. Es importante que estas claves estén actualizadas y sean correctas.\",\n    provider: {\n      title: \"Proveedor de base de datos vectorial\",\n      description: \"No se necesita configuración para LanceDB.\",\n    },\n  },\n  embeddable: {\n    title: \"Widgets de chat incrustables\",\n    description:\n      \"Los widgets de chat incrustables son interfaces de chat de cara al público que están vinculadas a un único espacio de trabajo. Estos te permiten crear espacios de trabajo que luego puedes publicar para todo el mundo.\",\n    create: \"Crear incrustación\",\n    table: {\n      workspace: \"Espacio de trabajo\",\n      chats: \"Chats enviados\",\n      active: \"Dominios activos\",\n      created: \"Creado\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Historial de chat incrustado\",\n    export: \"Exportar\",\n    description:\n      \"Estos son todos los chats y mensajes grabados de cualquier incrustación que hayas publicado.\",\n    table: {\n      embed: \"Incrustación\",\n      sender: \"Remitente\",\n      message: \"Mensaje\",\n      response: \"Respuesta\",\n      at: \"Enviado el\",\n    },\n  },\n  event: {\n    title: \"Registros de eventos\",\n    description:\n      \"Ve todas las acciones y eventos que ocurren en esta instancia para su supervisión.\",\n    clear: \"Borrar registros de eventos\",\n    table: {\n      type: \"Tipo de evento\",\n      user: \"Usuario\",\n      occurred: \"Ocurrido el\",\n    },\n  },\n  privacy: {\n    title: \"Privacidad y manejo de datos\",\n    description:\n      \"Esta es tu configuración sobre cómo los proveedores de terceros conectados y AnythingLLM manejan tus datos.\",\n    anonymous: \"Telemetría anónima habilitada\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Buscar conectores de datos\",\n    \"no-connectors\": \"No se encontraron conectores de datos.\",\n    obsidian: {\n      vault_location: \"Ubicación del vault\",\n      vault_description:\n        \"Selecciona la carpeta de tu vault de Obsidian para importar todas las notas y sus conexiones.\",\n      selected_files: \"Se encontraron {{count}} archivos markdown\",\n      importing: \"Importando vault...\",\n      import_vault: \"Importar vault\",\n      processing_time:\n        \"Esto puede llevar un tiempo dependiendo del tamaño de tu vault.\",\n      vault_warning:\n        \"Para evitar conflictos, asegúrate de que tu vault de Obsidian no esté abierto actualmente.\",\n    },\n    github: {\n      name: \"Repositorio de GitHub\",\n      description:\n        \"Importa un repositorio completo de GitHub, público o privado, con un solo clic.\",\n      URL: \"URL del repositorio de GitHub\",\n      URL_explained: \"URL del repositorio de GitHub que deseas recopilar.\",\n      token: \"Token de acceso de GitHub\",\n      optional: \"opcional\",\n      token_explained:\n        \"Token de acceso para evitar la limitación de velocidad.\",\n      token_explained_start: \"Sin un \",\n      token_explained_link1: \"Token de acceso personal\",\n      token_explained_middle:\n        \", la API de GitHub puede limitar el número de archivos que se pueden recopilar debido a los límites de velocidad. Puedes \",\n      token_explained_link2: \"crear un token de acceso temporal\",\n      token_explained_end: \" para evitar este problema.\",\n      ignores: \"Archivos ignorados\",\n      git_ignore:\n        \"Lista en formato .gitignore para ignorar archivos específicos durante la recopilación. Presiona Intro después de cada entrada que quieras guardar.\",\n      task_explained:\n        \"Una vez completado, todos los archivos estarán disponibles para incrustar en los espacios de trabajo en el selector de documentos.\",\n      branch: \"Rama de la que deseas recopilar archivos.\",\n      branch_loading: \"-- cargando ramas disponibles --\",\n      branch_explained: \"Rama de la que deseas recopilar archivos.\",\n      token_information:\n        \"Sin completar el <b>Token de acceso de GitHub</b>, este conector de datos solo podrá recopilar los archivos de <b>nivel superior</b> del repositorio debido a los límites de velocidad de la API pública de GitHub.\",\n      token_personal:\n        \"Obtén un token de acceso personal gratuito con una cuenta de GitHub aquí.\",\n    },\n    gitlab: {\n      name: \"Repositorio de GitLab\",\n      description:\n        \"Importa un repositorio completo de GitLab, público o privado, con un solo clic.\",\n      URL: \"URL del repositorio de GitLab\",\n      URL_explained: \"URL del repositorio de GitLab que deseas recopilar.\",\n      token: \"Token de acceso de GitLab\",\n      optional: \"opcional\",\n      token_description:\n        \"Selecciona entidades adicionales para obtener de la API de GitLab.\",\n      token_explained_start: \"Sin un \",\n      token_explained_link1: \"Token de acceso personal\",\n      token_explained_middle:\n        \", la API de GitLab puede limitar el número de archivos que se pueden recopilar debido a los límites de velocidad. Puedes \",\n      token_explained_link2: \"crear un token de acceso temporal\",\n      token_explained_end: \" para evitar este problema.\",\n      fetch_issues: \"Obtener issues como documentos\",\n      ignores: \"Archivos ignorados\",\n      git_ignore:\n        \"Lista en formato .gitignore para ignorar archivos específicos durante la recopilación. Presiona Intro después de cada entrada que quieras guardar.\",\n      task_explained:\n        \"Una vez completado, todos los archivos estarán disponibles para incrustar en los espacios de trabajo en el selector de documentos.\",\n      branch: \"Rama de la que deseas recopilar archivos\",\n      branch_loading: \"-- cargando ramas disponibles --\",\n      branch_explained: \"Rama de la que deseas recopilar archivos.\",\n      token_information:\n        \"Sin completar el <b>Token de acceso de GitLab</b>, este conector de datos solo podrá recopilar los archivos de <b>nivel superior</b> del repositorio debido a los límites de velocidad de la API pública de GitLab.\",\n      token_personal:\n        \"Obtén un token de acceso personal gratuito con una cuenta de GitLab aquí.\",\n    },\n    youtube: {\n      name: \"Transcripción de YouTube\",\n      description:\n        \"Importa la transcripción de un vídeo completo de YouTube desde un enlace.\",\n      URL: \"URL del vídeo de YouTube\",\n      URL_explained_start:\n        \"Introduce la URL de cualquier vídeo de YouTube para obtener su transcripción. El vídeo debe tener \",\n      URL_explained_link: \"subtítulos\",\n      URL_explained_end: \" disponibles.\",\n      task_explained:\n        \"Una vez completada, la transcripción estará disponible para incrustar en los espacios de trabajo en el selector de documentos.\",\n    },\n    \"website-depth\": {\n      name: \"Extractor de enlaces en masa\",\n      description:\n        \"Extrae un sitio web y sus subenlaces hasta una cierta profundidad.\",\n      URL: \"URL del sitio web\",\n      URL_explained: \"URL del sitio web que deseas extraer.\",\n      depth: \"Profundidad de rastreo\",\n      depth_explained:\n        \"Este es el número de enlaces secundarios que el trabajador debe seguir desde la URL de origen.\",\n      max_pages: \"Páginas máximas\",\n      max_pages_explained: \"Número máximo de enlaces a extraer.\",\n      task_explained:\n        \"Una vez completado, todo el contenido extraído estará disponible para incrustar en los espacios de trabajo en el selector de documentos.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description:\n        \"Importa una página completa de Confluence con un solo clic.\",\n      deployment_type: \"Tipo de implementación de Confluence\",\n      deployment_type_explained:\n        \"Determina si tu instancia de Confluence está alojada en la nube de Atlassian o es autohospedada.\",\n      base_url: \"URL base de Confluence\",\n      base_url_explained: \"Esta es la URL base de tu espacio de Confluence.\",\n      space_key: \"Clave del espacio de Confluence\",\n      space_key_explained:\n        \"Esta es la clave de los espacios de tu instancia de Confluence que se utilizará. Generalmente comienza con ~\",\n      username: \"Nombre de usuario de Confluence\",\n      username_explained: \"Tu nombre de usuario de Confluence\",\n      auth_type: \"Tipo de autenticación de Confluence\",\n      auth_type_explained:\n        \"Selecciona el tipo de autenticación que deseas usar para acceder a tus páginas de Confluence.\",\n      auth_type_username: \"Nombre de usuario y token de acceso\",\n      auth_type_personal: \"Token de acceso personal\",\n      token: \"Token de acceso de Confluence\",\n      token_explained_start:\n        \"Necesitas proporcionar un token de acceso para la autenticación. Puedes generar un token de acceso\",\n      token_explained_link: \"aquí\",\n      token_desc: \"Token de acceso para la autenticación\",\n      pat_token: \"Token de acceso personal de Confluence\",\n      pat_token_explained: \"Tu token de acceso personal de Confluence.\",\n      task_explained:\n        \"Una vez completado, el contenido de la página estará disponible para incrustar en los espacios de trabajo en el selector de documentos.\",\n      bypass_ssl: \"Omitir la validación del certificado SSL\",\n      bypass_ssl_explained:\n        \"Habilite esta opción para omitir la validación del certificado SSL para instancias de Confluence autohospedadas con certificados auto-firmados.\",\n    },\n    manage: {\n      documents: \"Documentos\",\n      \"data-connectors\": \"Conectores de datos\",\n      \"desktop-only\":\n        \"La edición de esta configuración solo está disponible en un dispositivo de escritorio. Accede a esta página en tu escritorio para continuar.\",\n      dismiss: \"Descartar\",\n      editing: \"Editando\",\n    },\n    directory: {\n      \"my-documents\": \"Mis documentos\",\n      \"new-folder\": \"Nueva carpeta\",\n      \"search-document\": \"Buscar documento\",\n      \"no-documents\": \"Sin documentos\",\n      \"move-workspace\": \"Mover al espacio de trabajo\",\n      \"delete-confirmation\":\n        \"¿Estás seguro de que quieres eliminar estos archivos y carpetas?\\nEsto eliminará los archivos del sistema y los eliminará de cualquier espacio de trabajo existente automáticamente.\\nEsta acción no es reversible.\",\n      \"removing-message\":\n        \"Eliminando {{count}} documentos y {{folderCount}} carpetas. Por favor, espera.\",\n      \"move-success\": \"Se movieron {{count}} documentos con éxito.\",\n      no_docs: \"Sin documentos\",\n      select_all: \"Seleccionar todo\",\n      deselect_all: \"Deseleccionar todo\",\n      remove_selected: \"Eliminar seleccionados\",\n      costs: \"*Costo único por incrustaciones\",\n      save_embed: \"Guardar e incrustar\",\n      \"total-documents_one\": \"{{count}} documento\",\n      \"total-documents_other\": \"{{count}} documentos\",\n    },\n    upload: {\n      \"processor-offline\": \"Procesador de documentos no disponible\",\n      \"processor-offline-desc\":\n        \"No podemos subir tus archivos en este momento porque el procesador de documentos no está disponible. Por favor, inténtalo de nuevo más tarde.\",\n      \"click-upload\": \"Haz clic para subir o arrastra y suelta\",\n      \"file-types\":\n        \"¡soporta archivos de texto, csv, hojas de cálculo, archivos de audio y más!\",\n      \"or-submit-link\": \"o envía un enlace\",\n      \"placeholder-link\": \"https://ejemplo.com\",\n      fetching: \"Obteniendo...\",\n      \"fetch-website\": \"Obtener sitio web\",\n      \"privacy-notice\":\n        \"Estos archivos se subirán al procesador de documentos que se ejecuta en esta instancia de AnythingLLM. Estos archivos no se envían ni se comparten con terceros.\",\n    },\n    pinning: {\n      what_pinning: \"¿Qué es fijar documentos?\",\n      pin_explained_block1:\n        \"Cuando <b>fijas</b> un documento en AnythingLLM, inyectaremos todo el contenido del documento en tu ventana de prompt para que tu LLM lo comprenda por completo.\",\n      pin_explained_block2:\n        \"Esto funciona mejor con <b>modelos de gran contexto</b> o archivos pequeños que son críticos para su base de conocimientos.\",\n      pin_explained_block3:\n        \"Si no obtienes las respuestas que deseas de AnythingLLM por defecto, fijar es una excelente manera de obtener respuestas de mayor calidad con un clic.\",\n      accept: \"Ok, entendido\",\n    },\n    watching: {\n      what_watching: \"¿Qué hace observar un documento?\",\n      watch_explained_block1:\n        \"Cuando <b>observas</b> un documento en AnythingLLM, sincronizaremos <i>automáticamente</i> el contenido de tu documento desde su fuente original a intervalos regulares. Esto actualizará automáticamente el contenido en cada espacio de trabajo donde se gestione este archivo.\",\n      watch_explained_block2:\n        \"Esta función actualmente admite contenido en línea y no estará disponible para documentos subidos manualmente.\",\n      watch_explained_block3_start:\n        \"Puedes administrar qué documentos se observan desde la vista de administrador del \",\n      watch_explained_block3_link: \"Administrador de archivos\",\n      watch_explained_block3_end: \".\",\n      accept: \"Ok, entendido\",\n    },\n  },\n  chat_window: {\n    attachments_processing:\n      \"Los archivos adjuntos se están procesando. Por favor, espera...\",\n    send_message: \"Enviar un mensaje\",\n    attach_file: \"Adjuntar un archivo a este chat\",\n    text_size: \"Cambiar tamaño del texto.\",\n    microphone: \"Habla tu prompt.\",\n    send: \"Enviar mensaje de prompt al espacio de trabajo\",\n    tts_speak_message: \"Mensaje de voz TTS\",\n    copy: \"Copiar\",\n    regenerate: \"Regenerar\",\n    regenerate_response: \"Regenerar respuesta\",\n    good_response: \"Buena respuesta\",\n    more_actions: \"Más acciones\",\n    fork: \"Bifurcar\",\n    delete: \"Eliminar\",\n    cancel: \"Cancelar\",\n    edit_prompt: \"Editar prompt\",\n    edit_response: \"Editar respuesta\",\n    preset_reset_description:\n      \"Borra tu historial de chat y comienza un nuevo chat\",\n    add_new_preset: \" Agregar nuevo preajuste\",\n    command: \"Comando\",\n    your_command: \"tu-comando\",\n    placeholder_prompt:\n      \"Este es el contenido que se inyectará delante de tu prompt.\",\n    description: \"Descripción\",\n    placeholder_description: \"Responde con un poema sobre los LLMs.\",\n    save: \"Guardar\",\n    small: \"Pequeño\",\n    normal: \"Normal\",\n    large: \"Grande\",\n    workspace_llm_manager: {\n      search: \"Buscar proveedores de LLM\",\n      loading_workspace_settings:\n        \"Cargando la configuración del espacio de trabajo...\",\n      available_models: \"Modelos disponibles para {{provider}}\",\n      available_models_description:\n        \"Selecciona un modelo para usar en este espacio de trabajo.\",\n      save: \"Usar este modelo\",\n      saving:\n        \"Estableciendo el modelo como predeterminado del espacio de trabajo...\",\n      missing_credentials: \"¡A este proveedor le faltan credenciales!\",\n      missing_credentials_description:\n        \"Haz clic para configurar las credenciales\",\n    },\n    submit: \"Enviar\",\n    edit_info_user:\n      '\"Enviar\" regenera la respuesta de la IA. \"Guardar\" actualiza solo tu mensaje.',\n    edit_info_assistant:\n      \"Los cambios que realice se guardarán directamente en esta respuesta.\",\n    see_less: \"Ver menos\",\n    see_more: \"Ver más\",\n    tools: \"Herramientas\",\n    browse: \"Explorar\",\n    text_size_label: \"Tamaño del texto\",\n    select_model: \"Seleccionar modelo\",\n    sources: \"Fuentes\",\n    document: \"Documento\",\n    similarity_match: \"partido\",\n    source_count_one: \"{{count}} de referencia\",\n    source_count_other: \"{{count}} referencias\",\n    preset_exit_description: \"Detener la sesión actual del agente.\",\n    add_new: \"Añadir nuevo\",\n    edit: \"Editar\",\n    publish: \"Publicar\",\n    stop_generating: \"Dejar de generar respuestas\",\n    pause_tts_speech_message: \"Pausa la lectura de voz del mensaje.\",\n    slash_commands: \"Comandos abreviados\",\n    agent_skills: \"Habilidades del agente\",\n    manage_agent_skills: \"Gestionar las habilidades del agente.\",\n    agent_skills_disabled_in_session:\n      \"No es posible modificar las habilidades durante una sesión con un agente activo. Primero, utilice el comando `/exit` para finalizar la sesión.\",\n    start_agent_session: \"Iniciar sesión como agente\",\n    use_agent_session_to_use_tools:\n      \"Puede utilizar las herramientas disponibles en el chat iniciando una sesión con un agente utilizando el prefijo '@agent' al principio de su mensaje.\",\n  },\n  profile_settings: {\n    edit_account: \"Editar cuenta\",\n    profile_picture: \"Foto de perfil\",\n    remove_profile_picture: \"Eliminar foto de perfil\",\n    username: \"Nombre de usuario\",\n    new_password: \"Nueva contraseña\",\n    password_description: \"La contraseña debe tener al menos 8 caracteres\",\n    cancel: \"Cancelar\",\n    update_account: \"Actualizar cuenta\",\n    theme: \"Preferencia de tema\",\n    language: \"Idioma preferido\",\n    failed_upload: \"Error al subir la foto de perfil: {{error}}\",\n    upload_success: \"Foto de perfil subida.\",\n    failed_remove: \"Error al eliminar la foto de perfil: {{error}}\",\n    profile_updated: \"Perfil actualizado.\",\n    failed_update_user: \"Error al actualizar el usuario: {{error}}\",\n    account: \"Cuenta\",\n    support: \"Soporte\",\n    signout: \"Cerrar sesión\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Atajos de teclado\",\n    shortcuts: {\n      settings: \"Abrir configuración\",\n      workspaceSettings: \"Abrir configuración del espacio de trabajo actual\",\n      home: \"Ir a Inicio\",\n      workspaces: \"Administrar espacios de trabajo\",\n      apiKeys: \"Configuración de claves de API\",\n      llmPreferences: \"Preferencias de LLM\",\n      chatSettings: \"Configuración del chat\",\n      help: \"Mostrar ayuda de atajos de teclado\",\n      showLLMSelector: \"Mostrar selector de LLM del espacio de trabajo\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"¡Éxito!\",\n        success_description:\n          \"¡Tu prompt del sistema ha sido publicado en el Centro de la Comunidad!\",\n        success_thank_you: \"¡Gracias por compartir con la Comunidad!\",\n        view_on_hub: \"Ver en el Centro de la Comunidad\",\n        modal_title: \"Publicar prompt del sistema\",\n        name_label: \"Nombre\",\n        name_description:\n          \"Este es el nombre para mostrar de tu prompt del sistema.\",\n        name_placeholder: \"Mi prompt del sistema\",\n        description_label: \"Descripción\",\n        description_description:\n          \"Esta es la descripción de tu prompt del sistema. Úsala para describir el propósito de tu prompt del sistema.\",\n        tags_label: \"Etiquetas\",\n        tags_description:\n          \"Las etiquetas se utilizan para identificar tu prompt del sistema para una búsqueda más fácil. Puedes agregar varias etiquetas. Máximo 5 etiquetas. Máximo 20 caracteres por etiqueta.\",\n        tags_placeholder: \"Escribe y presiona Enter para agregar etiquetas\",\n        visibility_label: \"Visibilidad\",\n        public_description:\n          \"Los prompts del sistema públicos son visibles para todos.\",\n        private_description:\n          \"Los prompts del sistema privados solo son visibles para ti.\",\n        publish_button: \"Publicar en el Centro de la Comunidad\",\n        submitting: \"Publicando...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Este es el prompt del sistema real que se utilizará para guiar al LLM.\",\n        prompt_placeholder: \"Ingresa tu prompt del sistema aquí...\",\n      },\n      agent_flow: {\n        success_title: \"¡Éxito!\",\n        success_description:\n          \"¡Tu flujo de agente ha sido publicado en el Centro de la Comunidad!\",\n        success_thank_you: \"¡Gracias por compartir con la Comunidad!\",\n        view_on_hub: \"Ver en el Centro de la Comunidad\",\n        modal_title: \"Publicar flujo de agente\",\n        name_label: \"Nombre\",\n        name_description:\n          \"Este es el nombre para mostrar de tu flujo de agente.\",\n        name_placeholder: \"Mi flujo de agente\",\n        description_label: \"Descripción\",\n        description_description:\n          \"Esta es la descripción de tu flujo de agente. Úsala para describir el propósito de tu flujo de agente.\",\n        tags_label: \"Etiquetas\",\n        tags_description:\n          \"Las etiquetas se utilizan para identificar tu flujo de agente para una búsqueda más fácil. Puedes agregar varias etiquetas. Máximo 5 etiquetas. Máximo 20 caracteres por etiqueta.\",\n        tags_placeholder: \"Escribe y presiona Enter para agregar etiquetas\",\n        visibility_label: \"Visibilidad\",\n        submitting: \"Publicando...\",\n        submit: \"Publicar en el Centro de la Comunidad\",\n        privacy_note:\n          \"Los flujos de agente siempre se suben como privados para proteger cualquier dato sensible. Puedes cambiar la visibilidad en el Centro de la Comunidad después de publicar. Por favor, verifica que tu flujo no contenga ninguna información sensible o privada antes de publicar.\",\n      },\n      slash_command: {\n        success_title: \"¡Éxito!\",\n        success_description:\n          \"¡Tu comando de barra ha sido publicado en el Centro de la Comunidad!\",\n        success_thank_you: \"¡Gracias por compartir con la Comunidad!\",\n        view_on_hub: \"Ver en el Centro de la Comunidad\",\n        modal_title: \"Publicar comando de barra\",\n        name_label: \"Nombre\",\n        name_description:\n          \"Este es el nombre para mostrar de tu comando de barra.\",\n        name_placeholder: \"Mi comando de barra\",\n        description_label: \"Descripción\",\n        description_description:\n          \"Esta es la descripción de tu comando de barra. Úsala para describir el propósito de tu comando de barra.\",\n        tags_label: \"Etiquetas\",\n        tags_description:\n          \"Las etiquetas se utilizan para identificar tu comando de barra para una búsqueda más fácil. Puedes agregar varias etiquetas. Máximo 5 etiquetas. Máximo 20 caracteres por etiqueta.\",\n        tags_placeholder: \"Escribe y presiona Enter para agregar etiquetas\",\n        visibility_label: \"Visibilidad\",\n        public_description:\n          \"Los comandos de barra públicos son visibles para todos.\",\n        private_description:\n          \"Los comandos de barra privados solo son visibles para ti.\",\n        publish_button: \"Publicar en el Centro de la Comunidad\",\n        submitting: \"Publicando...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Este es el prompt que se utilizará cuando se active el comando de barra.\",\n        prompt_placeholder: \"Ingresa tu prompt aquí...\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Se requiere autenticación\",\n          description:\n            \"Necesitas autenticarte con el Centro de la Comunidad de AnythingLLM antes de publicar elementos.\",\n          button: \"Conectar al Centro de la Comunidad\",\n        },\n      },\n    },\n  },\n  security: {\n    title: \"Seguridad\",\n    multiuser: {\n      title: \"Modo multiusuario\",\n      description:\n        \"Configura tu instancia para que sea compatible con tu equipo activando el modo multiusuario.\",\n      enable: {\n        \"is-enable\": \"El modo multiusuario está habilitado\",\n        enable: \"Habilitar modo multiusuario\",\n        description:\n          \"Por defecto, serás el único administrador. Como administrador, deberás crear cuentas para todos los nuevos usuarios o administradores. No pierdas tu contraseña, ya que solo un usuario administrador puede restablecer las contraseñas.\",\n        username: \"Nombre de usuario de la cuenta de administrador\",\n        password: \"Contraseña de la cuenta de administrador\",\n      },\n    },\n    password: {\n      title: \"Protección con contraseña\",\n      description:\n        \"Protege tu instancia de AnythingLLM con una contraseña. Si la olvidas, no hay método de recuperación, así que asegúrate de guardar esta contraseña.\",\n      \"password-label\": \"Contraseña de la instancia\",\n    },\n  },\n  home: {\n    welcome: \"Bienvenido\",\n    chooseWorkspace: \"Elige un espacio de trabajo para comenzar a chatear!\",\n    notAssigned:\n      \"Actualmente no estás asignado a ningún espacio de trabajo.\\nPor favor, contacta a tu administrador para solicitar acceso a un espacio de trabajo.\",\n    goToWorkspace: 'Ir a \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/et/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"Tere tulemast\",\n      getStarted: \"Alusta\",\n    },\n    llm: {\n      title: \"LLM-i eelistus\",\n      description:\n        \"AnythingLLM töötab paljude LLM-teenusepakkujatega. See teenus haldab vestlust.\",\n    },\n    userSetup: {\n      title: \"Kasutaja seadistus\",\n      description: \"Seadista oma kasutajasätted.\",\n      howManyUsers: \"Mitu kasutajat seda instantsi kasutab?\",\n      justMe: \"Ainult mina\",\n      myTeam: \"Minu meeskond\",\n      instancePassword: \"Instantsi parool\",\n      setPassword: \"Kas soovid parooli seadistada?\",\n      passwordReq: \"Parool peab olema vähemalt 8 märki.\",\n      passwordWarn:\n        \"Salvesta see parool hoolikalt, sest taastamisvõimalust ei ole.\",\n      adminUsername: \"Admini kasutajanimi\",\n      adminPassword: \"Admini parool\",\n      adminPasswordReq: \"Parool peab olema vähemalt 8 märki.\",\n      teamHint:\n        \"Vaikimisi oled ainus administraator. Pärast häälestust saad luua ning kutsuda teisi kasutajaid või administreerida neid. Parooli kaotamisel saab paroole lähtestada vaid administraator.\",\n    },\n    data: {\n      title: \"Andmetöötlus ja privaatsus\",\n      description:\n        \"Oleme pühendunud läbipaistvusele ning kontrollile sinu andmete osas.\",\n      settingsHint: \"Neid sätteid saab igal ajal seadetes muuta.\",\n    },\n    survey: {\n      title: \"Tere tulemast AnythingLLM-i\",\n      description:\n        \"Aita meil AnythingLLM sinu vajadustele vastavaks kujundada. Valikuline.\",\n      email: \"Mis on su e-post?\",\n      useCase: \"Milleks kasutad AnythingLLM-i?\",\n      useCaseWork: \"Töö jaoks\",\n      useCasePersonal: \"Isiklikuks kasutuseks\",\n      useCaseOther: \"Muu\",\n      comment: \"Kust kuulsid AnythingLLM-ist?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube jne – anna meile teada!\",\n      skip: \"Jäta vahele\",\n      thankYou: \"Aitäh tagasiside eest!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Tööruumide nimi\",\n    user: \"Kasutaja\",\n    selection: \"Mudeli valik\",\n    saving: \"Salvestan…\",\n    save: \"Salvesta muudatused\",\n    previous: \"Eelmine leht\",\n    next: \"Järgmine leht\",\n    optional: \"Valikuline\",\n    yes: \"Jah\",\n    no: \"Ei\",\n    search: \"otsing\",\n    username_requirements:\n      \"Kasutajanimi peab olema 2–32 tähemärki, algama väiketähega ning sisaldama ainult väiketähti, numbreid, alakriipse, sidekriipse ja punkte.\",\n    on: \"On\",\n    none: \"Ei\",\n    stopped: \"Peatas\",\n    loading: \"Laadimine\",\n    refresh: \"Värskendada\",\n  },\n  settings: {\n    title: \"Instantsi seaded\",\n    invites: \"Kutsed\",\n    users: \"Kasutajad\",\n    workspaces: \"Tööruumid\",\n    \"workspace-chats\": \"Tööruumi vestlused\",\n    customization: \"Kohandamine\",\n    interface: \"Kasutajaliidese eelistused\",\n    branding: \"Bränding ja valgesildistamine\",\n    chat: \"Vestlus\",\n    \"api-keys\": \"Arendaja API\",\n    llm: \"LLM\",\n    transcription: \"Transkriptsioon\",\n    embedder: \"Embeddija\",\n    \"text-splitting\": \"Teksti lõikamine ja tükeldus\",\n    \"voice-speech\": \"Hääle ja kõne seaded\",\n    \"vector-database\": \"Vektoriandmebaas\",\n    embeds: \"Vestluse embed\",\n    security: \"Turvalisus\",\n    \"event-logs\": \"Sündmuste logid\",\n    privacy: \"Privaatsus ja andmed\",\n    \"ai-providers\": \"AI-pakkujad\",\n    \"agent-skills\": \"Agendi oskused\",\n    admin: \"Admin\",\n    tools: \"Tööriistad\",\n    \"system-prompt-variables\": \"Süsteemprompti muutujad\",\n    \"experimental-features\": \"Eksperimentaalsed funktsioonid\",\n    contact: \"Tugi\",\n    \"browser-extension\": \"Brauserilaiend\",\n    \"mobile-app\": \"AnythingLLM mobiilversioon\",\n    \"community-hub\": {\n      title: \"Kogukonna keskpunkt\",\n      trending: \"Avasta populaarseid\",\n      \"your-account\": \"Teie konto\",\n      \"import-item\": \"Importeeritud toode\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Tere tulemast\",\n      \"placeholder-username\": \"Kasutajanimi\",\n      \"placeholder-password\": \"Parool\",\n      login: \"Logi sisse\",\n      validating: \"Kontrollin…\",\n      \"forgot-pass\": \"Unustasid parooli\",\n      reset: \"Lähtesta\",\n    },\n    \"sign-in\": \"Logi sisse oma {{appName}} kontosse.\",\n    \"password-reset\": {\n      title: \"Parooli lähtestamine\",\n      description: \"Sisesta all vajalik info, et parool lähtestada.\",\n      \"recovery-codes\": \"Taastamiskoodid\",\n      \"back-to-login\": \"Tagasi sisselogimisele\",\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Loo agent\",\n      editWorkspace: \"Redige tööruum\",\n      uploadDocument: \"Lae fail üles\",\n    },\n    greeting: \"Kuidas saan teid täna aidata?\",\n  },\n  \"new-workspace\": {\n    title: \"Uus tööruum\",\n    placeholder: \"Minu tööruum\",\n  },\n  \"workspaces—settings\": {\n    general: \"Üldseaded\",\n    chat: \"Vestluse seaded\",\n    vector: \"Vektoriandmebaas\",\n    members: \"Liikmed\",\n    agent: \"Agendi konfiguratsioon\",\n  },\n  general: {\n    vector: {\n      title: \"Vektorite arv\",\n      description: \"Vektorite koguarv sinu vektoriandmebaasis.\",\n    },\n    names: {\n      description: \"See muudab ainult tööruumi kuvatavat nime.\",\n    },\n    message: {\n      title: \"Soovitatud vestlussõnumid\",\n      description: \"Kohanda sõnumeid, mida tööruumi kasutajatele soovitatakse.\",\n      add: \"Lisa uus sõnum\",\n      save: \"Salvesta sõnumid\",\n      heading: \"Selgita mulle\",\n      body: \"AnythingLLM eeliseid\",\n    },\n    delete: {\n      title: \"Kustuta tööruum\",\n      description:\n        \"Kustuta see tööruum ja kõik selle andmed. See eemaldab tööruumi kõikidele kasutajatele.\",\n      delete: \"Kustuta tööruum\",\n      deleting: \"Kustutan tööruumi…\",\n      \"confirm-start\": \"Oled kustutamas kogu\",\n      \"confirm-end\":\n        \"tööruumi. See eemaldab kõik vektorid vektoriandmebaasist.\\n\\nAlgseid faile ei puudutata. Tegevust ei saa tagasi võtta.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Tööruumi LLM-pakkuja\",\n      description:\n        \"LLM-pakkuja ja mudel, mida selles tööruumis kasutatakse. Vaikimisi kasutatakse süsteemi LLM-seadeid.\",\n      search: \"Otsi LLM-pakkujaid\",\n    },\n    model: {\n      title: \"Tööruumi vestlusmudel\",\n      description:\n        \"Vestlusmudel, mida tööruumis kasutatakse. Kui tühi, kasutatakse süsteemi LLM-eelistust.\",\n    },\n    mode: {\n      title: \"Vestlusrežiim\",\n      chat: {\n        title: \"Vestlus\",\n        description:\n          'teenab vastuseid, kasutades LLM-i üldist teadmist ja dokumentide konteksti, mida on leitav.<br /> Selleks peate kasutama käsku \"@agent\".',\n      },\n      query: {\n        title: \"Päring\",\n        description:\n          'teenib vastuseid <b>ainult__, kui dokumendi kontekst on leitud.</b> Vajate kasutama käesu \"agent\", et kasutada tööriime.',\n      },\n      automatic: {\n        title: \"Automaailm\",\n        description:\n          'kasutab automaatselt tööriistu, kui mudel ja pakkuja toetavad native tööriistade kasutamist. <br />Kui native tööriistade kasutamine pole toetatud, peate kasutama käsku \"@agent\", et tööriiste kasutada.',\n      },\n    },\n    history: {\n      title: \"Vestlusajalugu\",\n      \"desc-start\": \"Eelmiste sõnumite arv, mis kaasatakse vastuse lühimällu.\",\n      recommend: \"Soovitatav 20. \",\n      \"desc-end\": \"Üle 45 võib sõltuvalt sõnumi suurusest põhjustada tõrkeid.\",\n    },\n    prompt: {\n      title: \"Süsteemprompt\",\n      description:\n        \"Prompt, mida tööruumis kasutatakse. Määra kontekst ja juhised, et AI toodaks asjakohase vastuse.\",\n      history: {\n        title: \"Süsteempromptide ajalugu\",\n        clearAll: \"Tühjenda kõik\",\n        noHistory: \"Ajalugu puudub\",\n        restore: \"Taasta\",\n        delete: \"Kustuta\",\n        publish: \"Avalda kogukonnas\",\n        deleteConfirm: \"Kas oled kindel, et soovid selle kirje kustutada?\",\n        clearAllConfirm:\n          \"Kas oled kindel, et soovid kogu ajaloo tühjendada? Tegevus on pöördumatu.\",\n        expand: \"Laienda\",\n      },\n    },\n    refusal: {\n      title: \"Päringurežiimi keeldumisteade\",\n      \"desc-start\": \"Kui ollakse\",\n      query: \"päringu\",\n      \"desc-end\":\n        \"režiimis, võib määrata kohandatud vastuse, kui konteksti ei leita.\",\n      \"tooltip-title\": \"Miks ma seda näen?\",\n      \"tooltip-description\":\n        \"Olete küsimise režiimis, mis kasutab ainult teie dokumentidest saadavat teavet. Valige vestlemise režiim, et pidada paindlikumaid vestlusi, või klõpsake siin, et külastada meie dokumentatsiooni ja saada lisateavet vestlemise režiimide kohta.\",\n    },\n    temperature: {\n      title: \"LLM-i temperatuur\",\n      \"desc-start\": 'Määrab, kui \"loovad\" vastused on.',\n      \"desc-end\":\n        \"Kõrgem väärtus = loovam, ent liiga kõrge võib tekitada ebaühtlasi vastuseid.\",\n      hint: \"Kontrolli pakkujalt lubatud vahemikke.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Vektoriandmebaasi identifikaator\",\n    snippets: {\n      title: \"Maksimaalne konteksti lõikude arv\",\n      description:\n        \"Maksimaalne lõikude arv, mis saadetakse LLM-ile ühe vestluse/päringu kohta.\",\n      recommend: \"Soovitatav: 4\",\n    },\n    doc: {\n      title: \"Dokumendi sarnasuse lävi\",\n      description:\n        \"Minimaalne sarnasusskoor, et allikas oleks vestlusega seotud. Mida kõrgem, seda sarnasem peab allikas olema.\",\n      zero: \"Piirang puudub\",\n      low: \"Madal (≥ 0,25)\",\n      medium: \"Keskmine (≥ 0,50)\",\n      high: \"Kõrge (≥ 0,75)\",\n    },\n    reset: {\n      reset: \"Lähtesta vektoriandmebaas\",\n      resetting: \"Puhastan vektoreid…\",\n      confirm:\n        \"Lähtestad selle tööruumi vektoriandmebaasi. Kõik vektorid eemaldatakse.\\n\\nAlgseid faile ei puudutata. Tegevus on pöördumatu.\",\n      error: \"Vektoriandmebaasi lähtestamine ebaõnnestus!\",\n      success: \"Vektoriandmebaas lähtestati!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"Mudelite, mis ei toeta tööriistakutsumisi, jõudlus sõltub tugevalt mudeli võimest. Mõned funktsioonid võivad olla piiratud.\",\n    provider: {\n      title: \"Tööruumi agendi LLM-pakkuja\",\n      description:\n        \"LLM-pakkuja ja mudel, mida kasutatakse @agent agendi jaoks.\",\n    },\n    mode: {\n      chat: {\n        title: \"Tööruumi agendi vestlusmudel\",\n        description: \"Vestlusmudel, mida @agent agendi jaoks kasutatakse.\",\n      },\n      title: \"Tööruumi agendi mudel\",\n      description: \"LLM-mudel, mida @agent agendi jaoks kasutatakse.\",\n      wait: \"-- laadib mudeleid --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG ja pikaajaline mälu\",\n        description:\n          'Lubab agendil kasutada kohalikke dokumente vastamiseks või \"meelde jätmiseks\".',\n      },\n      view: {\n        title: \"Vaata ja kokkuvõtlikusta dokumente\",\n        description:\n          \"Lubab agendil loetleda ja kokku võtta praegu põimitud faile.\",\n      },\n      scrape: {\n        title: \"Kraabi veebilehti\",\n        description: \"Lubab agendil külastada ja kraapida veebisisu.\",\n      },\n      generate: {\n        title: \"Loo diagramme\",\n        description: \"Lubab agendil luua erinevaid diagramme antud andmetest.\",\n      },\n      save: {\n        title: \"Loo ja salvesta faile brauserisse\",\n        description:\n          \"Lubab agendil luua faile, mis salvestatakse ja allalaaditakse brauseris.\",\n      },\n      web: {\n        title: \"Reaalajas veebihaku tugi\",\n        description:\n          \"Lisage oma esindajale võimalus veebis otsida, et vastata teie küsimustele, ühendades selle veebiotsingu (SERP) teenusega.\",\n      },\n      sql: {\n        title: \"SQL-i ühendus\",\n        description:\n          \"Tagage, et teie esindaja saaks kasutada SQL-i, et vastata teie küsimustele, ühendades erinevate SQL andmebaasiteenustega.\",\n      },\n      default_skill:\n        \"Vaikimisi on see funktsioon lubatud, kuid saate seda välja lülitada, kui ei soovi, et see oleks saadaval kaagentile.\",\n    },\n    mcp: {\n      title: \"MCP-serverid\",\n      \"loading-from-config\": \"MCP-serverid laaditakse konfiguraadifailist\",\n      \"learn-more\": \"Lisateabe saamiseks tutvuge MCP-serveridega.\",\n      \"no-servers-found\": \"MCP-servereid ei leitud.\",\n      \"tool-warning\":\n        \"Parima tulemuse saavutamiseks, võtke kaalutluseks, et välja lülitada tarbetud vahendid, et säilitada kontekst.\",\n      \"stop-server\": \"Lülitage MCP-server välja\",\n      \"start-server\": \"Alusta MCP-serverit\",\n      \"delete-server\": \"Kasuta MCP-serveri kustutamise funktsiooni\",\n      \"tool-count-warning\":\n        \"See MCP server on lubanud <b>_, mis tarbivad konteksti igas vestluses.</b> Selle asemel võid soovimatuid tööriistu välja lülitada, et säästa konteksti.\",\n      \"startup-command\": \"Alustamine\",\n      command: \"Juhendamine\",\n      arguments: \"Argumentid\",\n      \"not-running-warning\":\n        \"See MCP-server ei tööta – see võib olla peatatud või alguses võib tekkida viga.\",\n      \"tool-call-arguments\": '\"Tooli käivitamise argumentid\"',\n      \"tools-enabled\": \"vahendid on lubatud\",\n    },\n    settings: {\n      title: \"Agenti oskuste seaded\",\n      \"max-tool-calls\": {\n        title: \"Maximaalne töö-kõned vastuse kohta\",\n        description:\n          \"Максимаalne arv, mis agent võib ühendada, et genereerida ühe vastuse. See takistab liigse töö tegevuse ja lõpmatute ringide tekkimist.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Nutikad oskuste valiku meetodid\",\n        \"beta-badge\": \"Beeta\",\n        description:\n          \"Lubage piiramatu hulga tööriistade kasutamist ning vähendage küsimuse kohta kasutatavate tokenide arv kuni 80% – AnythingLLM valib automaatselt iga küsimuse jaoks sobivad oskused.\",\n        \"max-tools\": {\n          title: \"Max Tools\",\n          description:\n            \"Maksimaalne arv tööriistu, mida saab valida igale küsimusele. Soovitame seada see väärtus suuremate kontekstmudelite jaoks suuremaks.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Tööruumi vestlused\",\n    description:\n      \"Kõik salvestatud vestlused ja sõnumid kuvatakse loomise aja järgi.\",\n    export: \"Ekspordi\",\n    table: {\n      id: \"ID\",\n      by: \"Saatja\",\n      workspace: \"Tööruum\",\n      prompt: \"Päring\",\n      response: \"Vastus\",\n      at: \"Saadetud\",\n    },\n  },\n  customization: {\n    interface: {\n      title: \"Kasutajaliidese eelistused\",\n      description: \"Sea AnythingLLM-i UI eelistused.\",\n    },\n    branding: {\n      title: \"Bränding ja valgesildistamine\",\n      description: \"Valgesildista oma AnythingLLM kohandatud brändinguga.\",\n    },\n    chat: {\n      title: \"Vestlus\",\n      description: \"Sea vestluse eelistused.\",\n      auto_submit: {\n        title: \"Automaatselt esita kõnesisend\",\n        description: \"Saada kõnesisend ära pärast vaikuse perioodi\",\n      },\n      auto_speak: {\n        title: \"Loe vastused ette\",\n        description: \"AI loeb vastused automaatselt ette\",\n      },\n      spellcheck: {\n        title: \"Luba õigekirjakontroll\",\n        description: \"Lülita vestlusväljale õigekirjakontroll sisse/välja\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Teema\",\n        description: \"Vali rakenduse värviteema.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Kuva kerimisriba\",\n        description: \"Kuva või peida vestlusakna kerimisriba.\",\n      },\n      \"support-email\": {\n        title: \"Toe e-post\",\n        description:\n          \"Määra e-posti aadress, kuhu kasutajad saavad abi saamiseks pöörduda.\",\n      },\n      \"app-name\": {\n        title: \"Nimi\",\n        description:\n          \"Nimi, mis kuvatakse kõigile kasutajatele sisselogimislehel.\",\n      },\n      \"display-language\": {\n        title: \"Kuvakeel\",\n        description:\n          \"Vali keel, milles AnythingLLM UI kuvatakse (kui tõlge on olemas).\",\n      },\n      logo: {\n        title: \"Brändi logo\",\n        description: \"Laadi üles kohandatud logo, mis kuvatakse kõikjal.\",\n        add: \"Lisa logo\",\n        recommended: \"Soovituslik suurus: 800 × 200\",\n        remove: \"Eemalda\",\n        replace: \"Asenda\",\n      },\n      \"browser-appearance\": {\n        title: \"Brauseri välimus\",\n        description: \"Kohanda brauseri vahekaardi pealkirja ja ikooni.\",\n        tab: {\n          title: \"Pealkiri\",\n          description:\n            \"Sea kohandatud vahekaardi pealkiri, kui rakendus on avatud.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description: \"Kasuta kohandatud faviconi brauseri vahekaardil.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Külgriba jaluse elemendid\",\n        description: \"Kohanda külgriba allosas kuvatavaid linke/ikooni.\",\n        icon: \"Ikoon\",\n        link: \"Link\",\n      },\n      \"render-html\": {\n        title: \"Renderi HTML-koodi veebisaidil\",\n        description:\n          \"HTML-vastuste kuvamine abivasside vastustes.\\nSee võib viia suurema vastuste kvaliteedi, kuid võib ka põhjustada potentsiaalseid turvaohusid.\",\n      },\n    },\n  },\n  api: {\n    title: \"API võtmed\",\n    description:\n      \"API võtmed võimaldavad programmipõhiselt hallata seda AnythingLLM instantsi.\",\n    link: \"Loe API dokumentatsiooni\",\n    generate: \"Genereeri uus API võti\",\n    table: {\n      key: \"API võti\",\n      by: \"Loonud\",\n      created: \"Loodud\",\n    },\n  },\n  llm: {\n    title: \"LLM-i eelistus\",\n    description:\n      \"Siin on sinu valitud LLM-teenusepakkuja võtmed ja seaded. Need peavad olema õiged, vastasel juhul AnythingLLM ei tööta.\",\n    provider: \"LLM-pakkuja\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Azure teenuse lõpp-punkt\",\n        api_key: \"API võti\",\n        chat_deployment_name: \"Vestluse deploy nimi\",\n        chat_model_token_limit: \"Mudeli tokeni limiit\",\n        model_type: \"Mudeli tüüp\",\n        default: \"Vaikimisi\",\n        reasoning: \"Põhjendus\",\n        model_type_tooltip:\n          'Kui teie rakendus kasutab loogika mudelit (o1, o1-mini, o3-mini jne), siis määrake see väärtuseks \"Loogika\". Muu korral võivad teie vestlussõnumid ebaõiglas.',\n      },\n    },\n  },\n  transcription: {\n    title: \"Transkriptsiooni mudeli eelistus\",\n    description:\n      \"Siin on sinu valitud transkriptsioonimudeli pakkuja seaded. Vale seadistuse korral heli- ja meediafaile ei transkribeerita.\",\n    provider: \"Transkriptsiooni pakkuja\",\n    \"warn-start\":\n      \"Kohaliku Whisper-mudeli kasutamine vähese RAM-i või CPU-ga masinal võib faili töötlemise ummistada.\",\n    \"warn-recommend\": \"Soovitame vähemalt 2 GB RAM-i ning <10 MB faile.\",\n    \"warn-end\":\n      \"Sisseehitatud mudel laaditakse alla esmakasutusel automaatselt.\",\n  },\n  embedding: {\n    title: \"Embedding-i eelistus\",\n    \"desc-start\":\n      \"Kui kasutad LLM-i, mis ei sisalda embedding-mootorit, tuleb määrata täiendavad võtmed.\",\n    \"desc-end\":\n      \"Embedding muudab teksti vektoriteks. Need võtmed on vajalikud, et AnythingLLM saaks sinu failid ja päringud töödelda.\",\n    provider: {\n      title: \"Embedding-i pakkuja\",\n    },\n  },\n  text: {\n    title: \"Teksti lõikamise ja tükeldamise seaded\",\n    \"desc-start\":\n      \"Vahel soovid muuta, kuidas uued dokumendid enne vektoriandmebaasi lisamist tükeldatakse.\",\n    \"desc-end\": \"Muuda seda ainult siis, kui mõistad tekstilõike mõju.\",\n    size: {\n      title: \"Tekstitüki suurus\",\n      description: \"Maksimaalne märgipikkus ühes vektoris.\",\n      recommend: \"Embed-mudeli maks pikkus on\",\n    },\n    overlap: {\n      title: \"Tekstitüki kattuvus\",\n      description: \"Maksimaalne märkide kattuvus kahe kõrvuti tüki vahel.\",\n    },\n  },\n  vector: {\n    title: \"Vektoriandmebaas\",\n    description:\n      \"Siin on seaded, kuidas AnythingLLM töötab. Vale seadistus võib põhjustada tõrkeid.\",\n    provider: {\n      title: \"Vektoriandmebaasi pakkuja\",\n      description: \"LanceDB puhul seadistust pole vaja.\",\n    },\n  },\n  embeddable: {\n    title: \"Embed-vestlusvidinad\",\n    description:\n      \"Avalikkusele suunatud vestlusliidesed, mis on seotud ühe tööruumiga.\",\n    create: \"Loo embed\",\n    table: {\n      workspace: \"Tööruum\",\n      chats: \"Saadetud vestlused\",\n      active: \"Aktiivsed domeenid\",\n      created: \"Loodud\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Embed-vestluste ajalugu\",\n    export: \"Ekspordi\",\n    description: \"Kõik embed-ide salvestatud vestlused ja sõnumid.\",\n    table: {\n      embed: \"Embed\",\n      sender: \"Saatja\",\n      message: \"Sõnum\",\n      response: \"Vastus\",\n      at: \"Saadetud\",\n    },\n  },\n  event: {\n    title: \"Sündmuste logid\",\n    description: \"Vaata instantsis toimuvaid tegevusi ja jälgi sündmusi.\",\n    clear: \"Tühjenda logid\",\n    table: {\n      type: \"Sündmuse tüüp\",\n      user: \"Kasutaja\",\n      occurred: \"Toimus\",\n    },\n  },\n  privacy: {\n    title: \"Privaatsus ja andmetöötlus\",\n    description:\n      \"Konfiguratsioon kolmandate osapoolte ja AnythingLLM-i andmekäitluse kohta.\",\n    anonymous: \"Anonüümne telemeetria lubatud\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Otsi andmepistikuid\",\n    \"no-connectors\": \"Andmepistikuid ei leitud.\",\n    obsidian: {\n      vault_location: \"Vaulti asukoht\",\n      vault_description:\n        \"Vali oma Obsidiani vaulti kaust, et importida kõik märkmed ja nende seosed.\",\n      selected_files: \"Leiti {{count}} Markdown-faili\",\n      importing: \"Vaulti importimine…\",\n      import_vault: \"Impordi vault\",\n      processing_time: \"See võib võtta aega sõltuvalt vaulti suurusest.\",\n      vault_warning:\n        \"Konfliktide vältimiseks veendu, et Obsidiani vault ei oleks praegu avatud.\",\n    },\n    github: {\n      name: \"GitHubi repo\",\n      description:\n        \"Impordi kogu avalik või privaatne GitHubi repo ühe klõpsuga.\",\n      URL: \"GitHubi repo URL\",\n      URL_explained: \"Repo URL, mida soovid koguda.\",\n      token: \"GitHubi juurdepääsuvõti\",\n      optional: \"valikuline\",\n      token_explained: \"Võti API piirangute vältimiseks.\",\n      token_explained_start: \"Ilma \",\n      token_explained_link1: \"isikliku juurdepääsuvõtmeta\",\n      token_explained_middle:\n        \" võib GitHubi API piirata kogutavate failide hulka. Sa võid \",\n      token_explained_link2: \"luua ajutise juurdepääsuvõtme\",\n      token_explained_end: \" selle vältimiseks.\",\n      ignores: \"Faili välistused\",\n      git_ignore:\n        \".gitignore formaadis nimekiri failidest, mida kogumisel ignoreerida. Vajuta Enter iga kirje järel.\",\n      task_explained:\n        \"Kui valmis, on failid dokumentide valijas tööruumidesse põimimiseks saadaval.\",\n      branch: \"Haru, kust faile koguda\",\n      branch_loading: \"-- harude laadimine --\",\n      branch_explained: \"Haru, kust faile koguda.\",\n      token_information:\n        \"Ilma <b>GitHubi juurdepääsuvõtmeta</b> saab pistik koguda ainult repo <b>juurtaseme</b> faile GitHubi API piirangute tõttu.\",\n      token_personal: \"Hangi tasuta isiklik juurdepääsuvõti GitHubist siit.\",\n    },\n    gitlab: {\n      name: \"GitLabi repo\",\n      description:\n        \"Impordi kogu avalik või privaatne GitLabi repo ühe klõpsuga.\",\n      URL: \"GitLabi repo URL\",\n      URL_explained: \"Repo URL, mida soovid koguda.\",\n      token: \"GitLabi juurdepääsuvõti\",\n      optional: \"valikuline\",\n      token_description: \"Vali täiendavad objektid, mida GitLabi API-st tuua.\",\n      token_explained_start: \"Ilma \",\n      token_explained_link1: \"isikliku juurdepääsuvõtmeta\",\n      token_explained_middle:\n        \" võib GitLabi API piirata kogutavate failide hulka. Sa võid \",\n      token_explained_link2: \"luua ajutise juurdepääsuvõtme\",\n      token_explained_end: \" selle vältimiseks.\",\n      fetch_issues: \"Tõmba Issues dokumendina\",\n      ignores: \"Faili välistused\",\n      git_ignore:\n        \".gitignore formaadis nimekiri failidest, mida kogumisel ignoreerida. Vajuta Enter iga kirje järel.\",\n      task_explained:\n        \"Kui valmis, on failid dokumentide valijas tööruumidesse põimimiseks saadaval.\",\n      branch: \"Haru, kust faile koguda\",\n      branch_loading: \"-- harude laadimine --\",\n      branch_explained: \"Haru, kust faile koguda.\",\n      token_information:\n        \"Ilma <b>GitLabi juurdepääsuvõtmeta</b> saab pistik koguda ainult repo <b>juurtaseme</b> faile GitLabi API piirangute tõttu.\",\n      token_personal: \"Hangi tasuta isiklik juurdepääsuvõti GitLabist siit.\",\n    },\n    youtube: {\n      name: \"YouTube'i transkript\",\n      description: \"Impordi YouTube'i video täielik transkript lingi abil.\",\n      URL: \"YouTube'i video URL\",\n      URL_explained_start:\n        \"Sisesta ükskõik millise YouTube'i video URL, et tuua selle transkript. Videol peavad olema \",\n      URL_explained_link: \"subtiitrid\",\n      URL_explained_end: \" saadaval.\",\n      task_explained:\n        \"Kui valmis, on transkript dokumentide valijas tööruumidesse põimimiseks saadaval.\",\n    },\n    \"website-depth\": {\n      name: \"Massiline linkide kraapija\",\n      description: \"Kraabi veebisaiti ja selle alamlinke määratud sügavuseni.\",\n      URL: \"Veebisaidi URL\",\n      URL_explained: \"Veebisaidi URL, mida soovid kraapida.\",\n      depth: \"Kraapimissügavus\",\n      depth_explained: \"Alamlinkide arv, mida töötaja alg-URL-ist järgib.\",\n      max_pages: \"Maksimaalne lehtede arv\",\n      max_pages_explained: \"Maksimaalne linkide arv, mida kraapida.\",\n      task_explained:\n        \"Kui valmis, on kogu kraabitud sisu dokumentide valijas tööruumidesse põimimiseks saadaval.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Impordi kogu Confluence'i leht ühe klõpsuga.\",\n      deployment_type: \"Confluence'i tüüp\",\n      deployment_type_explained:\n        \"Määra, kas Confluence töötab Atlassiani pilves või on isemajutatud.\",\n      base_url: \"Confluence'i baas-URL\",\n      base_url_explained: \"Sinu Confluence'i ruumi baas-URL.\",\n      space_key: \"Confluence'i space key\",\n      space_key_explained:\n        \"Space key, mida kasutatakse (tavaliselt algab ~ märgiga).\",\n      username: \"Confluence'i kasutajanimi\",\n      username_explained: \"Sinu Confluence'i kasutajanimi.\",\n      auth_type: \"Autentimise tüüp\",\n      auth_type_explained:\n        \"Vali autentimise tüüp, millega Confluence'i lehtedele ligi pääseda.\",\n      auth_type_username: \"Kasutajanimi + juurdepääsuvõti\",\n      auth_type_personal: \"Isiklik juurdepääsuvõti\",\n      token: \"Confluence'i juurdepääsuvõti\",\n      token_explained_start:\n        \"Autentimiseks on vajalik juurdepääsuvõti. Saad selle genereerida\",\n      token_explained_link: \"siin\",\n      token_desc: \"Juurdepääsuvõti autentimiseks\",\n      pat_token: \"Confluence'i PAT-võti\",\n      pat_token_explained: \"Sinu isiklik juurdepääsuvõti.\",\n      task_explained:\n        \"Kui valmis, on lehe sisu dokumentide valijas tööruumidesse põimimiseks saadaval.\",\n      bypass_ssl: \"SSL-sertifikaadi valideerimise ümber\",\n      bypass_ssl_explained:\n        \"Selle valiku aktiveerimine võimaldab SSL sertifikaadi valideerimise ületada, kui kasutate enda hallatud Confluence instantsi, millel on enda välja antud sertifikaat.\",\n    },\n    manage: {\n      documents: \"Dokumendid\",\n      \"data-connectors\": \"Andmepistikud\",\n      \"desktop-only\":\n        \"Neid sätteid saab muuta ainult lauaarvutis. Ava see leht töölaual.\",\n      dismiss: \"Sulge\",\n      editing: \"Muudan\",\n    },\n    directory: {\n      \"my-documents\": \"Minu dokumendid\",\n      \"new-folder\": \"Uus kaust\",\n      \"search-document\": \"Otsi dokumenti\",\n      \"no-documents\": \"Dokumendid puuduvad\",\n      \"move-workspace\": \"Liiguta tööruumi\",\n      \"delete-confirmation\":\n        \"Kas oled kindel, et soovid need failid ja kaustad kustutada?\\nFailid eemaldatakse süsteemist ning kõigist tööruumidest.\\nTegevust ei saa tagasi võtta.\",\n      \"removing-message\":\n        \"Eemaldan {{count}} dokumenti ja {{folderCount}} kausta. Palun oota.\",\n      \"move-success\": \"Liigutatud edukalt {{count}} dokumenti.\",\n      no_docs: \"Dokumendid puuduvad\",\n      select_all: \"Vali kõik\",\n      deselect_all: \"Tühista valik\",\n      remove_selected: \"Eemalda valitud\",\n      costs: \"*Ühekordne embeddingu kulu\",\n      save_embed: \"Salvesta ja põimi\",\n      \"total-documents_one\": \"{{count}} dokument\",\n      \"total-documents_other\": \"{{count}} dokumendid\",\n    },\n    upload: {\n      \"processor-offline\": \"Dokumenditöötleja pole saadaval\",\n      \"processor-offline-desc\":\n        \"Failide üleslaadimine pole võimalik, sest töötleja on offline. Proovi hiljem uuesti.\",\n      \"click-upload\": \"Klõpsa või lohista failid siia\",\n      \"file-types\":\n        \"toetab tekstifaile, CSV-sid, arvutustabeleid, helifaile jpm!\",\n      \"or-submit-link\": \"või esita link\",\n      \"placeholder-link\": \"https://näide.ee\",\n      fetching: \"Laen…\",\n      \"fetch-website\": \"Tõmba veebisait\",\n      \"privacy-notice\":\n        \"Failid laetakse üles selle instantsi dokumenditöötlejasse ega jagata kolmandatele osapooltele.\",\n    },\n    pinning: {\n      what_pinning: \"Mis on dokumendi kinnitamine?\",\n      pin_explained_block1:\n        \"Kui <b>kinnitad</b> dokumendi, lisatakse kogu selle sisu sinu päringule, et LLM mõistaks seda täielikult.\",\n      pin_explained_block2:\n        \"Sobib eriti <b>suure kontekstiga mudelitele</b> või väikestele, kuid olulistele failidele.\",\n      pin_explained_block3:\n        \"Kui vaikimisi vastused ei rahulda, on kinnitamine lihtne viis kvaliteedi tõstmiseks.\",\n      accept: \"Selge\",\n    },\n    watching: {\n      what_watching: \"Mida tähendab dokumendi jälgimine?\",\n      watch_explained_block1:\n        \"Kui <b>jälgid</b> dokumenti, sünkroniseerime selle sisu <i>automaatselt</i> allikast kindlate intervallidega, uuendades seda kõigis tööruumides.\",\n      watch_explained_block2:\n        \"Hetkel toetab see ainult veebi-põhist sisu, mitte käsitsi üleslaetud faile.\",\n      watch_explained_block3_start: \"Saad jälgitavaid dokumente hallata \",\n      watch_explained_block3_link: \"Failihalduri\",\n      watch_explained_block3_end: \" vaates.\",\n      accept: \"Selge\",\n    },\n  },\n  chat_window: {\n    attachments_processing: \"Manused töötlevad. Palun oota…\",\n    send_message: \"Saada sõnum\",\n    attach_file: \"Lisa fail vestlusele\",\n    text_size: \"Muuda teksti suurust.\",\n    microphone: \"Esita päring häälega.\",\n    send: \"Saada päring tööruumi\",\n    tts_speak_message: \"Loe sõnum ette\",\n    copy: \"Kopeeri\",\n    regenerate: \"Loo uuesti\",\n    regenerate_response: \"Loo vastus uuesti\",\n    good_response: \"Hea vastus\",\n    more_actions: \"Rohkem toiminguid\",\n    fork: \"Hargnemine\",\n    delete: \"Kustuta\",\n    cancel: \"Tühista\",\n    edit_prompt: \"Redigeeri päringut\",\n    edit_response: \"Redigeeri vastust\",\n    preset_reset_description: \"Tühjenda vestlusajalugu ja alusta uut vestlust\",\n    add_new_preset: \" Lisa uus preset\",\n    command: \"Käsk\",\n    your_command: \"sinu-käsk\",\n    placeholder_prompt: \"Sisu, mis süstitakse sinu päringu ette.\",\n    description: \"Kirjeldus\",\n    placeholder_description: \"Vastab luuletusega LLM-idest.\",\n    save: \"Salvesta\",\n    small: \"Väike\",\n    normal: \"Tavaline\",\n    large: \"Suur\",\n    workspace_llm_manager: {\n      search: \"Otsi LLM-pakkujaid\",\n      loading_workspace_settings: \"Laen tööruumi seadeid…\",\n      available_models: \"Saadaval mudelid pakkujalt {{provider}}\",\n      available_models_description: \"Vali mudel, mida tööruumis kasutada.\",\n      save: \"Kasuta seda mudelit\",\n      saving: \"Määran mudelit vaikimisi…\",\n      missing_credentials: \"Sellel pakkujal puuduvad võtmed!\",\n      missing_credentials_description: \"Klõpsa, et määrata võtmed\",\n    },\n    submit: \"Saada\",\n    edit_info_user:\n      '\"Saada\" taastab AI vastuse. \"Salvesta\" muudab ainult teie sõnumi.',\n    edit_info_assistant: \"Teie muutused salvestatakse otse sellele vastusele.\",\n    see_less: \"Näita vähem\",\n    see_more: \"Vaata rohkem\",\n    tools: \"Vahendid\",\n    browse: \"Sirva\",\n    text_size_label: \"Teksti suurus\",\n    select_model: \"Valige mudel\",\n    sources: \"Allikasid\",\n    document: \"Dokument\",\n    similarity_match: \"mäng\",\n    source_count_one: \"{{count}} viidatud\",\n    source_count_other: \"Viidatud allikad\",\n    preset_exit_description: \"Lõpeta hetkeseisuga\",\n    add_new: \"Lisada uus\",\n    edit: \"Redigeerimine\",\n    publish: \"Avaldada\",\n    stop_generating: \"Lõpeta vastuste genereerimine\",\n    pause_tts_speech_message: \"Peata sõna-sünteesi (TTS) rääkimine sõnumis\",\n    slash_commands: \"Lihtsasti kasutatavad käsud\",\n    agent_skills: \"Agentide oskused\",\n    manage_agent_skills: \"Halda agentide oskusi\",\n    agent_skills_disabled_in_session:\n      \"Ei ole võimalik muuta oskusi aktiivse agenti seanssi ajal. Enne seanssi lõpetamist kasutage käsku /exit.\",\n    start_agent_session: \"Alusta agenti sessiooni\",\n    use_agent_session_to_use_tools:\n      \"Saate kasutada vahendeid vestluses, alustades agenti sessiooni, lisades käskile '@agent' sõna.\",\n  },\n  profile_settings: {\n    edit_account: \"Muuda kontot\",\n    profile_picture: \"Profiilipilt\",\n    remove_profile_picture: \"Eemalda profiilipilt\",\n    username: \"Kasutajanimi\",\n    new_password: \"Uus parool\",\n    password_description: \"Parool peab olema vähemalt 8 märki\",\n    cancel: \"Tühista\",\n    update_account: \"Uuenda kontot\",\n    theme: \"Teema eelistus\",\n    language: \"Eelistatud keel\",\n    failed_upload: \"Profiilipildi üleslaadimine ebaõnnestus: {{error}}\",\n    upload_success: \"Profiilipilt üles laaditud.\",\n    failed_remove: \"Profiilipildi eemaldamine ebaõnnestus: {{error}}\",\n    profile_updated: \"Profiil uuendatud.\",\n    failed_update_user: \"Kasutaja uuendamine ebaõnnestus: {{error}}\",\n    account: \"Konto\",\n    support: \"Tugi\",\n    signout: \"Logi välja\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Klaviatuuri otseteed\",\n    shortcuts: {\n      settings: \"Ava seaded\",\n      workspaceSettings: \"Ava praeguse tööruumi seaded\",\n      home: \"Mine avalehele\",\n      workspaces: \"Halda tööruume\",\n      apiKeys: \"API võtmete seaded\",\n      llmPreferences: \"LLM-eelistused\",\n      chatSettings: \"Vestluse seaded\",\n      help: \"Näita otseteeabi\",\n      showLLMSelector: \"Näita tööruumi LLM-valikut\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Edu!\",\n        success_description: \"Sinu süsteemprompt avaldati Community Hubis!\",\n        success_thank_you: \"Aitäh jagamast!\",\n        view_on_hub: \"Vaata Hubis\",\n        modal_title: \"Avalda süsteemprompt\",\n        name_label: \"Nimi\",\n        name_description: \"Süsteemprompti kuvanimi.\",\n        name_placeholder: \"Minu süsteemprompt\",\n        description_label: \"Kirjeldus\",\n        description_description:\n          \"Kirjeldus, mis selgitab süsteemprompti eesmärki.\",\n        tags_label: \"Sildid\",\n        tags_description:\n          \"Lisa kuni 5 silti (kuni 20 tähemärki igaüks) otsingu lihtsustamiseks.\",\n        tags_placeholder: \"Kirjuta ja vajuta Enter, et lisada silte\",\n        visibility_label: \"Nähtavus\",\n        public_description: \"Avalikud promptid on kõigile nähtavad.\",\n        private_description: \"Privaatseid prompte näed vaid sina.\",\n        publish_button: \"Avalda Community Hubis\",\n        submitting: \"Avaldan…\",\n        prompt_label: \"Prompt\",\n        prompt_description: \"Süsteemprompt, mis juhendab LLM-i.\",\n        prompt_placeholder: \"Sisesta süsteemprompt siia…\",\n      },\n      agent_flow: {\n        success_title: \"Edu!\",\n        success_description: \"Sinu agendi voog avaldati Community Hubis!\",\n        success_thank_you: \"Aitäh jagamast!\",\n        view_on_hub: \"Vaata Hubis\",\n        modal_title: \"Avalda agendi voog\",\n        name_label: \"Nimi\",\n        name_description: \"Agendi voo kuvanimi.\",\n        name_placeholder: \"Minu agendi voog\",\n        description_label: \"Kirjeldus\",\n        description_description: \"Kirjeldus, mis selgitab agendi voo eesmärki.\",\n        tags_label: \"Sildid\",\n        tags_description:\n          \"Lisa kuni 5 silti (kuni 20 tähemärki) otsingu lihtsustamiseks.\",\n        tags_placeholder: \"Kirjuta ja vajuta Enter, et lisada silte\",\n        visibility_label: \"Nähtavus\",\n        submitting: \"Avaldan…\",\n        submit: \"Avalda Community Hubis\",\n        privacy_note:\n          \"Agendi vood laetakse üles alati privaatsena, et kaitsta tundlikku infot. Nähtavust saab hiljem Hubis muuta. Kontrolli, et voog ei sisaldaks privaatseid andmeid.\",\n      },\n      slash_command: {\n        success_title: \"Edu!\",\n        success_description: \"Sinu slash-käsk avaldati Community Hubis!\",\n        success_thank_you: \"Aitäh jagamast!\",\n        view_on_hub: \"Vaata Hubis\",\n        modal_title: \"Avalda slash-käsk\",\n        name_label: \"Nimi\",\n        name_description: \"Slash-käsku kuvatav nimi.\",\n        name_placeholder: \"Minu slash-käsk\",\n        description_label: \"Kirjeldus\",\n        description_description:\n          \"Kirjeldus, mis selgitab slash-käsku eesmärki.\",\n        tags_label: \"Sildid\",\n        tags_description:\n          \"Lisa kuni 5 silti (kuni 20 tähemärki) otsingu lihtsustamiseks.\",\n        tags_placeholder: \"Kirjuta ja vajuta Enter, et lisada silte\",\n        visibility_label: \"Nähtavus\",\n        public_description: \"Avalikud käsud on kõigile nähtavad.\",\n        private_description: \"Privaatseid käske näed vaid sina.\",\n        publish_button: \"Avalda Community Hubis\",\n        submitting: \"Avaldan…\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Prompt, mida kasutatakse, kui slash-käsk käivitub.\",\n        prompt_placeholder: \"Sisesta prompt siia…\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Autentimine vajalik\",\n          description: \"Enne avaldamist pead Community Hubi sisselogima.\",\n          button: \"Ühendu Community Hubiga\",\n        },\n      },\n    },\n  },\n  security: {\n    title: \"Turvalisus\",\n    multiuser: {\n      title: \"Mitme kasutaja režiim\",\n      description:\n        \"Lülita mitme kasutaja tugi sisse, et meeskond saaks instantsi kasutada.\",\n      enable: {\n        \"is-enable\": \"Mitme kasutaja režiim on sisse lülitatud\",\n        enable: \"Lülita sisse\",\n        description:\n          \"Vaikimisi oled ainus administraator. Adminid loovad uued kasutajad ja paroole.\",\n        username: \"Admini kasutajanimi\",\n        password: \"Admini parool\",\n      },\n    },\n    password: {\n      title: \"Paroolikaitse\",\n      description:\n        \"Kaitse oma instantsi parooliga. Kui unustad selle, taastamisvõimalust ei ole.\",\n      \"password-label\": \"Instantsi parool\",\n    },\n  },\n  home: {\n    welcome: \"Tere tulemast\",\n    chooseWorkspace: \"Vali tööruum, et alustada vestlust!\",\n    notAssigned:\n      \"Sa ei ole täidetud ühtegi tööruumi.\\nPäringu tööruumiks, palun pööra teie administraatorile.\",\n    goToWorkspace: 'Mine tööruumiks \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/fa/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    survey: {\n      email: \"آدرس ایمیل شما چیست؟\",\n      useCase: \"شما از AnythingLLM برای چه منظوری استفاده خواهید کرد؟\",\n      useCaseWork: \"برای کار\",\n      useCasePersonal: \"برای استفاده شخصی\",\n      useCaseOther: \"سایر\",\n      comment: \"شما از کجا در مورد AnythingLLM مطلع شدید؟\",\n      commentPlaceholder:\n        \"Reddit، توییتر، گیت‌هاب، یوتیوب و غیره - لطفاً به ما بگویید که چگونه ما را پیدا کردید!\",\n      skip: \"پرش از نظرسنجی\",\n      thankYou: \"از بازخورد شما سپاسگزاریم.\",\n      title: \"به AnythingLLM خوش آمدید\",\n      description:\n        \"ما را در ساخت مدل AnythingLLM متناسب با نیازهای شما یاری دهید. (این بخش اختیاری است)\",\n    },\n    home: {\n      title: \"به\",\n      getStarted: \"شروع کنید\",\n    },\n    llm: {\n      title: \"ترجیحات مدل‌های زبان بزرگ\",\n      description:\n        \"AnythingLLM می‌تواند با بسیاری از ارائه‌دهندگان مدل‌های زبانی کار کند. این سرویس، مسئولیت انجام مکالمات را بر عهده خواهد داشت.\",\n    },\n    userSetup: {\n      title: \"راه‌اندازی حساب کاربری\",\n      description: \"تنظیمات کاربری خود را انجام دهید.\",\n      howManyUsers:\n        \"تعداد کاربران که از این نمونه استفاده خواهند کرد چقدر است؟\",\n      justMe: \"فقط من\",\n      myTeam: \"تیم من\",\n      instancePassword: \"رمز عبور\",\n      setPassword: \"آیا می‌خواهید یک رمز عبور تعیین کنید؟\",\n      passwordReq: \"رمز عبور باید حداقل 8 کاراکتر باشد.\",\n      passwordWarn:\n        \"مهم است که این رمز عبور را حفظ کنید، زیرا هیچ روشی برای بازیابی آن وجود ندارد.\",\n      adminUsername: \"نام کاربری حساب مدیر\",\n      adminPassword: \"رمز عبور حساب کاربری\",\n      adminPasswordReq: \"رمز عبور باید حداقل 8 کاراکتر باشد.\",\n      teamHint:\n        \"به طور پیش‌فرض، شما تنها مدیر خواهید بود. پس از اتمام فرآیند ثبت‌نام، می‌توانید افراد دیگری را به عنوان کاربران یا مدیران اضافه کنید. لطفاً رمز عبور خود را فراموش نکنید، زیرا تنها مدیران می‌توانند رمز عبور را بازنشانی کنند.\",\n    },\n    data: {\n      title: \"مدیریت داده‌ها و حریم خصوصی\",\n      description:\n        \"ما متعهد به شفافیت و کنترل در رابطه با اطلاعات شخصی شما هستیم.\",\n      settingsHint:\n        \"این تنظیمات می‌توانند در هر زمان در بخش تنظیمات تغییر داده شوند.\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"نام فضای کار\",\n    user: \"کاربر\",\n    selection: \"انتخاب مدل\",\n    saving: \"در حال ذخیره...\",\n    save: \"ذخیره تغییرات\",\n    previous: \"صفحه قبلی\",\n    next: \"صفحه بعدی\",\n    optional: \"اختیاری\",\n    yes: \"بله\",\n    no: \"نه\",\n    search: \"جستجو\",\n    username_requirements:\n      \"نام کاربری باید 2 تا 32 کاراکتر باشد، با حرف کوچک شروع شود و فقط شامل حروف کوچک، اعداد، زیرخط، خط تیره و نقطه باشد.\",\n    on: \"در\",\n    none: \"هیچ\",\n    stopped: \"متوقف شده\",\n    loading: \"بارگذاری\",\n    refresh: \"تازه‌سازی کردن\",\n  },\n  settings: {\n    title: \"تنظیمات سامانه\",\n    invites: \"دعوت‌نامه‌ها\",\n    users: \"کاربران\",\n    workspaces: \"فضاهای کاری\",\n    \"workspace-chats\": \"گفتگوهای فضای کاری\",\n    customization: \"شخصی‌سازی\",\n    \"api-keys\": \"API توسعه‌دهندگان\",\n    llm: \"مدل زبانی\",\n    transcription: \"رونویسی\",\n    embedder: \"جاسازی\",\n    \"text-splitting\": \"تقسیم متن و تکه‌بندی\",\n    \"voice-speech\": \"صدا و گفتار\",\n    \"vector-database\": \"پایگاه داده برداری\",\n    embeds: \"جاسازی گفتگو\",\n    security: \"امنیت\",\n    \"event-logs\": \"گزارش رویدادها\",\n    privacy: \"حریم خصوصی و داده‌ها\",\n    \"ai-providers\": \"ارائه‌دهندگان هوش مصنوعی\",\n    \"agent-skills\": \"مهارت‌های عامل\",\n    admin: \"مدیریت\",\n    tools: \"ابزارها\",\n    \"experimental-features\": \"ویژگی‌های آزمایشی\",\n    contact: \"تماس با پشتیبانی\",\n    \"browser-extension\": \"افزونه مرورگر\",\n    \"system-prompt-variables\": \"متغیرهای اعلان سیستم\\n\\n\\nمتغیرهای اعلان سیستم\",\n    interface: \"تنظیمات رابط کاربری\",\n    branding: \"برندسازی و تولید محصولات با برچسب سفید\",\n    chat: \"چت\",\n    \"mobile-app\": \"AnythingLLM Mobile\",\n    \"community-hub\": {\n      title: \"مرکز محلی\",\n      trending: \"بررسی ترندها\",\n      \"your-account\": \"حساب شما\",\n      \"import-item\": \"وارد کردن کالا\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"خوش آمدید به\",\n      \"placeholder-username\": \"نام کاربری\",\n      \"placeholder-password\": \"رمز عبور\",\n      login: \"ورود\",\n      validating: \"در حال اعتبارسنجی...\",\n      \"forgot-pass\": \"فراموشی رمز عبور\",\n      reset: \"بازنشانی\",\n    },\n    \"sign-in\": \"ورود به حساب {{appName}} کاربری شما.\",\n    \"password-reset\": {\n      title: \"بازنشانی رمز عبور\",\n      description: \"برای بازنشانی رمز عبور خود، اطلاعات لازم را وارد کنید.\",\n      \"recovery-codes\": \"کدهای بازیابی\",\n      \"back-to-login\": \"بازگشت به صفحه ورود\",\n    },\n  },\n  \"new-workspace\": {\n    title: \"فضای کاری جدید\",\n    placeholder: \"فضای کاری من\",\n  },\n  \"workspaces—settings\": {\n    general: \"تنظیمات عمومی\",\n    chat: \"تنظیمات گفتگو\",\n    vector: \"پایگاه داده برداری\",\n    members: \"اعضا\",\n    agent: \"پیکربندی عامل\",\n  },\n  general: {\n    vector: {\n      title: \"تعداد بردارها\",\n      description: \"تعداد کل بردارها در پایگاه داده برداری شما.\",\n    },\n    names: {\n      description: \"این فقط نام نمایشی فضای کاری شما را تغییر خواهد داد.\",\n    },\n    message: {\n      title: \"پیام‌های گفتگوی پیشنهادی\",\n      description:\n        \"پیام‌هایی که به کاربران فضای کاری پیشنهاد می‌شود را شخصی‌سازی کنید.\",\n      add: \"افزودن پیام جدید\",\n      save: \"ذخیره پیام‌ها\",\n      heading: \"برایم توضیح بده\",\n      body: \"مزایای AnythingLLM را\",\n    },\n    delete: {\n      title: \"حذف فضای کاری\",\n      description:\n        \"این فضای کاری و تمام داده‌های آن را حذف کنید. این کار فضای کاری را برای همه کاربران حذف خواهد کرد.\",\n      delete: \"حذف فضای کاری\",\n      deleting: \"در حال حذف فضای کاری...\",\n      \"confirm-start\": \"شما در حال حذف کامل\",\n      \"confirm-end\":\n        \"فضای کاری هستید. این کار تمام جاسازی‌های برداری را از پایگاه داده برداری شما حذف خواهد کرد.\\n\\nفایل‌های اصلی منبع دست نخورده باقی خواهند ماند. این عمل برگشت‌ناپذیر است.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"ارائه‌دهنده LLM فضای کاری\",\n      description:\n        \"ارائه‌دهنده و مدل LLM خاصی که برای این فضای کاری استفاده خواهد شد. به طور پیش‌فرض، از ارائه‌دهنده و تنظیمات LLM سیستم استفاده می‌کند.\",\n      search: \"جستجوی تمام ارائه‌دهندگان LLM\",\n    },\n    model: {\n      title: \"مدل گفتگوی فضای کاری\",\n      description:\n        \"مدل گفتگوی خاصی که برای این فضای کاری استفاده خواهد شد. اگر خالی باشد، از ترجیحات LLM سیستم استفاده خواهد کرد.\",\n    },\n    mode: {\n      title: \"حالت گفتگو\",\n      chat: {\n        title: \"گفتگو\",\n        description:\n          \"با استفاده از دانش عمومی مدل زبانی و اطلاعات موجود در سند، پاسخ‌ها را ارائه خواهد داد. برای استفاده از ابزارها، باید از دستور @agent استفاده کنید.\",\n      },\n      query: {\n        title: \"پرس‌وجو\",\n        description:\n          \"پاسخ‌ها را تنها در صورت یافتن زمینه سند ارائه می‌دهد. برای استفاده از ابزارها، باید از دستور @agent استفاده کنید.\",\n      },\n      automatic: {\n        title: \"خودرو\",\n        description:\n          \"اگر مدل و ارائه‌دهنده از فراخوانی ابزار به صورت پیش‌فرض پشتیبانی کنند، ابزارها به‌طور خودکار استفاده خواهند شد. <br />در صورتی که فراخوانی ابزار به صورت پیش‌فرض پشتیبانی نشود، شما باید از دستور @agent برای استفاده از ابزارها استفاده کنید.\",\n      },\n    },\n    history: {\n      title: \"تاریخچه گفتگو\",\n      \"desc-start\":\n        \"تعداد گفتگوهای قبلی که در حافظه کوتاه‌مدت پاسخ گنجانده خواهد شد.\",\n      recommend: \"پیشنهاد: ۲۰. \",\n      \"desc-end\":\n        \"بیش از ۴۵ احتمالاً منجر به شکست مداوم گفتگو می‌شود که به اندازه پیام‌ها بستگی دارد.\",\n    },\n    prompt: {\n      title: \"پیش‌متن\",\n      description:\n        \"پیش‌متنی که در این فضای کاری استفاده خواهد شد. زمینه و دستورالعمل‌ها را برای تولید پاسخ توسط هوش مصنوعی تعریف کنید. باید یک پیش‌متن دقیق ارائه دهید تا هوش مصنوعی بتواند پاسخی مرتبط و دقیق تولید کند.\",\n      history: {\n        title: \"تاریخچه دستورات سیستم\",\n        clearAll: \"پاک کردن همه\",\n        noHistory: \"هیچ سابقه دستورالعمل در دسترس نیست.\",\n        restore: \"بازگرداندن\",\n        delete: \"حذف\",\n        deleteConfirm:\n          \"آیا مطمئن هستید که می‌خواهید این آیتم تاریخ را حذف کنید؟\",\n        clearAllConfirm:\n          \"آیا مطمئن هستید که می‌خواهید تمام تاریخچه را پاک کنید؟ این اقدام قابل لغو نیست.\",\n        expand: \"گسترش\",\n        publish: \"انتشار در مرکز جامعه\",\n      },\n    },\n    refusal: {\n      title: \"پاسخ رد در حالت پرس‌وجو\",\n      \"desc-start\": \"در حالت\",\n      query: \"پرس‌وجو\",\n      \"desc-end\":\n        \"ممکن است بخواهید هنگامی که هیچ محتوایی یافت نمی‌شود، یک پاسخ رد سفارشی برگردانید.\",\n      \"tooltip-title\": \"من این را می‌بینم، چرا؟\",\n      \"tooltip-description\":\n        \"شما در حالت پرس‌وجو هستید، که تنها از اطلاعات موجود در اسناد شما استفاده می‌کند. برای گفتگوهای انعطاف‌پذیرتر، به حالت چت بروید، یا برای کسب اطلاعات بیشتر در مورد حالت‌های چت، اینجا را کلیک کنید.\",\n    },\n    temperature: {\n      title: \"دمای LLM\",\n      \"desc-start\":\n        'این تنظیم میزان \"خلاقیت\" پاسخ‌های LLM شما را کنترل می‌کند.',\n      \"desc-end\":\n        \"هر چه عدد بالاتر باشد، خلاقیت بیشتر است. برای برخی مدل‌ها، تنظیم بسیار بالا می‌تواند منجر به پاسخ‌های نامفهوم شود.\",\n      hint: \"اکثر LLMها محدوده‌های مختلفی از مقادیر معتبر را دارند. برای این اطلاعات به ارائه‌دهنده LLM خود مراجعه کنید.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"شناسه پایگاه داده برداری\",\n    snippets: {\n      title: \"حداکثر قطعات متنی\",\n      description:\n        \"این تنظیم حداکثر تعداد قطعات متنی که برای هر گفتگو یا پرس‌وجو به LLM ارسال می‌شود را کنترل می‌کند.\",\n      recommend: \"پیشنهادی: 4\",\n    },\n    doc: {\n      title: \"آستانه شباهت سند\",\n      description:\n        \"حداقل امتیاز شباهت مورد نیاز برای اینکه یک منبع مرتبط با گفتگو در نظر گرفته شود. هر چه عدد بالاتر باشد، منبع باید شباهت بیشتری با گفتگو داشته باشد.\",\n      zero: \"بدون محدودیت\",\n      low: \"پایین (امتیاز شباهت ≥ .25)\",\n      medium: \"متوسط (امتیاز شباهت ≥ .50)\",\n      high: \"بالا (امتیاز شباهت ≥ .75)\",\n    },\n    reset: {\n      reset: \"بازنشانی پایگاه داده برداری\",\n      resetting: \"در حال پاک کردن بردارها...\",\n      confirm:\n        \"شما در حال بازنشانی پایگاه داده برداری این فضای کاری هستید. این کار تمام جاسازی‌های برداری فعلی را حذف خواهد کرد.\\n\\nفایل‌های اصلی منبع دست نخورده باقی خواهند ماند. این عمل برگشت‌ناپذیر است.\",\n      error: \"بازنشانی پایگاه داده برداری فضای کاری امکان‌پذیر نبود!\",\n      success: \"پایگاه داده برداری فضای کاری بازنشانی شد!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"عملکرد LLMهایی که به طور صریح از فراخوانی ابزار پشتیبانی نمی‌کنند، به شدت به قابلیت‌ها و دقت مدل وابسته است. برخی توانایی‌ها ممکن است محدود یا غیرفعال باشند.\",\n    provider: {\n      title: \"ارائه‌دهنده LLM عامل فضای کاری\",\n      description:\n        \"ارائه‌دهنده و مدل LLM خاصی که برای عامل @agent این فضای کاری استفاده خواهد شد.\",\n    },\n    mode: {\n      chat: {\n        title: \"مدل گفتگوی عامل فضای کاری\",\n        description:\n          \"مدل گفتگوی خاصی که برای عامل @agent این فضای کاری استفاده خواهد شد.\",\n      },\n      title: \"مدل عامل فضای کاری\",\n      description:\n        \"مدل LLM خاصی که برای عامل @agent این فضای کاری استفاده خواهد شد.\",\n      wait: \"-- در انتظار مدل‌ها --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG و حافظه بلندمدت\",\n        description:\n          'به عامل اجازه دهید از اسناد محلی شما برای پاسخ به پرس‌وجو استفاده کند یا از عامل بخواهید قطعات محتوا را برای بازیابی حافظه بلندمدت \"به خاطر بسپارد\".',\n      },\n      view: {\n        title: \"مشاهده و خلاصه‌سازی اسناد\",\n        description:\n          \"به عامل اجازه دهید محتوای فایل‌های جاسازی شده فعلی فضای کاری را فهرست و خلاصه کند.\",\n      },\n      scrape: {\n        title: \"استخراج از وب‌سایت‌ها\",\n        description:\n          \"به عامل اجازه دهید محتوای وب‌سایت‌ها را بازدید و استخراج کند.\",\n      },\n      generate: {\n        title: \"تولید نمودارها\",\n        description:\n          \"به عامل پیش‌فرض امکان تولید انواع مختلف نمودار از داده‌های ارائه شده یا داده شده در گفتگو را بدهید.\",\n      },\n      save: {\n        title: \"تولید و ذخیره فایل‌ها در مرورگر\",\n        description:\n          \"به عامل پیش‌فرض امکان تولید و نوشتن در فایل‌هایی که ذخیره می‌شوند و می‌توانند در مرورگر شما دانلود شوند را بدهید.\",\n      },\n      web: {\n        title: \"جستجو و مرور زنده وب\",\n        description:\n          \"با اتصال به یک ارائه‌دهنده خدمات جستجوی وب (SERP)، به نماینده خود این امکان را بدهید تا از طریق اینترنت، به سوالات شما پاسخ دهد.\",\n      },\n      sql: {\n        title: \"اتصال دهنده SQL\",\n        description:\n          \"به اپراتور خود اجازه دهید تا با اتصال به ارائه‌دهندگان مختلف پایگاه داده SQL، از SQL برای پاسخگویی به سوالات شما استفاده کند.\",\n      },\n      default_skill:\n        \"به طور پیش‌فرض، این قابلیت فعال است، اما می‌توانید آن را غیرفعال کنید اگر نمی‌خواهید این قابلیت برای نمایندگی در دسترس باشد.\",\n    },\n    mcp: {\n      title: \"سرورهای MCP\",\n      \"loading-from-config\": \"بارگذاری سرورهای MCP از طریق فایل پیکربندی\",\n      \"learn-more\": \"در مورد سرورهای MCP اطلاعات بیشتری کسب کنید.\",\n      \"no-servers-found\": \"هیچ سرور MCP یافت نشد.\",\n      \"tool-warning\":\n        \"برای به دست آوردن بهترین عملکرد، می‌توانید ابزارهایی که نیاز ندارید را غیرفعال کنید تا از منابع و اطلاعات موجود بهره‌برداری بهتری داشته باشید.\",\n      \"stop-server\": \"متوقف کردن سرور MCP\",\n      \"start-server\": \"شروع سرور MCP\",\n      \"delete-server\": \"حذف سرور MCP\",\n      \"tool-count-warning\":\n        \"این سرور MCP دارای ابزارهای <b> با قابلیت {{count}} است که در هر چت، از فضای مورد استفاده (context) بهره می‌برند. </b> برای صرفه‌جویی در فضای مورد استفاده، توصیه می‌شود این ابزارهای غیرضروری را غیرفعال کنید.\",\n      \"startup-command\": \"دستورالعمل اولیه\",\n      command: \"دستورالعمل\",\n      arguments: \"استدلال‌ها، بحث‌ها، دلایل\",\n      \"not-running-warning\":\n        \"این سرور MCP در حال اجرا نیست - ممکن است متوقف شده باشد یا در هنگام راه‌اندازی با مشکل مواجه شده باشد.\",\n      \"tool-call-arguments\": \"آرگومان‌های فراخوانی ابزار\",\n      \"tools-enabled\": \"ابزارهای فعال\",\n    },\n    settings: {\n      title: \"تنظیمات مهارت‌های کارمند\",\n      \"max-tool-calls\": {\n        title: \"حداکثر تعداد درخواست‌های ابزار در هر پاسخ\",\n        description:\n          \"حداکثر تعداد ابزارهایی که یک عامل می‌تواند برای تولید یک پاسخ واحد به آن‌ها متصل کند. این کار از اجرای مکرر و بی‌نهایت ابزارها جلوگیری می‌کند.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"انتخاب مهارت‌های هوشمند\",\n        \"beta-badge\": \"بتا\",\n        description:\n          \"امکان استفاده از ابزارهای نامحدود و کاهش مصرف توکن تا 80 درصد برای هر پرس و جو را فراهم کنید - AnythingLLM به طور خودکار بهترین مهارت‌ها را برای هر پرس و جو انتخاب می‌کند.\",\n        \"max-tools\": {\n          title: \"ابزارهای مکس\",\n          description:\n            \"حداکثر تعداد ابزارهایی که می‌توان برای هر پرس و جو انتخاب کرد. ما توصیه می‌کنیم که این مقدار را برای مدل‌های با زمینه بزرگتر، به مقادیر بالاتر تنظیم کنید.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"گفتگوهای فضای کاری\",\n    description:\n      \"این‌ها تمام گفتگوها و پیام‌های ثبت شده هستند که توسط کاربران ارسال شده‌اند و بر اساس تاریخ ایجاد مرتب شده‌اند.\",\n    export: \"خروجی‌گیری\",\n    table: {\n      id: \"شناسه\",\n      by: \"ارسال شده توسط\",\n      workspace: \"فضای کاری\",\n      prompt: \"درخواست\",\n      response: \"پاسخ\",\n      at: \"زمان ارسال\",\n    },\n  },\n  api: {\n    title: \"کلیدهای API\",\n    description:\n      \"کلیدهای API به دارنده آن‌ها اجازه می‌دهند به صورت برنامه‌نویسی به این نمونه AnythingLLM دسترسی داشته و آن را مدیریت کنند.\",\n    link: \"مطالعه مستندات API\",\n    generate: \"ایجاد کلید API جدید\",\n    table: {\n      key: \"کلید API\",\n      by: \"ایجاد شده توسط\",\n      created: \"تاریخ ایجاد\",\n    },\n  },\n  llm: {\n    title: \"ترجیحات مدل زبانی\",\n    description:\n      \"این‌ها اعتبارنامه‌ها و تنظیمات ارائه‌دهنده مدل زبانی و جاسازی انتخابی شما هستند. مهم است که این کلیدها به‌روز و صحیح باشند در غیر این صورت AnythingLLM به درستی کار نخواهد کرد.\",\n    provider: \"ارائه‌دهنده مدل زبانی\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"پایان‌نقطه سرویس Azure\",\n        api_key: \"کلید API\",\n        chat_deployment_name: \"نام استقرار چت\",\n        chat_model_token_limit: \"محدودیت تعداد توکن در مدل چت\",\n        model_type: \"نوع مدل\",\n        default: \"پیش‌فرض\",\n        reasoning: \"استدلال\",\n        model_type_tooltip:\n          'اگر سیستم شما از یک مدل استدلال (مانند o1، o1-mini، o3-mini و غیره) استفاده می‌کند، این گزینه را روی \"استدلال\" تنظیم کنید. در غیر این صورت، درخواست‌های چت شما ممکن است با شکست مواجه شوند.',\n      },\n    },\n  },\n  transcription: {\n    title: \"ترجیحات مدل رونویسی\",\n    description:\n      \"این‌ها اعتبارنامه‌ها و تنظیمات ارائه‌دهنده مدل رونویسی انتخابی شما هستند. مهم است که این کلیدها به‌روز و صحیح باشند در غیر این صورت فایل‌های رسانه و صوتی رونویسی نخواهند شد.\",\n    provider: \"ارائه‌دهنده رونویسی\",\n    \"warn-start\":\n      \"استفاده از مدل محلی Whisper روی دستگاه‌هایی با RAM یا CPU محدود می‌تواند هنگام پردازش فایل‌های رسانه‌ای باعث توقف AnythingLLM شود.\",\n    \"warn-recommend\":\n      \"ما حداقل ۲ گیگابایت RAM و آپلود فایل‌های کمتر از ۱۰ مگابایت را توصیه می‌کنیم.\",\n    \"warn-end\": \"مدل داخلی در اولین استفاده به صورت خودکار دانلود خواهد شد.\",\n  },\n  embedding: {\n    title: \"ترجیحات جاسازی\",\n    \"desc-start\":\n      \"هنگام استفاده از یک LLM که به طور پیش‌فرض از موتور جاسازی پشتیبانی نمی‌کند - ممکن است نیاز به تعیین اعتبارنامه‌های اضافی برای جاسازی متن داشته باشید.\",\n    \"desc-end\":\n      \"جاسازی فرآیند تبدیل متن به بردارها است. این اعتبارنامه‌ها برای تبدیل فایل‌ها و درخواست‌های شما به فرمتی که AnythingLLM بتواند پردازش کند، ضروری هستند.\",\n    provider: {\n      title: \"ارائه‌دهنده جاسازی\",\n    },\n  },\n  text: {\n    title: \"تقسیم متن و تکه‌بندی\",\n    \"desc-start\":\n      \"تقسیم متن به شما امکان می‌دهد اسناد بزرگ را به بخش‌های کوچک‌تر تقسیم کنید که برای جاسازی و پردازش مناسب‌تر هستند.\",\n    \"desc-end\":\n      \"سعی کنید تعادلی بین اندازه بخش و همپوشانی ایجاد کنید تا از دست رفتن اطلاعات را به حداقل برسانید.\",\n    size: {\n      title: \"حداکثر اندازه بخش\",\n      description:\n        \"این حداکثر تعداد کاراکترهایی است که می‌تواند در یک بردار وجود داشته باشد.\",\n      recommend: \"حداکثر طول مدل جاسازی\",\n    },\n    overlap: {\n      title: \"همپوشانی بخش‌های متن\",\n      description:\n        \"این حداکثر همپوشانی کاراکترها است که در هنگام تکه‌بندی بین دو بخش متن مجاور رخ می‌دهد.\",\n    },\n  },\n  vector: {\n    title: \"پایگاه داده برداری\",\n    description:\n      \"این‌ها اعتبارنامه‌ها و تنظیمات نحوه عملکرد نمونه AnythingLLM شما هستند. مهم است که این کلیدها به‌روز و صحیح باشند.\",\n    provider: {\n      title: \"ارائه‌دهنده پایگاه داده برداری\",\n      description: \"برای LanceDB نیازی به پیکربندی نیست.\",\n    },\n  },\n  embeddable: {\n    title: \"جاسازی گفتگو\",\n    description:\n      \"جاسازی گفتگو به شما امکان می‌دهد گفتگوی فضای کاری را در وب‌سایت یا برنامه خود قرار دهید.\",\n    create: \"ایجاد جاسازی جدید\",\n    table: {\n      workspace: \"فضای کاری\",\n      chats: \"گفتگوهای ارسال شده\",\n      active: \"دامنه‌های فعال\",\n      created: \"ایجاد شده\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"گفتگوهای جاسازی شده\",\n    export: \"خروجی‌گیری\",\n    description:\n      \"این لیست تمام گفتگوها و پیام‌های ثبت شده از هر جاسازی که منتشر کرده‌اید را نشان می‌دهد.\",\n    table: {\n      embed: \"جاسازی\",\n      sender: \"فرستنده\",\n      message: \"پیام\",\n      response: \"پاسخ\",\n      at: \"زمان ارسال\",\n    },\n  },\n  event: {\n    title: \"گزارش رویدادها\",\n    description:\n      \"مشاهده تمام اقدامات و رویدادهای در حال وقوع در این نمونه برای نظارت.\",\n    clear: \"پاک کردن گزارش رویدادها\",\n    table: {\n      type: \"نوع رویداد\",\n      user: \"کاربر\",\n      occurred: \"زمان وقوع\",\n    },\n  },\n  privacy: {\n    title: \"حریم خصوصی و مدیریت داده‌ها\",\n    description:\n      \"این پیکربندی شما برای نحوه مدیریت داده‌ها توسط ارائه‌دهندگان شخص ثالث متصل و AnythingLLM است.\",\n    anonymous: \"ارسال تله‌متری ناشناس فعال است\",\n  },\n  connectors: {\n    \"search-placeholder\": \"اتصال‌دهنده‌های داده\",\n    \"no-connectors\": \"هیچ اتصال داده‌ای یافت نشد.\",\n    github: {\n      name: \"ذوبان\",\n      description: \"وارد کردن کل یک مخزن عمومی یا خصوصی در GitHub با یک کلیک.\",\n      URL: \"آدرس مخزن GitHub\",\n      URL_explained:\n        \"آدرس مخزن GitHub که می‌خواهید از آن اطلاعات جمع‌آوری کنید.\",\n      token: \"توکن دسترسی به گیت‌هاب\",\n      optional: \"اختیاری\",\n      token_explained: \"توکن دسترسی برای جلوگیری از محدودیت سرعت.\",\n      token_explained_start: \"بدون\",\n      token_explained_link1: \"توکن دسترسی شخصی\",\n      token_explained_middle:\n        \"، به دلیل محدودیت‌های سرعت، ممکن است API GitHub تعداد فایل‌هایی که می‌توان جمع‌آوری کرد را محدود کند. شما می‌توانید\",\n      token_explained_link2: \"ایجاد یک توکن دسترسی موقت\",\n      token_explained_end: \"برای جلوگیری از این مشکل.\",\n      ignores: \"فایل را نادیده بگیرید\",\n      git_ignore:\n        \"فایل را در فرمت .gitignore برای نادیده گرفتن فایل‌های خاص در حین جمع‌آوری، وارد کنید. پس از هر ورودی که می‌خواهید ذخیره کنید، کلید Enter را فشار دهید.\",\n      task_explained:\n        \"پس از اتمام، تمام فایل‌ها برای درج در محیط‌های کاری در انتخاب‌گر اسناد در دسترس خواهند بود.\",\n      branch: \"دایرکتی که می‌خواهید فایل‌ها را از آن دریافت کنید.\",\n      branch_loading: \"-- بارگذاری شاخ‌های موجود --\",\n      branch_explained: \"دایرتی که می‌خواهید فایل‌ها را از آن دریافت کنید.\",\n      token_information:\n        \"با وارد نکردن **توکن دسترسی GitHub**، این اتصال داده فقط می‌تواند فایل‌های سطح بالایی از مخزن را جمع‌آوری کند، به دلیل محدودیت‌های نرخ دسترسی API عمومی GitHub.\",\n      token_personal:\n        \"با داشتن یک حساب کاربری در GitHub، می‌توانید یک توکن دسترسی شخصی رایگان دریافت کنید.\",\n    },\n    gitlab: {\n      name: \"ذخیره GitLab\",\n      description: \"وارد کردن کل یک مخزن عمومی یا خصوصی GitLab با یک کلیک.\",\n      URL: \"آدرس مخزن GitLab\",\n      URL_explained:\n        \"آدرس مخزن GitLab که می‌خواهید از آن اطلاعات جمع‌آوری کنید.\",\n      token: \"توکن دسترسی GitLab\",\n      optional: \"اختیاری\",\n      token_description:\n        \"برای دریافت اطلاعات از API GitLab، موجودیت‌های اضافی را انتخاب کنید.\",\n      token_explained_start: \"بدون\",\n      token_explained_link1: \"توکن دسترسی شخصی\",\n      token_explained_middle:\n        \"، API گیت‌لاب ممکن است به دلیل محدودیت‌های سرعت، تعداد فایل‌هایی که می‌توان جمع‌آوری کرد را محدود کند. شما می‌توانید\",\n      token_explained_link2: \"ایجاد یک توکن دسترسی موقت\",\n      token_explained_end: \"برای جلوگیری از این مشکل.\",\n      fetch_issues: \"استخراج مسائل به صورت اسناد\",\n      ignores: \"فایل را نادیده بگیرید\",\n      git_ignore:\n        \"فایل را در فرمت .gitignore برای نادیده گرفتن فایل‌های خاص در حین جمع‌آوری، وارد کنید. پس از هر ورودی که می‌خواهید ذخیره کنید، کلید Enter را فشار دهید.\",\n      task_explained:\n        \"پس از اتمام، تمام فایل‌ها برای قرار دادن در محیط‌های کاری در انتخاب‌گر فایل‌ها در دسترس خواهند بود.\",\n      branch: \"دایرتی که می‌خواهید فایل‌ها را از آن دریافت کنید\",\n      branch_loading: \"-- بارگذاری شاخ‌های موجود --\",\n      branch_explained: \"دایرکتی که می‌خواهید فایل‌ها را از آن دریافت کنید.\",\n      token_information:\n        \"با عدم وارد کردن **توکن دسترسی GitLab**، این اتصال داده تنها قادر به جمع‌آوری **فایل‌های سطح اول** مخزن خواهد بود، به دلیل محدودیت‌های نرخ دسترسی API عمومی GitLab.\",\n      token_personal:\n        \"با داشتن یک حساب کاربری در GitLab، می‌توانید یک توکن دسترسی شخصی رایگان دریافت کنید.\",\n    },\n    youtube: {\n      name: \"اسکریپت یوتیوب\",\n      description: \"وارد کردن متن یک ویدیو کامل از یوتیوب از طریق یک لینک.\",\n      URL: \"لینک ویدیو در یوتیوب\",\n      URL_explained_start:\n        \"برای دریافت زیرنویس هر ویدیوی یوتیوب، آدرس URL آن را وارد کنید. ویدیوی مورد نظر باید دارای\",\n      URL_explained_link: \"زیرنویس\",\n      URL_explained_end: \"در دسترس است.\",\n      task_explained:\n        \"پس از اتمام، این متن می‌تواند در ابزارهای کاری مختلف، از طریق انتخاب فایل، قرار داده شود.\",\n    },\n    \"website-depth\": {\n      name: \"ابزار جمع‌آوری لینک‌های حجمی\",\n      description:\n        \"استخراج محتوای یک وب‌سایت و لینک‌های فرعی آن تا یک سطح مشخص.\",\n      URL: \"آدرس وب‌سایت\",\n      URL_explained: \"آدرس وب‌سایتی که می‌خواهید اطلاعات آن را استخراج کنید.\",\n      depth: \"عمق خزیدن\",\n      depth_explained:\n        \"این تعداد، تعداد لینک‌های مربوط به کودکان است که کارگر باید از آدرس اصلی دنبال کند.\",\n      max_pages: \"صفحات بیشتر\",\n      max_pages_explained: \"حداکثر تعداد لینک‌هایی که باید جمع‌آوری شوند.\",\n      task_explained:\n        \"پس از اتمام، تمام محتوای جمع‌آوری‌شده در دسترس خواهد بود تا بتوان آن را در برنامه‌های کاری (یا فضاهای کاری) از طریق انتخاب اسناد، وارد کرد.\",\n    },\n    confluence: {\n      name: \"همگرایی\",\n      description: \"با یک کلیک، کل صفحه Confluence را وارد کنید.\",\n      deployment_type: \"نوع استقرار:\",\n      deployment_type_explained:\n        \"لطفاً مشخص کنید که آیا نمونه‌ی Atlassian شما در فضای ابری Atlassian یا در سرور خود میزبانی می‌شود.\",\n      base_url: \"آدرس پایه برای confluence\",\n      base_url_explained: \"این آدرس پایه برای فضای Confluence شما است.\",\n      space_key: 'کلید فضای \"کانفلوانس\"',\n      space_key_explained:\n        \"این کلید فضایی مربوط به نمونه‌ی confluence شما است که برای استفاده خواهد شد. معمولاً با ~ شروع می‌شود.\",\n      username: \"نام کاربری confluent\",\n      username_explained: \"نام کاربری شما در Confluence\",\n      auth_type: \"نوع احراز هویت: Confluence\",\n      auth_type_explained:\n        \"نوع احراز هویت مورد نظر خود را برای دسترسی به صفحات Confluence انتخاب کنید.\",\n      auth_type_username: \"نام کاربری و توکن دسترسی\",\n      auth_type_personal: \"توکن دسترسی شخصی\",\n      token: \"توکن دسترسی به confluent\",\n      token_explained_start:\n        \"شما باید یک توکن دسترسی برای احراز هویت ارائه دهید. شما می‌توانید یک توکن دسترسی ایجاد کنید.\",\n      token_explained_link: \"اینجا\",\n      token_desc: \"توکنی برای احراز هویت\",\n      pat_token: \"توکن دسترسی شخصی confluence\",\n      pat_token_explained: \"توکن دسترسی شخصی شما در Confluence.\",\n      task_explained:\n        \"پس از اتمام، محتوای صفحه برای درج در فضاهای کاری در ابزار انتخاب اسناد در دسترس خواهد بود.\",\n      bypass_ssl: \"عدم اعتبار سنجی گواهی SSL\",\n      bypass_ssl_explained:\n        \"برای دور زدن اعتبار سنجی گواهی SSL در نمونه‌های خود میزبانی شده confluence با استفاده از گواهی امضا شده توسط خود، این گزینه را فعال کنید.\",\n    },\n    manage: {\n      documents: \"اسناد\",\n      \"data-connectors\": \"اتصال‌دهنده‌ها\",\n      \"desktop-only\":\n        \"تغییر این تنظیمات تنها در دستگاه‌های دسکتاپ در دسترس است. لطفاً برای ادامه، این صفحه را در دستگاه دسکتاپ خود باز کنید.\",\n      dismiss: \"<\",\n      editing: \"ویرایش\",\n    },\n    directory: {\n      \"my-documents\": \"اسناد من\",\n      \"new-folder\": \"فোলدر جدید\",\n      \"search-document\": \"جستجو در مستند\",\n      \"no-documents\": \"بدون مدارک\",\n      \"move-workspace\": \"رفتن به فضای کاری\",\n      \"delete-confirmation\":\n        \"آیا مطمئن هستید که می‌خواهید این فایل‌ها و پوشه‌ها را حذف کنید؟\\nاین کار باعث حذف فایل‌ها از سیستم و حذف خودکار آن‌ها از هر فضای کاری موجود می‌شود.\\nاین اقدام غیرقابل بازگشت است.\",\n      \"removing-message\":\n        \"حذف {{count}} سند و {{folderCount}} پوشه. لطفاً منتظر بمانید.\",\n      \"move-success\": \"انتقال موفقیت‌آمیز {{count}} سند.\",\n      no_docs: \"بدون مدارک\",\n      select_all: \"انتخاب همه\",\n      deselect_all: \"انتخاب همه را لغو کنید\",\n      remove_selected: \"حذف انتخاب‌شده\",\n      costs: \"*هزینه یکباره برای ایجاد مدل‌های برداری\",\n      save_embed: \"ذخیره و وارد کردن\",\n      \"total-documents_one\": \"{{count}} سند\",\n      \"total-documents_other\": \"{{count}} اسناد\",\n    },\n    upload: {\n      \"processor-offline\":\n        \"دسترسی به سیستم پردازش اسناد غیر ممکن است.\\n\\nدسترسی به سیستم پردازش اسناد غیر ممکن است.\",\n      \"processor-offline-desc\":\n        \"ما نمی‌توانیم فایل‌های شما را در حال حاضر آپلود کنیم، زیرا پردازشگر اسناد غیرفعال است. لطفاً بعداً دوباره امتحان کنید.\",\n      \"click-upload\":\n        \"برای بارگذاری، روی آن کلیک کنید یا از طریق کشیدن و رها کردن\",\n      \"file-types\":\n        \"پشتیبانی از فایل‌های متنی، CSV، صفحات گسترده، فایل‌های صوتی و موارد دیگر!\",\n      \"or-submit-link\": \"یا یک لینک ارسال کنید\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"در حال دریافت...\",\n      \"fetch-website\": \"دسترسی به وب‌سایت\",\n      \"privacy-notice\":\n        \"این فایل‌ها در پردازشگر اسناد که روی این نمونه از AnythingLLM در حال اجرا است، بارگذاری خواهند شد. این فایل‌ها به هیچ شخص ثالثی ارسال یا به اشتراک گذاشته نمی‌شوند.\",\n    },\n    pinning: {\n      what_pinning: \"مخزن کردن اسناد چیست؟\",\n      pin_explained_block1:\n        'هنگامی که شما یک سند را در AnythingLLM \"فیکس\" می‌کنید، محتوای کامل سند را در پنجره دستورالعمل برای مدل زبان بزرگ شما قرار می‌دهیم تا مدل بتواند به طور کامل آن را درک کند.',\n      pin_explained_block2:\n        \"این روش بهترین نتیجه را با مدل‌هایی که دارای **زمینه وسیع** هستند یا فایل‌های کوچک و مهم که برای پایگاه دانش آن ضروری هستند، ارائه می‌دهد.\",\n      pin_explained_block3:\n        \"اگر به پاسخ‌های مورد نظر خود از AnythingLLM به طور پیش‌فرض دریافت نمی‌کنید، «پین کردن» یک راه عالی برای دریافت پاسخ‌های با کیفیت بالاتر در یک مرحله است.\",\n      accept: \"باشه، متوجه شدم.\",\n    },\n    watching: {\n      what_watching: \"تماشای یک مستند چه تاثیری دارد؟\",\n      watch_explained_block1:\n        \"هنگام مشاهده یک سند در AnythingLLM، محتوای سند به طور خودکار از منبع اصلی آن، در فواصل زمانی منظم، همگام‌سازی می‌شود. این کار، به‌طور خودکار محتوا را در هر فضای کاری که این فایل در آن مدیریت می‌شود، به‌روز می‌کند.\",\n      watch_explained_block2:\n        \"این ویژگی در حال حاضر از محتوای مبتنی بر اینترنت پشتیبانی می‌کند و برای اسناد ارسالی به صورت دستی در دسترس نخواهد بود.\",\n      watch_explained_block3_start:\n        \"می‌توانید تعیین کنید که کدام اسناد باید مشاهده شوند، از طریق\",\n      watch_explained_block3_link: \"مدیریت فایل\",\n      watch_explained_block3_end: \"مدیریت دیدگاه.\",\n      accept: \"باشه، متوجه شدم.\",\n    },\n    obsidian: {\n      vault_location: \"موقعیت گاوصندوق\",\n      vault_description:\n        'برای وارد کردن تمام یادداشت‌ها و ارتباطات آن‌ها، پوشه مربوط به \"Obsidian\" خود را انتخاب کنید.',\n      selected_files: \"کشف {{count}} فایل Markdown\",\n      importing: \"وارد کردن کپسول...\",\n      import_vault: \"وارد کردن از بایوت\",\n      processing_time: \"این ممکن است بسته به اندازه خزانه شما، مدتی طول بکشد.\",\n      vault_warning:\n        \"برای جلوگیری از هرگونه اختلاف، مطمئن شوید که دیسک Obsidian شما در حال حاضر بسته است.\",\n    },\n  },\n  chat_window: {\n    send_message: \"یک پیام ارسال کنید\",\n    attach_file: \"لطفاً یک فایل را به این چت پیوست کنید.\",\n    text_size: \"تغییر اندازه متن.\",\n    microphone: \"سوال خود را بپرسید.\",\n    send: \"پیام فوری را برای فضای کاری ارسال کنید\",\n    attachments_processing: \"در حال پردازش پیوست‌ها. لطفاً منتظر بمانید...\",\n    tts_speak_message: \"پیام TTS Speak\",\n    copy: \"کپی\",\n    regenerate: \"بازسازی\",\n    regenerate_response: \"بازسازی پاسخ\",\n    good_response: \"پاسخ خوب\",\n    more_actions: \"اقدامات بیشتر\",\n    fork: \"چنگال\",\n    delete: \"حذف\",\n    cancel: \"ยกد\",\n    edit_prompt: \"لطفاً دستور ویرایش را ارائه دهید.\",\n    edit_response: \"لطفا پاسخ را ویرایش کنید.\",\n    preset_reset_description: \"حذف تاریخچه چت خود و شروع یک چت جدید\",\n    add_new_preset: \"اضافه کردن تنظیمات پیش‌فرض جدید\",\n    command: \"دستورالعمل\",\n    your_command: \"دستور شما\",\n    placeholder_prompt:\n      \"این محتوایی است که در ابتدای درخواست شما قرار خواهد گرفت.\",\n    description: \"توضیحات\",\n    placeholder_description: \"با شعر درباره مدل‌های زبانی بزرگ پاسخ می‌دهد.\",\n    save: \"ذخیره\",\n    small: \"کوچک\",\n    normal: \"عادی\",\n    large: \"بزرگ\",\n    workspace_llm_manager: {\n      search: \"پیدا کردن ارائه‌دهندگان مدل‌های زبانی بزرگ (LLM)\",\n      loading_workspace_settings: \"بارگذاری تنظیمات فضای کاری...\",\n      available_models: \"مدل‌های موجود برای {{provider}}\",\n      available_models_description:\n        \"یک مدل را برای استفاده در این محیط کاری انتخاب کنید.\",\n      save: \"از این مدل استفاده کنید.\",\n      saving: \"تنظیم مدل به عنوان پیش‌فرض فضای کاری...\",\n      missing_credentials: \"این ارائه دهنده فاقد مدارک لازم است!\",\n      missing_credentials_description:\n        \"برای تنظیم اعتبارها، اینجا را کلیک کنید\",\n    },\n    submit: \"ارسال\",\n    edit_info_user:\n      '\"ارسال\" پاسخ تولید شده توسط هوش مصنوعی را دوباره ایجاد می‌کند. \"ذخیره\" فقط پیام شما را به‌روز می‌کند.',\n    edit_info_assistant: \"تغییرات شما مستقیماً در این پاسخ ذخیره خواهند شد.\",\n    see_less: \"کمی بیشتر\",\n    see_more: \"بیشتر\",\n    tools: \"ابزارها\",\n    browse: \"جستجو\",\n    text_size_label: \"اندازه متن\",\n    select_model: \"انتخاب مدل\",\n    sources: \"منابع\",\n    document: \"اسناد\",\n    similarity_match: \"مسابقه\",\n    source_count_one: \"{{count}}، مرجع\",\n    source_count_other: \"{{count}}، منابع\",\n    preset_exit_description: \"متوقف کردن جلسه فعلی با نمایندگی\",\n    add_new: \"اضافه کردن موارد جدید\",\n    edit: \"ویرایش\",\n    publish: \"انتشار\",\n    stop_generating: \"متوقف کردن تولید پاسخ\",\n    pause_tts_speech_message: \"مکث در پخش صدای متن\",\n    slash_commands: \"دستورات کوتاه‌شده\",\n    agent_skills: \"مهارت‌های کارگزار\",\n    manage_agent_skills: \"مدیریت مهارت‌های نمایندگان\",\n    agent_skills_disabled_in_session:\n      \"امکان تغییر مهارت‌ها در حین یک جلسه فعال با یک عامل وجود ندارد. ابتدا با استفاده از دستور /exit، جلسه را به پایان برسانید.\",\n    start_agent_session: \"شروع جلسه با نماینده\",\n    use_agent_session_to_use_tools:\n      \"شما می‌توانید از ابزارهای موجود در چت با شروع یک جلسه با یک عامل از طریق استفاده از '@agent' در ابتدای پیام خود استفاده کنید.\",\n  },\n  profile_settings: {\n    edit_account: \"ویرایش حساب\",\n    profile_picture: \"تصویر پروفایل\",\n    remove_profile_picture: \"حذف تصویر پروفایل\",\n    username: \"نام کاربری\",\n    new_password: \"رمز عبور جدید\",\n    password_description: \"رمز عبور باید حداقل 8 کاراکتر طول داشته باشد.\",\n    cancel: \"ยกد\",\n    update_account: \"به‌روزرسانی حساب\",\n    theme: \"ترجیحات موضوعی\",\n    language: \"زبان ترجیحی\",\n    failed_upload: \"عدم امکان بارگذاری تصویر پروفایل: {{error}}\",\n    upload_success: \"تصویر پروفایل آپلود شد.\",\n    failed_remove: \"عدم امکان حذف تصویر پروفایل: {{error}}\",\n    profile_updated: \"صفحه به‌روز شد.\",\n    failed_update_user: \"عدم به‌روزرسانی کاربر: {{error}}\",\n    account: \"حساب\",\n    support: \"حمایت\",\n    signout: \"خروج\",\n  },\n  customization: {\n    interface: {\n      title: \"تنظیمات رابط کاربری\",\n      description: \"تنظیمات رابط کاربری خود را برای AnythingLLM تعیین کنید.\",\n    },\n    branding: {\n      title: \"برندسازی و ارائه خدمات با برچسب سفید\",\n      description:\n        \"با استفاده از برندسازی سفارشی، نمونه‌ی AnythingLLM خود را با برچسب سفید (White-label) ارائه دهید.\",\n    },\n    chat: {\n      title: \"چت\",\n      description: \"تنظیم ترجیحات چت خود برای AnythingLLM.\",\n      auto_submit: {\n        title: \"وارد کردن خودکار گفتار\",\n        description: \"ارسال خودکار ورودی گفتار پس از یک دوره سکوت\",\n      },\n      auto_speak: {\n        title: \"پاسخ‌های خودکار\",\n        description: \"پاسخ‌های خودکار تولید شده توسط هوش مصنوعی\",\n      },\n      spellcheck: {\n        title: \"فعال کردن بررسی املایی\",\n        description: \"فعال یا غیرفعال کردن بررسی املایی در فیلد ورودی چت\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"موضوع\",\n        description: \"رنگ مورد علاقه خود را برای برنامه انتخاب کنید.\",\n      },\n      \"show-scrollbar\": {\n        title: \"نمایش نوار پیمایش\",\n        description: \"فعال یا غیرفعال کردن نوار پیمایش در پنجره چت.\",\n      },\n      \"support-email\": {\n        title: \"پشتیبانی از طریق ایمیل\",\n        description:\n          \"آدرس ایمیل پشتیبانی را تعیین کنید که کاربران در صورت نیاز به کمک، می‌توانند از آن استفاده کنند.\",\n      },\n      \"app-name\": {\n        title: \"نام\",\n        description: \"یک نام را برای تمام کاربران در صفحه ورود مشخص کنید.\",\n      },\n      \"display-language\": {\n        title: \"زبان نمایش\",\n        description:\n          \"زبان مورد نظر برای نمایش رابط کاربری AnythingLLM را انتخاب کنید - در صورت وجود ترجمه‌ها.\",\n      },\n      logo: {\n        title: \"لوگوی برند\",\n        description:\n          \"لوگوی سفارشی خود را برای نمایش در تمام صفحات بارگذاری کنید.\",\n        add: \"اضافه کردن یک لوگوی سفارشی\",\n        recommended: \"اندازه پیشنهادی: 800 در 200\",\n        remove: \"حذف\",\n        replace: \"جایگزین کردن\",\n      },\n      \"browser-appearance\": {\n        title: \"ظاهر مرورگر\",\n        description:\n          \"ظاهر تب و عنوان مرورگر را هنگام باز بودن برنامه، سفارشی کنید.\",\n        tab: {\n          title: \"عنوان\",\n          description:\n            \"هنگام باز شدن برنامه در یک مرورگر، یک عنوان سفارشی برای تب تنظیم کنید.\",\n        },\n        favicon: {\n          title: \"آیکون Favicon\",\n          description: \"از آیکون سفارشی برای تب مرورگر استفاده کنید.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"عناصر پایینی نوار کناری\",\n        description: \"تنظیم عناصر پاورهای نمایش داده شده در پایین بخش کناری.\",\n        icon: \"آیکون\",\n        link: \"لینک\",\n      },\n      \"render-html\": {\n        title: \"نمایش کد HTML در چت\",\n        description:\n          \"ارائه پاسخ‌های HTML در پاسخ‌های دستی.\\nاین می‌تواند منجر به کیفیت پاسخ با سطح دقت بسیار بالاتر شود، اما همچنین می‌تواند خطرات امنیتی بالقوه‌ای را به همراه داشته باشد.\",\n      },\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"ایجاد یک عامل\",\n      editWorkspace: \"ویرایش فضای کاری\",\n      uploadDocument: \"بارگذاری یک سند\",\n    },\n    greeting: \"امروز چگونه می‌توانم به شما کمک کنم؟\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"کلیدهای میانبر\",\n    shortcuts: {\n      settings: \"تنظیمات را باز کنید\",\n      workspaceSettings: \"تنظیمات فضای کاری فعلی را باز کنید\",\n      home: \"بازگشت به صفحه اصلی\",\n      workspaces: \"مدیریت فضاهای کاری\",\n      apiKeys: \"تنظیمات کلیدهای API\",\n      llmPreferences: \"ترجیحات مدل‌های زبان بزرگ\",\n      chatSettings: \"تنظیمات چت\",\n      help: \"راهنمای کلیدهای میانبر\",\n      showLLMSelector: \"انتخاب فضای کاری برای مدل‌های زبانی بزرگ\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"موفقیت!\",\n        success_description: 'پیام شما در بخش \"انجمن\" منتشر شده است!',\n        success_thank_you: \"از اینکه با جامعه به اشتراک گذاشتید، سپاسگزاریم!\",\n        view_on_hub: \"مشاهده در مرکز جامعه\",\n        modal_title: \"دستورالعمل انتشار\",\n        name_label: \"نام\",\n        name_description: \"این نام نمایش برای سیستم شما است.\",\n        name_placeholder: \"دستورالعمل سیستم من\",\n        description_label: \"توضیحات\",\n        description_description:\n          \"این، توضیحی برای دستورالعمل سیستم شما است. از این برای توضیح هدف دستورالعمل سیستم خود استفاده کنید.\",\n        tags_label: \"برچسب‌ها\",\n        tags_description:\n          \"برچسب‌ها برای شناسایی و جستجوی آسان‌تر دستورالعمل‌های سیستم استفاده می‌شوند. شما می‌توانید چندین برچسب را اضافه کنید. حداکثر 5 برچسب. حداکثر 20 کاراکتر برای هر برچسب.\",\n        tags_placeholder:\n          \"برای افزودن برچسب‌ها، نوع را وارد کنید و Enter را بزنید.\",\n        visibility_label: \"دیده‌شدن\",\n        public_description: \"پیام‌های عمومی در دسترس همه افراد قرار دارند.\",\n        private_description: \"پیام‌های خصوصی فقط برای شما قابل مشاهده هستند.\",\n        publish_button: \"انتشار در مرکز جامعه\",\n        submitting: \"انتشار...\",\n        prompt_label: \"شروع\",\n        prompt_description:\n          \"این دستورالعمل اصلی است که برای هدایت مدل زبان بزرگ (LLM) استفاده خواهد شد.\",\n        prompt_placeholder: \"لطفاً دستور خود را در اینجا وارد کنید...\",\n      },\n      agent_flow: {\n        success_title: \"موفقیت!\",\n        success_description:\n          'پلتفرم \"Agent Flow\" شما در مرکز جامعه منتشر شده است!',\n        success_thank_you: \"از اینکه با جامعه به اشتراک گذاشتید، سپاسگزاریم!\",\n        view_on_hub: \"مشاهده در مرکز جامعه\",\n        modal_title: \"آژانس انتشار\",\n        name_label: \"نام\",\n        name_description: \"این نام نمایش برای جریان کاری شما است.\",\n        name_placeholder: \"آژانس من\",\n        description_label: \"توضیحات\",\n        description_description:\n          \"این، شرح جریان کاری شما است. از این برای توضیح هدف جریان کاری خود استفاده کنید.\",\n        tags_label: \"برچسب‌ها\",\n        tags_description:\n          \"برچسب‌ها برای شناسایی و سازماندهی جریان‌های کاری خود به منظور جستجوی آسان‌تر استفاده می‌شوند. شما می‌توانید چندین برچسب را اضافه کنید. حداکثر 5 برچسب. حداکثر 20 کاراکتر برای هر برچسب.\",\n        tags_placeholder:\n          \"برای افزودن برچسب‌ها، نوع را وارد کنید و Enter را فشار دهید.\",\n        visibility_label: \"دیده‌شدن\",\n        submitting: \"انتشار...\",\n        submit: \"انتشار در مرکز جامعه\",\n        privacy_note:\n          \"جریان‌ها همیشه به صورت خصوصی بارگذاری می‌شوند تا از هرگونه اطلاعات حساس محافظت شود. شما می‌توانید پس از انتشار، قابلیت مشاهده را در مرکز جامعه تغییر دهید. لطفاً قبل از انتشار، از این نکته اطمینان حاصل کنید که جریان شما حاوی هیچ اطلاعات حساس یا خصوصی نیست.\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"احراز هویت الزامی است\",\n          description:\n            \"شما باید قبل از انتشار مطالب، با مرکز جامعه AnythingLLM احراز هویت کنید.\",\n          button: \"اتصال به مرکز جامعه\",\n        },\n      },\n      slash_command: {\n        success_title: \"موفقیت!\",\n        success_description: \"دستور Slash شما در مرکز جامعه منتشر شده است!\",\n        success_thank_you: \"از اینکه با جامعه به اشتراک گذاشتید، سپاسگزاریم!\",\n        view_on_hub: \"مشاهده در مرکز جامعه\",\n        modal_title: \"انتشار دستور Slash\",\n        name_label: \"نام\",\n        name_description: \"این نام نمایش برای دستورslash شما است.\",\n        name_placeholder: \"دستور Slash من\",\n        description_label: \"توضیحات\",\n        description_description:\n          \"این، توضیحی برای دستور slash شما است. از این برای توضیح هدف دستور slash خود استفاده کنید.\",\n        tags_label: \"برچسب‌ها\",\n        tags_description:\n          \"برچسب‌ها برای شناسایی دستورات Slash Command به منظور جستجوی آسان‌تر استفاده می‌شوند. شما می‌توانید چندین برچسب را اضافه کنید. حداکثر 5 برچسب. حداکثر 20 کاراکتر برای هر برچسب.\",\n        tags_placeholder:\n          \"برای افزودن برچسب‌ها، نوع را وارد کنید و Enter را بزنید.\",\n        visibility_label: \"دیده‌شدن\",\n        public_description: \"دستورات عمومی در دسترس همه کاربران است.\",\n        private_description: \"دستورات خصوصی فقط برای شما قابل مشاهده هستند.\",\n        publish_button: \"انتشار در مرکز جامعه\",\n        submitting: \"انتشار...\",\n        prompt_label: \"شروع\",\n        prompt_description:\n          \"این دستور، زمانی استفاده می‌شود که دستور با خط (slash command) فعال شود.\",\n        prompt_placeholder: \"لطفاً درخواست خود را در اینجا وارد کنید...\",\n      },\n    },\n  },\n  security: {\n    title: \"امنیت\",\n    multiuser: {\n      title: \"حالت چند کاربره\",\n      description:\n        \"نمونه خود را برای پشتیبانی از تیم خود با فعال‌سازی حالت چند کاربره تنظیم کنید.\",\n      enable: {\n        \"is-enable\": \"حالت چند کاربره فعال است\",\n        enable: \"فعال‌سازی حالت چند کاربره\",\n        description:\n          \"به طور پیش‌فرض، شما تنها مدیر خواهید بود. به عنوان مدیر، باید برای تمام کاربران یا مدیران جدید حساب کاربری ایجاد کنید. رمز عبور خود را گم نکنید زیرا فقط یک کاربر مدیر می‌تواند رمزهای عبور را بازنشانی کند.\",\n        username: \"نام کاربری حساب مدیر\",\n        password: \"رمز عبور حساب مدیر\",\n      },\n    },\n    password: {\n      title: \"حفاظت با رمز عبور\",\n      description:\n        \"از نمونه AnythingLLM خود با رمز عبور محافظت کنید. اگر این رمز را فراموش کنید هیچ روش بازیابی وجود ندارد، پس حتماً این رمز عبور را ذخیره کنید.\",\n      \"password-label\": \"رمز عبور نمونه\",\n    },\n  },\n  home: {\n    welcome: \"خوش آمدید\",\n    chooseWorkspace: \"انتخاب یک فضای کار برای شروع گفتگو!\",\n    notAssigned:\n      \"شما در حال حاضر به هیچ فضای کاری اختصاص نیافته‌اید.\\nلطفاً با مدیر خود تماس بگیرید تا دسترسی به یک فضای کار را درخواست کنید.\",\n    goToWorkspace: 'به فضای کار \"{{workspace}}\" بروید',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/findUnusedTranslations.mjs",
    "content": "/* global process */\n// This script finds translation keys defined in en/common.js that are not\n// referenced anywhere in the frontend source code. It exits with code 1\n// when unused keys are found so it can be used as a CI check.\n//\n// Usage:\n//   node findUnusedTranslations.mjs            # Report unused keys\n//   node findUnusedTranslations.mjs --delete   # Remove unused keys from en/common.js\nimport fs from \"fs\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { resources } from \"./resources.js\";\nimport DYNAMIC_KEY_ALLOWLIST from \"./dynamicKeyAllowlist.js\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst FRONTEND_SRC = path.resolve(__dirname, \"..\");\nconst shouldDelete = process.argv.includes(\"--delete\");\n\n// ---------------------------------------------------------------------------\n// 1. Extract all dot-path keys from the English translation object\n// ---------------------------------------------------------------------------\nfunction extractKeys(obj, prefix = \"\") {\n  const keys = [];\n  for (const [key, value] of Object.entries(obj)) {\n    const dotPath = prefix ? `${prefix}.${key}` : key;\n    if (value && typeof value === \"object\") {\n      keys.push(...extractKeys(value, dotPath));\n    } else {\n      keys.push(dotPath);\n    }\n  }\n  return keys;\n}\n\nconst definedKeys = new Set(extractKeys(resources.en.common));\n\n// ---------------------------------------------------------------------------\n// 2. Collect all frontend source files, excluding locales/\n// ---------------------------------------------------------------------------\nfunction collectFiles(dir, results = []) {\n  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n    const full = path.join(dir, entry.name);\n    if (entry.isDirectory()) {\n      // Skip the locales directory to avoid self-references\n      if (entry.name === \"locales\") continue;\n      collectFiles(full, results);\n    } else if (/\\.(js|jsx)$/.test(entry.name)) {\n      results.push(full);\n    }\n  }\n  return results;\n}\n\nconst sourceFiles = collectFiles(FRONTEND_SRC);\n\n// ---------------------------------------------------------------------------\n// 3. Scan source files for t() references (literal and dynamic)\n// ---------------------------------------------------------------------------\nconst referencedKeys = new Set();\nconst tCallRegex = /\\bt\\(\\s*[\"'`]([^\"'`]+)[\"'`]/g;\nconst dynamicTCallRegex = /\\bt\\(\\s*([a-zA-Z_$][a-zA-Z0-9_$.]*)\\s*[,)]/g;\nconst templateTCallRegex = /\\bt\\(\\s*`([^`]*\\$\\{[^`]*)`\\s*[,)]/g;\nconst dynamicUsages = [];\n\nfor (const file of sourceFiles) {\n  const content = fs.readFileSync(file, \"utf-8\");\n\n  let match;\n  while ((match = tCallRegex.exec(content)) !== null) {\n    referencedKeys.add(match[1]);\n  }\n\n  while ((match = dynamicTCallRegex.exec(content)) !== null) {\n    const arg = match[1];\n    if (/^[\"'`]/.test(arg)) continue;\n    dynamicUsages.push({ file: path.relative(FRONTEND_SRC, file), arg });\n  }\n\n  while ((match = templateTCallRegex.exec(content)) !== null) {\n    dynamicUsages.push({\n      file: path.relative(FRONTEND_SRC, file),\n      arg: `\\`${match[1]}\\``,\n    });\n  }\n}\n\nif (dynamicUsages.length > 0) {\n  console.log(\n    `\\nWarning: Found ${dynamicUsages.length} dynamic t() call(s) that cannot be statically analyzed:\\n`\n  );\n  for (const { file, arg } of dynamicUsages) {\n    console.log(`  ${file}: t(${arg})`);\n  }\n  console.log(\n    `\\nIf these reference valid keys, add them to locales/dynamicKeyAllowlist.js to prevent accidental deletion.\\n`\n  );\n}\n\n// ---------------------------------------------------------------------------\n// 4. Diff and report (excluding allowlisted keys and i18n plural forms)\n// ---------------------------------------------------------------------------\nconst allowlist = new Set(DYNAMIC_KEY_ALLOWLIST);\nconst PLURAL_SUFFIXES = [\"_zero\", \"_one\", \"_two\", \"_few\", \"_many\", \"_other\"];\n\nfunction isPluralFormOfReferencedKey(key) {\n  for (const suffix of PLURAL_SUFFIXES) {\n    if (key.endsWith(suffix)) {\n      const baseKey = key.slice(0, -suffix.length);\n      if (referencedKeys.has(baseKey)) return true;\n    }\n  }\n  return false;\n}\n\nconst unusedKeys = [...definedKeys]\n  .filter(\n    (key) =>\n      !referencedKeys.has(key) &&\n      !allowlist.has(key) &&\n      !isPluralFormOfReferencedKey(key)\n  )\n  .sort();\n\nif (unusedKeys.length === 0) {\n  console.log(\"👍 All translation keys are referenced in the source code!\");\n  process.exit(0);\n}\n\nconsole.log(`Found ${unusedKeys.length} unused translation key(s):\\n`);\nfor (const key of unusedKeys) {\n  console.log(`  • ${key}`);\n}\n\nif (!shouldDelete) {\n  console.log(\n    `\\nThese keys are defined in en/common.js but not referenced in any source file.`\n  );\n  console.log(`Run with --delete to remove them from en/common.js.`);\n  process.exit(1);\n}\n\n// ---------------------------------------------------------------------------\n// 5. Delete unused keys from en/common.js\n// ---------------------------------------------------------------------------\nfunction deleteKey(obj, dotPath) {\n  const parts = dotPath.split(\".\");\n  let current = obj;\n  for (let i = 0; i < parts.length - 1; i++) {\n    if (!current[parts[i]] || typeof current[parts[i]] !== \"object\") return;\n    current = current[parts[i]];\n  }\n  delete current[parts[parts.length - 1]];\n}\n\nfunction pruneEmptyObjects(obj) {\n  for (const key of Object.keys(obj)) {\n    if (obj[key] && typeof obj[key] === \"object\") {\n      pruneEmptyObjects(obj[key]);\n      if (Object.keys(obj[key]).length === 0) delete obj[key];\n    }\n  }\n}\n\nconst translations = structuredClone(resources.en.common);\nfor (const key of unusedKeys) {\n  deleteKey(translations, key);\n}\npruneEmptyObjects(translations);\n\nconst filePath = path.join(__dirname, \"en\", \"common.js\");\nconst output = `const TRANSLATIONS = ${JSON.stringify(translations, null, 2)}\n\nexport default TRANSLATIONS;\\n`;\nfs.writeFileSync(filePath, output, \"utf-8\");\n\nconsole.log(\n  `\\n✅ Deleted ${unusedKeys.length} unused key(s) from en/common.js.`\n);\nprocess.exit(0);\n"
  },
  {
    "path": "frontend/src/locales/fr/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    survey: {\n      email: \"Adresse e-mail\",\n      useCase: \"Pour quel usage utiliserez-vous AnythingLLM ?\",\n      useCaseWork: \"Pour le travail\",\n      useCasePersonal: \"Pour un usage personnel\",\n      useCaseOther: \"Autre\",\n      comment: \"Comment avez-vous découvert AnythingLLM ?\",\n      commentPlaceholder: \"Recherche, recommandation, Twitter, YouTube, etc.\",\n      skip: \"Ignorer l'enquête\",\n      thankYou: \"Merci pour votre retour !\",\n      title: \"Bienvenue\",\n      description:\n        \"Aidez-nous à améliorer AnythingLLM en répondant à quelques questions.\",\n    },\n    home: {\n      title: \"Bienvenue\",\n      getStarted: \"Commencer\",\n    },\n    llm: {\n      title: \"Préférence LLM\",\n      description:\n        \"AnythingLLM peut fonctionner avec de nombreux fournisseurs LLM. Ce sera le service qui traitera vos discussions.\",\n    },\n    userSetup: {\n      title: \"Configuration utilisateur\",\n      description: \"Configurez votre accès utilisateur.\",\n      howManyUsers: \"Combien de personnes utiliseront cette instance ?\",\n      justMe: \"Juste moi\",\n      myTeam: \"Mon équipe\",\n      instancePassword: \"Mot de passe de l'instance\",\n      setPassword: \"Définir un mot de passe\",\n      passwordReq: \"Le mot de passe doit contenir au moins 8 caractères.\",\n      passwordWarn:\n        \"Conservez ce mot de passe, il n'y a pas de récupération possible.\",\n      adminUsername: \"Nom d'utilisateur administrateur\",\n      adminPassword: \"Mot de passe administrateur\",\n      adminPasswordReq: \"Le mot de passe doit contenir au moins 8 caractères.\",\n      teamHint:\n        \"Vous pourrez ajouter d'autres utilisateurs après la configuration initiale.\",\n    },\n    data: {\n      title: \"Gestion des données\",\n      description:\n        \"Configurez comment AnythingLLM stocke et traite vos données.\",\n      settingsHint:\n        \"Ces paramètres peuvent être modifiés ultérieurement dans les paramètres.\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Nom des espaces de travail\",\n    user: \"Utilisateur\",\n    selection: \"Sélection du modèle\",\n    saving: \"Enregistrement...\",\n    save: \"Enregistrer les modifications\",\n    previous: \"Page précédente\",\n    next: \"Page suivante\",\n    optional: \"Optionnel\",\n    yes: \"Oui\",\n    no: \"Non\",\n    search: \"Rechercher\",\n    username_requirements:\n      \"Le nom d'utilisateur doit comporter entre 2 et 32 caractères, commencer par une lettre minuscule et ne contenir que des lettres minuscules, des chiffres, des tirets bas, des tirets et des points.\",\n    on: \"Sur\",\n    none: \"Aucun\",\n    stopped: \"Arrêté\",\n    loading: \"Chargement\",\n    refresh: \"Rafraîchir\",\n  },\n  settings: {\n    title: \"Paramètres de l'instance\",\n    invites: \"Invitation\",\n    users: \"Utilisateurs\",\n    workspaces: \"Espaces de travail\",\n    \"workspace-chats\": \"Chat de l'espace de travail\",\n    customization: \"Apparence\",\n    \"api-keys\": \"Clés API\",\n    llm: \"Préférence LLM\",\n    transcription: \"Modèle de transcription\",\n    embedder: \"Préférences d'intégration\",\n    \"text-splitting\": \"Diviseur de texte et découpage\",\n    \"voice-speech\": \"Voix et Parole\",\n    \"vector-database\": \"Base de données vectorielle\",\n    embeds: \"Widgets de chat intégrés\",\n    security: \"Sécurité\",\n    \"event-logs\": \"Journaux d'événements\",\n    privacy: \"Confidentialité et données\",\n    \"ai-providers\": \"Fournisseurs d'IA\",\n    \"agent-skills\": \"Compétences de l'agent\",\n    admin: \"Admin\",\n    tools: \"Outils\",\n    \"experimental-features\": \"Fonctionnalités Expérimentales\",\n    contact: \"Contacter le Support\",\n    \"browser-extension\": \"Extension de navigateur\",\n    \"system-prompt-variables\": \"Variables de prompt système\",\n    interface: \"Interface\",\n    branding: \"Personnalisation\",\n    chat: \"Chat\",\n    \"mobile-app\": \"AnythingLLM Mobile\",\n    \"community-hub\": {\n      title: \"Centre communautaire\",\n      trending: \"Découvrez les tendances\",\n      \"your-account\": \"Votre compte\",\n      \"import-item\": \"Importer\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Bienvenue\",\n      \"placeholder-username\": \"Nom d'utilisateur\",\n      \"placeholder-password\": \"Mot de passe\",\n      login: \"Connexion\",\n      validating: \"Validation...\",\n      \"forgot-pass\": \"Mot de passe oublié\",\n      reset: \"Réinitialiser\",\n    },\n    \"sign-in\": \"Connectez-vous à votre compte {{appName}}.\",\n    \"password-reset\": {\n      title: \"Réinitialisation du mot de passe\",\n      description:\n        \"Fournissez les informations nécessaires ci-dessous pour réinitialiser votre mot de passe.\",\n      \"recovery-codes\": \"Codes de récupération\",\n      \"back-to-login\": \"Retour à la connexion\",\n    },\n  },\n  \"new-workspace\": {\n    title: \"Nouvel Espace de Travail\",\n    placeholder: \"Mon Espace de Travail\",\n  },\n  \"workspaces—settings\": {\n    general: \"Paramètres généraux\",\n    chat: \"Paramètres du chat\",\n    vector: \"Base de données vectorielle\",\n    members: \"Membres\",\n    agent: \"Configuration de l'agent\",\n  },\n  general: {\n    vector: {\n      title: \"Nombre de vecteurs\",\n      description:\n        \"Nombre total de vecteurs dans votre base de données vectorielle.\",\n    },\n    names: {\n      description:\n        \"Cela ne changera que le nom d'affichage de votre espace de travail.\",\n    },\n    message: {\n      title: \"Messages de chat suggérés\",\n      description:\n        \"Personnalisez les messages qui seront suggérés aux utilisateurs de votre espace de travail.\",\n      add: \"Ajouter un nouveau message\",\n      save: \"Enregistrer les messages\",\n      heading: \"Expliquez-moi\",\n      body: \"les avantages de AnythingLLM\",\n    },\n    delete: {\n      title: \"Supprimer l'Espace de Travail\",\n      description:\n        \"Supprimer cet espace de travail et toutes ses données. Cela supprimera l'espace de travail pour tous les utilisateurs.\",\n      delete: \"Supprimer l'espace de travail\",\n      deleting: \"Suppression de l'espace de travail...\",\n      \"confirm-start\": \"Vous êtes sur le point de supprimer votre\",\n      \"confirm-end\":\n        \"espace de travail. Cela supprimera toutes les intégrations vectorielles dans votre base de données vectorielle.\\n\\nLes fichiers source originaux resteront intacts. Cette action est irréversible.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Fournisseur LLM de l'espace de travail\",\n      description:\n        \"Le fournisseur et le modèle LLM spécifiques qui seront utilisés pour cet espace de travail. Par défaut, il utilise le fournisseur et les paramètres LLM du système.\",\n      search: \"Rechercher tous les fournisseurs LLM\",\n    },\n    model: {\n      title: \"Modèle de chat de l'espace de travail\",\n      description:\n        \"Le modèle de chat spécifique qui sera utilisé pour cet espace de travail. Si vide, utilisera la préférence LLM du système.\",\n    },\n    mode: {\n      title: \"Mode de chat\",\n      chat: {\n        title: \"Chat\",\n        description:\n          'fournira des réponses en utilisant les connaissances générales du LLM et le contexte du document correspondant. <br />Vous devrez utiliser la commande \"@agent\" pour utiliser les outils.',\n      },\n      query: {\n        title: \"Requête\",\n        description:\n          \"fournira des réponses <b>uniquement</b> si le contexte du document est trouvé.<br />Vous devrez utiliser la commande @agent pour utiliser les outils.\",\n      },\n      automatic: {\n        title: \"Voiture\",\n        description:\n          \"utilisera automatiquement les outils si le modèle et le fournisseur prennent en charge l'appel de outils natifs. <br />Si l'utilisation d'outils natifs n'est pas prise en charge, vous devrez utiliser la commande \\\"@agent\\\" pour utiliser les outils.\",\n      },\n    },\n    history: {\n      title: \"Historique des chats\",\n      \"desc-start\":\n        \"Le nombre de chats précédents qui seront inclus dans la mémoire à court terme de la réponse.\",\n      recommend: \"Recommandé: 20.\",\n      \"desc-end\":\n        \"Tout nombre supérieur à 45 risque de provoquer des échecs de chat continus en fonction de la taille du message.\",\n    },\n    prompt: {\n      title: \"Invite\",\n      description:\n        \"L'invite qui sera utilisée sur cet espace de travail. Définissez le contexte et les instructions pour que l'IA génère une réponse. Vous devez fournir une invite soigneusement conçue pour que l'IA puisse générer une réponse pertinente et précise.\",\n      history: {\n        title: \"Historique des prompts\",\n        clearAll: \"Tout effacer\",\n        noHistory: \"Aucun historique\",\n        restore: \"Restaurer\",\n        delete: \"Supprimer\",\n        deleteConfirm: \"Êtes-vous sûr de vouloir supprimer ce prompt ?\",\n        clearAllConfirm: \"Êtes-vous sûr de vouloir effacer tout l'historique ?\",\n        expand: \"Développer\",\n        publish: \"Publier\",\n      },\n    },\n    refusal: {\n      title: \"Réponse de refus en mode requête\",\n      \"desc-start\": \"En mode\",\n      query: \"requête\",\n      \"desc-end\":\n        \", vous pouvez souhaiter retourner une réponse de refus personnalisée lorsque aucun contexte n'est trouvé.\",\n      \"tooltip-title\": \"Personnaliser la réponse de refus\",\n      \"tooltip-description\":\n        \"Personnalisez la réponse qui sera affichée lorsque aucun contexte pertinent n'est trouvé dans vos documents.\",\n    },\n    temperature: {\n      title: \"Température LLM\",\n      \"desc-start\":\n        \"Ce paramètre contrôle le niveau de créativité des réponses de votre LLM.\",\n      \"desc-end\":\n        \"Plus le nombre est élevé, plus la réponse sera créative. Pour certains modèles, cela peut entraîner des réponses incohérentes si la valeur est trop élevée.\",\n      hint: \"La plupart des LLM ont diverses plages acceptables de valeurs valides. Consultez votre fournisseur LLM pour cette information.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Identifiant de la base de données vectorielle\",\n    snippets: {\n      title: \"Nombre maximum de contextes\",\n      description:\n        \"Ce paramètre contrôle le nombre maximum de contextes qui seront envoyés au LLM par chat ou requête.\",\n      recommend: \"Recommandé: 4\",\n    },\n    doc: {\n      title: \"Seuil de similarité des documents\",\n      description:\n        \"Le score de similarité minimum requis pour qu'une source soit considérée comme liée au chat. Plus le nombre est élevé, plus la source doit être similaire au chat.\",\n      zero: \"Aucune restriction\",\n      low: \"Bas (score de similarité ≥ .25)\",\n      medium: \"Moyen (score de similarité ≥ .50)\",\n      high: \"Élevé (score de similarité ≥ .75)\",\n    },\n    reset: {\n      reset: \"Réinitialiser la base de données vectorielle\",\n      resetting: \"Effacement des vecteurs...\",\n      confirm:\n        \"Vous êtes sur le point de réinitialiser la base de données vectorielle de cet espace de travail. Cela supprimera toutes les intégrations vectorielles actuellement intégrées.\\n\\nLes fichiers source originaux resteront intacts. Cette action est irréversible.\",\n      error:\n        \"La base de données vectorielle de l'espace de travail n'a pas pu être réinitialisée !\",\n      success:\n        \"La base de données vectorielle de l'espace de travail a été réinitialisée !\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"La performance des LLM qui ne supportent pas explicitement l'appel d'outils dépend fortement des capacités et de la précision du modèle. Certaines capacités peuvent être limitées ou non fonctionnelles.\",\n    provider: {\n      title: \"Fournisseur LLM de l'agent de l'espace de travail\",\n      description:\n        \"Le fournisseur et le modèle LLM spécifiques qui seront utilisés pour l'agent @agent de cet espace de travail.\",\n    },\n    mode: {\n      chat: {\n        title: \"Modèle de chat de l'agent de l'espace de travail\",\n        description:\n          \"Le modèle de chat spécifique qui sera utilisé pour l'agent @agent de cet espace de travail.\",\n      },\n      title: \"Modèle de l'agent de l'espace de travail\",\n      description:\n        \"Le modèle LLM spécifique qui sera utilisé pour l'agent @agent de cet espace de travail.\",\n      wait: \"-- en attente des modèles --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG et mémoire à long terme\",\n        description:\n          \"Permettez à l'agent de s'appuyer sur vos documents locaux pour répondre à une requête ou demandez à l'agent de se souvenir de morceaux de contenu pour la récupération de mémoire à long terme.\",\n      },\n      view: {\n        title: \"Voir et résumer des documents\",\n        description:\n          \"Permettez à l'agent de lister et de résumer le contenu des fichiers de l'espace de travail actuellement intégrés.\",\n      },\n      scrape: {\n        title: \"Récupérer des sites web\",\n        description:\n          \"Permettez à l'agent de visiter et de récupérer le contenu des sites web.\",\n      },\n      generate: {\n        title: \"Générer des graphiques\",\n        description:\n          \"Activez l'agent par défaut pour générer différents types de graphiques à partir des données fournies ou données dans le chat.\",\n      },\n      save: {\n        title: \"Générer et sauvegarder des fichiers dans le navigateur\",\n        description:\n          \"Activez l'agent par défaut pour générer et écrire des fichiers qui peuvent être sauvegardés et téléchargés dans votre navigateur.\",\n      },\n      web: {\n        title: \"Recherche web en direct et navigation\",\n        description:\n          \"Permettez à votre agent de rechercher sur le web pour répondre à vos questions en vous connectant à un fournisseur de recherche web (SERP).\",\n      },\n      sql: {\n        title: \"Connecteur SQL\",\n        description:\n          \"Permettez à votre agent d'utiliser SQL pour répondre à vos questions en lui fournissant un accès à divers fournisseurs de bases de données SQL.\",\n      },\n      default_skill:\n        \"Par défaut, cette fonctionnalité est activée, mais vous pouvez la désactiver si vous ne souhaitez pas qu'elle soit disponible pour l'agent.\",\n    },\n    mcp: {\n      title: \"Serveurs MCP\",\n      \"loading-from-config\":\n        \"Chargement des serveurs MCP à partir du fichier de configuration\",\n      \"learn-more\": \"En savoir plus sur les serveurs MCP.\",\n      \"no-servers-found\": \"Aucun serveur MCP n'a été trouvé.\",\n      \"tool-warning\":\n        \"Pour obtenir les meilleures performances, envisagez de désactiver les outils inutiles afin de préserver le contexte.\",\n      \"stop-server\": \"Arrêter le serveur MCP\",\n      \"start-server\": \"Démarrer le serveur MCP\",\n      \"delete-server\": \"Supprimer le serveur MCP\",\n      \"tool-count-warning\":\n        \"Ce serveur MCP a <b> des outils {{count}} activés</b> qui consommeront du contexte dans chaque conversation.<br /> Envisagez de désactiver les outils inutiles pour préserver le contexte.\",\n      \"startup-command\": \"Commande de démarrage\",\n      command: \"Ordre\",\n      arguments: \"Arguments\",\n      \"not-running-warning\":\n        \"Ce serveur MCP n'est pas en cours de fonctionnement ; il peut être arrêté ou rencontrer une erreur lors du démarrage.\",\n      \"tool-call-arguments\": \"Arguments des appels de fonctions/outils\",\n      \"tools-enabled\": \"outils activés\",\n    },\n    settings: {\n      title: \"Paramètres des compétences des agents\",\n      \"max-tool-calls\": {\n        title: \"Nombre maximal de requêtes Max Tool par réponse\",\n        description:\n          \"Le nombre maximal d'outils qu'un agent peut utiliser en chaîne pour générer une seule réponse. Cela empêche les appels excessifs aux outils et les boucles infinies.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Sélection de compétences basée sur l'intelligence\",\n        \"beta-badge\": \"Bêta\",\n        description:\n          \"Permettez l'utilisation illimitée d'outils et réduisez la consommation de jetons jusqu'à 80 % par requête – AnythingLLM sélectionne automatiquement les compétences appropriées pour chaque requête.\",\n        \"max-tools\": {\n          title: \"Max Tools\",\n          description:\n            \"Le nombre maximal d'outils à sélectionner pour chaque requête. Nous recommandons de définir cette valeur sur une valeur plus élevée pour les modèles de contexte plus importants.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Chats de l'espace de travail\",\n    description:\n      \"Voici tous les chats et messages enregistrés qui ont été envoyés par les utilisateurs, classés par date de création.\",\n    export: \"Exporter\",\n    table: {\n      id: \"Id\",\n      by: \"Envoyé par\",\n      workspace: \"Espace de travail\",\n      prompt: \"Invite\",\n      response: \"Réponse\",\n      at: \"Envoyé à\",\n    },\n  },\n  api: {\n    title: \"Clés API\",\n    description:\n      \"Les clés API permettent au titulaire d'accéder et de gérer de manière programmatique cette instance AnythingLLM.\",\n    link: \"Lisez la documentation de l'API\",\n    generate: \"Générer une nouvelle clé API\",\n    table: {\n      key: \"Clé API\",\n      by: \"Créé par\",\n      created: \"Créé\",\n    },\n  },\n  llm: {\n    title: \"Préférence LLM\",\n    description:\n      \"Voici les identifiants et les paramètres de votre fournisseur LLM de chat et d'intégration préféré. Il est important que ces clés soient actuelles et correctes, sinon AnythingLLM ne fonctionnera pas correctement.\",\n    provider: \"Fournisseur LLM\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Point de terminaison du service Azure\",\n        api_key: \"Clé API\",\n        chat_deployment_name: \"Nom du déploiement de chat\",\n        chat_model_token_limit: \"Limite de tokens du modèle de chat\",\n        model_type: \"Type de modèle\",\n        default: \"Par défaut\",\n        reasoning: \"Raisonnement\",\n        model_type_tooltip:\n          \"Si votre déploiement utilise un modèle de raisonnement (o1, o1-mini, o3-mini, etc.), veuillez définir cette option sur « Raisonnement ». Sinon, vos requêtes de conversation pourraient échouer.\",\n      },\n    },\n  },\n  transcription: {\n    title: \"Préférence du modèle de transcription\",\n    description:\n      \"Voici les identifiants et les paramètres de votre fournisseur de modèle de transcription préféré. Il est important que ces clés soient actuelles et correctes, sinon les fichiers multimédias et audio ne seront pas transcrits.\",\n    provider: \"Fournisseur de transcription\",\n    \"warn-start\":\n      \"L'utilisation du modèle local whisper sur des machines avec une RAM ou un CPU limités peut bloquer AnythingLLM lors du traitement des fichiers multimédias.\",\n    \"warn-recommend\":\n      \"Nous recommandons au moins 2 Go de RAM et des fichiers téléchargés <10 Mo.\",\n    \"warn-end\":\n      \"Le modèle intégré se téléchargera automatiquement lors de la première utilisation.\",\n  },\n  embedding: {\n    title: \"Préférence d'intégration\",\n    \"desc-start\":\n      \"Lorsque vous utilisez un LLM qui ne supporte pas nativement un moteur d'intégration - vous devrez peut-être spécifier en plus des identifiants pour intégrer le texte.\",\n    \"desc-end\":\n      \"L'intégration est le processus de transformation du texte en vecteurs. Ces identifiants sont nécessaires pour transformer vos fichiers et invites en un format que AnythingLLM peut utiliser pour traiter.\",\n    provider: {\n      title: \"Fournisseur d'intégration\",\n    },\n  },\n  text: {\n    title: \"Préférences de division et de découpage du texte\",\n    \"desc-start\":\n      \"Parfois, vous voudrez peut-être changer la façon dont les nouveaux documents sont divisés et découpés avant d'être insérés dans votre base de données vectorielle.\",\n    \"desc-end\":\n      \"Vous ne devez modifier ce paramètre que si vous comprenez comment fonctionne la division du texte et ses effets secondaires.\",\n    size: {\n      title: \"Taille des segments de texte\",\n      description:\n        \"C'est la longueur maximale de caractères pouvant être présents dans un seul vecteur.\",\n      recommend: \"Longueur maximale du modèle d'intégration est\",\n    },\n    overlap: {\n      title: \"Chevauchement des segments de texte\",\n      description:\n        \"C'est le chevauchement maximal de caractères qui se produit pendant le découpage entre deux segments de texte adjacents.\",\n    },\n  },\n  vector: {\n    title: \"Base de données vectorielle\",\n    description:\n      \"Voici les identifiants et les paramètres de fonctionnement de votre instance AnythingLLM. Il est important que ces clés soient actuelles et correctes.\",\n    provider: {\n      title: \"Fournisseur de base de données vectorielle\",\n      description: \"Aucune configuration n'est nécessaire pour LanceDB.\",\n    },\n  },\n  embeddable: {\n    title: \"Widgets de chat intégrables\",\n    description:\n      \"Les widgets de chat intégrables sont des interfaces de chat publiques associées à un espace de travail unique. Ils vous permettent de créer des espaces de travail que vous pouvez ensuite publier dans le monde entier.\",\n    create: \"Créer un widget intégré\",\n    table: {\n      workspace: \"Espace de travail\",\n      chats: \"Chats envoyés\",\n      active: \"Domaines actifs\",\n      created: \"Créé le\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Chats intégrés\",\n    export: \"Exporter\",\n    description:\n      \"Voici tous les chats et messages enregistrés de tout widget intégré que vous avez publié.\",\n    table: {\n      embed: \"Intégration\",\n      sender: \"Expéditeur\",\n      message: \"Message\",\n      response: \"Réponse\",\n      at: \"Envoyé à\",\n    },\n  },\n  event: {\n    title: \"Journaux d'événements\",\n    description:\n      \"Consultez toutes les actions et événements se produisant sur cette instance pour la surveillance.\",\n    clear: \"Effacer les journaux d'événements\",\n    table: {\n      type: \"Type d'événement\",\n      user: \"Utilisateur\",\n      occurred: \"Survenu à\",\n    },\n  },\n  privacy: {\n    title: \"Confidentialité et gestion des données\",\n    description:\n      \"Voici votre configuration pour la gestion des données et des fournisseurs tiers connectés avec AnythingLLM.\",\n    anonymous: \"Télémétrie anonyme activée\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Rechercher des connecteurs de données\",\n    \"no-connectors\": \"Aucun connecteur de données trouvé.\",\n    github: {\n      name: \"Dépôt GitHub\",\n      description: \"Importez un dépôt GitHub entier en un seul clic.\",\n      URL: \"URL du dépôt GitHub\",\n      URL_explained: \"URL du dépôt GitHub que vous souhaitez collecter.\",\n      token: \"Jeton d'accès GitHub\",\n      optional: \"Optionnel\",\n      token_explained: \"Jeton d'accès pour les dépôts privés.\",\n      token_explained_start:\n        \"Sans jeton d'accès, vous ne pourrez collecter que les dépôts publics. Vous pouvez\",\n      token_explained_link1: \"créer un jeton d'accès temporaire\",\n      token_explained_middle: \"ou\",\n      token_explained_link2: \"en créer un ici\",\n      token_explained_end: \"avec la portée 'repo'.\",\n      ignores: \"Exclusions de fichiers\",\n      git_ignore:\n        \"Liste au format .gitignore pour exclure des fichiers de la collecte. Appuyez sur Entrée après chaque entrée.\",\n      task_explained:\n        \"Une fois terminé, tous les fichiers seront disponibles pour être intégrés dans les espaces de travail dans le menu de documents.\",\n      branch: \"Branche\",\n      branch_loading: \"-- chargement des branches disponibles --\",\n      branch_explained: \"Branche à collecter.\",\n      token_information: \"Informations sur le jeton\",\n      token_personal:\n        \"Créez un jeton d'accès personnel sur GitHub pour accéder aux dépôts privés.\",\n    },\n    gitlab: {\n      name: \"Dépôt GitLab\",\n      description: \"Importez un dépôt GitLab entier en un seul clic.\",\n      URL: \"URL du dépôt GitLab\",\n      URL_explained: \"URL du dépôt GitLab que vous souhaitez collecter.\",\n      token: \"Jeton d'accès GitLab\",\n      optional: \"Optionnel\",\n      token_description:\n        \"Sélectionnez les portées d'accès au dépôt lors de la création du jeton.\",\n      token_explained_start:\n        \"Sans jeton d'accès, vous ne pourrez collecter que les dépôts publics. Vous pouvez\",\n      token_explained_link1: \"créer un jeton d'accès temporaire\",\n      token_explained_middle: \"ou\",\n      token_explained_link2: \"en créer un ici\",\n      token_explained_end: \"avec la portée 'read_repository'.\",\n      fetch_issues: \"Récupérer les issues GitLab\",\n      ignores: \"Exclusions de fichiers\",\n      git_ignore:\n        \"Liste au format .gitignore pour exclure des fichiers de la collecte. Appuyez sur Entrée après chaque entrée.\",\n      task_explained:\n        \"Une fois terminé, tous les fichiers seront disponibles pour être intégrés dans les espaces de travail dans le menu de documents.\",\n      branch: \"Branche\",\n      branch_loading: \"-- chargement des branches disponibles --\",\n      branch_explained: \"Branche à collecter.\",\n      token_information: \"Informations sur le jeton\",\n      token_personal:\n        \"Créez un jeton d'accès personnel sur GitLab pour accéder aux dépôts privés.\",\n    },\n    youtube: {\n      name: \"Transcription YouTube\",\n      description:\n        \"Importez la transcription d'une vidéo YouTube à partir d'un lien.\",\n      URL: \"URL de la vidéo YouTube\",\n      URL_explained_start:\n        \"Entrez l'URL d'une vidéo YouTube pour récupérer sa transcription. La vidéo doit avoir les\",\n      URL_explained_link: \"sous-titres activés\",\n      URL_explained_end: \".\",\n      task_explained:\n        \"Une fois terminé, la transcription sera disponible pour être intégrée dans les espaces de travail dans le menu de documents.\",\n    },\n    \"website-depth\": {\n      name: \"Récupération de site web en masse\",\n      description:\n        \"Récupérez un site web et ses sous-liens jusqu'à une certaine profondeur.\",\n      URL: \"URL du site web\",\n      URL_explained: \"URL du site web que vous souhaitez récupérer.\",\n      depth: \"Profondeur de récupération\",\n      depth_explained:\n        \"Nombre de niveaux de sous-liens à suivre à partir de l'URL de base.\",\n      max_pages: \"Nombre maximum de pages\",\n      max_pages_explained: \"Nombre maximum de pages à récupérer.\",\n      task_explained:\n        \"Une fois terminé, toutes les pages récupérées seront disponibles pour être intégrées dans les espaces de travail dans le menu de documents.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Importez un espace Confluence entier en un seul clic.\",\n      deployment_type: \"Type de déploiement Confluence\",\n      deployment_type_explained:\n        \"Choisissez si votre instance Confluence est hébergée dans le cloud ou sur serveur.\",\n      base_url: \"URL de base Confluence\",\n      base_url_explained: \"L'URL de base de votre instance Confluence.\",\n      space_key: \"Clé de l'espace Confluence\",\n      space_key_explained:\n        \"La clé de l'espace que vous souhaitez importer. Se trouve généralement dans l'URL de l'espace.\",\n      username: \"Nom d'utilisateur Confluence\",\n      username_explained:\n        \"Votre nom d'utilisateur ou adresse e-mail Confluence.\",\n      auth_type: \"Type d'authentification\",\n      auth_type_explained:\n        \"Choisissez le type de jeton utilisé pour l'authentification.\",\n      auth_type_username: \"Jeton API (nom d'utilisateur + jeton)\",\n      auth_type_personal: \"Jeton d'accès personnel (PAT)\",\n      token: \"Jeton API Confluence\",\n      token_explained_start:\n        \"Un jeton API est requis pour l'authentification. Vous pouvez\",\n      token_explained_link: \"générer un jeton API ici\",\n      token_desc: \"Jeton API pour l'authentification.\",\n      pat_token: \"Jeton d'accès personnel\",\n      pat_token_explained:\n        \"Jeton d'accès personnel pour l'authentification sur les déploiements serveur.\",\n      task_explained:\n        \"Une fois terminé, toutes les pages de l'espace seront disponibles pour être intégrées dans les espaces de travail dans le menu de documents.\",\n      bypass_ssl: \"Ignorer la vérification SSL\",\n      bypass_ssl_explained:\n        \"Ignorez la vérification des certificats SSL pour les instances auto-hébergées avec des certificats auto-signés.\",\n    },\n    manage: {\n      documents: \"Documents\",\n      \"data-connectors\": \"Connecteurs de données\",\n      \"desktop-only\":\n        \"Cette fonctionnalité n'est disponible que sur ordinateur de bureau.\",\n      dismiss: \"Fermer\",\n      editing: \"Modification\",\n    },\n    directory: {\n      \"my-documents\": \"Mes documents\",\n      \"new-folder\": \"Nouveau dossier\",\n      \"search-document\": \"Rechercher un document\",\n      \"no-documents\": \"Aucun document\",\n      \"move-workspace\": \"Déplacer vers l'espace de travail\",\n      \"delete-confirmation\":\n        \"Êtes-vous sûr de vouloir supprimer ces fichiers et dossiers ?\\nCela supprimera les fichiers du système et les retirera automatiquement de tout espace de travail existant.\\nCette action est irréversible.\",\n      \"removing-message\":\n        \"Suppression de {{count}} documents et dossiers. Veuillez patienter.\",\n      \"move-success\": \"{{count}} documents déplacés avec succès.\",\n      no_docs: \"Aucun document\",\n      select_all: \"Tout sélectionner\",\n      deselect_all: \"Tout désélectionner\",\n      remove_selected: \"Supprimer la sélection\",\n      costs: \"Coûts\",\n      save_embed: \"Sauvegarder et intégrer\",\n      \"total-documents_one\": \"{{count}}\",\n      \"total-documents_other\": \"{{count}} documents\",\n    },\n    upload: {\n      \"processor-offline\": \"Processeur de documents hors ligne\",\n      \"processor-offline-desc\":\n        \"Nous ne pouvons pas télécharger vos fichiers pour le moment. Veuillez réessayer plus tard.\",\n      \"click-upload\": \"Cliquez pour télécharger ou glissez-déposez\",\n      \"file-types\":\n        \"prend en charge les fichiers texte, CSV, feuilles de calcul, fichiers audio, et plus encore !\",\n      \"or-submit-link\": \"ou soumettre un lien\",\n      \"placeholder-link\": \"https://exemple.com\",\n      fetching: \"Récupération...\",\n      \"fetch-website\": \"Récupérer le site web\",\n      \"privacy-notice\":\n        \"Ces fichiers seront téléchargés sur cette instance AnythingLLM uniquement.\",\n    },\n    pinning: {\n      what_pinning: \"Qu'est-ce que l'épinglage de documents ?\",\n      pin_explained_block1:\n        \"Lorsque vous épinglez un document, AnythingLLM injectera le contenu intégral du document dans votre fenêtre de prompt comme contexte préalable pour chaque interaction.\",\n      pin_explained_block2:\n        \"Ceci est idéal pour les documents que vous souhaitez référencer fréquemment ou pour fournir un contexte constant à l'IA.\",\n      pin_explained_block3:\n        \"L'épinglage fonctionne mieux avec des documents plus petits. Les documents volumineux peuvent affecter les performances.\",\n      accept: \"J'ai compris\",\n    },\n    watching: {\n      what_watching: \"Qu'est-ce que la surveillance de documents ?\",\n      watch_explained_block1:\n        \"Lorsque vous surveillez un document, AnythingLLM re-synchronisera automatiquement le contenu du document depuis sa source de manière périodique.\",\n      watch_explained_block2:\n        \"Cela gardera le contenu à jour si le fichier source change.\",\n      watch_explained_block3_start:\n        \"Cette fonctionnalité est actuellement limitée à\",\n      watch_explained_block3_link: \"certains types de fichiers\",\n      watch_explained_block3_end: \".\",\n      accept: \"J'ai compris\",\n    },\n    obsidian: {\n      vault_location: \"Emplacement du coffre\",\n      vault_description:\n        \"Sélectionnez le dossier racine de votre coffre Obsidian.\",\n      selected_files: \"fichiers sélectionnés\",\n      importing: \"Importation...\",\n      import_vault: \"Importer le coffre\",\n      processing_time:\n        \"Le traitement peut prendre quelques minutes selon la taille du coffre.\",\n      vault_warning:\n        \"Assurez-vous de sélectionner le dossier racine contenant le dossier .obsidian.\",\n    },\n  },\n  chat_window: {\n    send_message: \"Envoyer un message\",\n    attach_file: \"Joindre un fichier\",\n    text_size: \"Modifier la taille du texte\",\n    microphone: \"Enregistrer un message vocal\",\n    send: \"Envoyer le message au chatbot\",\n    attachments_processing:\n      \"Les pièces jointes sont en cours de traitement. Veuillez attendre avant d'envoyer un autre message.\",\n    tts_speak_message: \"Écouter le message\",\n    copy: \"Copier\",\n    regenerate: \"Régénérer\",\n    regenerate_response: \"Régénérer la réponse\",\n    good_response: \"Bonne réponse\",\n    more_actions: \"Plus d'actions\",\n    fork: \"Dupliquer\",\n    delete: \"Supprimer\",\n    cancel: \"Annuler\",\n    edit_prompt: \"Modifier le prompt\",\n    edit_response: \"Modifier la réponse\",\n    preset_reset_description:\n      \"Efface l'historique du chat actuel et commence une nouvelle conversation.\",\n    add_new_preset: \"Ajouter une nouvelle commande preset\",\n    command: \"Commande\",\n    your_command: \"Votre commande\",\n    placeholder_prompt: \"Quel est le prompt pour cette commande ?\",\n    description: \"Description\",\n    placeholder_description: \"Décrivez ce que fait cette commande\",\n    save: \"Sauvegarder\",\n    small: \"Petit\",\n    normal: \"Normal\",\n    large: \"Grand\",\n    workspace_llm_manager: {\n      search: \"Rechercher des modèles\",\n      loading_workspace_settings:\n        \"Chargement des paramètres de l'espace de travail...\",\n      available_models: \"Modèles disponibles\",\n      available_models_description:\n        \"Sélectionnez un modèle à utiliser pour cet espace de travail.\",\n      save: \"Sauvegarder\",\n      saving: \"Sauvegarde...\",\n      missing_credentials: \"Identifiants manquants\",\n      missing_credentials_description:\n        \"Vous devez configurer vos identifiants de fournisseur LLM avant de pouvoir sélectionner un modèle.\",\n    },\n    submit: \"Soumettre\",\n    edit_info_user:\n      '\"Soumettre\" permet de régénérer la réponse de l\\'IA. \"Enregistrer\" met uniquement à jour votre message.',\n    edit_info_assistant:\n      \"Vos modifications seront enregistrées directement dans cette réponse.\",\n    see_less: \"Voir moins\",\n    see_more: \"Voir plus\",\n    tools: \"Outils\",\n    browse: \"Parcourir\",\n    text_size_label: \"Taille du texte\",\n    select_model: \"Sélectionner le modèle\",\n    sources: \"Sources\",\n    document: \"Document\",\n    similarity_match: \"match\",\n    source_count_one: \"{{count}} référence\",\n    source_count_other: \"Références à {{count}}\",\n    preset_exit_description: \"Arrêter la session actuelle de l'agent\",\n    add_new: \"Ajouter\",\n    edit: \"Modifier\",\n    publish: \"Publier\",\n    stop_generating: \"Arrêtez de générer des réponses\",\n    pause_tts_speech_message:\n      \"Mettre en pause la lecture de la voix synthétique du message\",\n    slash_commands: \"Commandes abrégées\",\n    agent_skills: \"Compétences des agents\",\n    manage_agent_skills: \"Gérer les compétences des agents\",\n    agent_skills_disabled_in_session:\n      \"Il n'est pas possible de modifier les compétences pendant une session avec un agent actif. Utilisez la commande `/exit` pour terminer la session en premier.\",\n    start_agent_session: \"Démarrer la session de l'agent\",\n    use_agent_session_to_use_tools:\n      'Vous pouvez utiliser des outils via le chat en lançant une session avec un agent en utilisant le préfixe \"@agent\" au début de votre requête.',\n  },\n  profile_settings: {\n    edit_account: \"Modifier le compte\",\n    profile_picture: \"Photo de profil\",\n    remove_profile_picture: \"Supprimer la photo de profil\",\n    username: \"Nom d'utilisateur\",\n    new_password: \"Nouveau mot de passe\",\n    password_description:\n      \"Le mot de passe doit contenir au moins 8 caractères.\",\n    cancel: \"Annuler\",\n    update_account: \"Mettre à jour le compte\",\n    theme: \"Thème\",\n    language: \"Langue\",\n    failed_upload: \"Échec du téléchargement de l'image.\",\n    upload_success: \"Image téléchargée avec succès.\",\n    failed_remove: \"Échec de la suppression de l'image.\",\n    profile_updated: \"Profil mis à jour avec succès.\",\n    failed_update_user: \"Échec de la mise à jour de l'utilisateur.\",\n    account: \"Compte\",\n    support: \"Support\",\n    signout: \"Déconnexion\",\n  },\n  customization: {\n    interface: {\n      title: \"Interface\",\n      description: \"Personnalisez l'apparence de l'interface utilisateur.\",\n    },\n    branding: {\n      title: \"Personnalisation de la marque\",\n      description: \"Personnalisez les éléments de marque de votre instance.\",\n    },\n    chat: {\n      title: \"Chat\",\n      description: \"Personnalisez le comportement du chat.\",\n      auto_submit: {\n        title: \"Soumission automatique\",\n        description:\n          \"Soumet automatiquement le message lorsque vous utilisez la reconnaissance vocale.\",\n      },\n      auto_speak: {\n        title: \"Lecture automatique\",\n        description: \"Lit automatiquement les réponses de l'IA à haute voix.\",\n      },\n      spellcheck: {\n        title: \"Correction orthographique\",\n        description:\n          \"Active la correction orthographique dans la zone de saisie du chat.\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Thème\",\n        description: \"Sélectionnez votre thème d'interface préféré.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Afficher la barre de défilement\",\n        description: \"Affiche la barre de défilement dans l'interface de chat.\",\n      },\n      \"support-email\": {\n        title: \"E-mail de support\",\n        description:\n          \"Définissez l'adresse e-mail de support affichée aux utilisateurs.\",\n      },\n      \"app-name\": {\n        title: \"Nom de l'application\",\n        description: \"Définissez le nom affiché dans l'interface.\",\n      },\n      \"display-language\": {\n        title: \"Langue d'affichage\",\n        description: \"Sélectionnez la langue de l'interface utilisateur.\",\n      },\n      logo: {\n        title: \"Logo\",\n        description: \"Téléchargez votre logo personnalisé.\",\n        add: \"Ajouter un logo personnalisé\",\n        recommended: \"Taille recommandée : 800 x 200\",\n        remove: \"Supprimer\",\n        replace: \"Remplacer\",\n      },\n      \"browser-appearance\": {\n        title: \"Apparence du navigateur\",\n        description: \"Personnalisez l'apparence de l'onglet du navigateur.\",\n        tab: {\n          title: \"Titre de l'onglet\",\n          description:\n            \"Définissez le titre affiché dans l'onglet du navigateur.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description:\n            \"Définissez l'icône affichée dans l'onglet du navigateur.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Pied de page de la barre latérale\",\n        description:\n          \"Ajoutez des icônes et des liens personnalisés au pied de page de la barre latérale.\",\n        icon: \"URL de l'icône\",\n        link: \"URL de destination\",\n      },\n      \"render-html\": {\n        title: \"Rendu HTML\",\n        description:\n          \"Autorise le rendu du contenu HTML dans les réponses du chat.\",\n      },\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Créer un agent\",\n      editWorkspace: \"Modifier l'espace de travail\",\n      uploadDocument: \"Télécharger un document\",\n    },\n    greeting: \"Comment puis-je vous aider aujourd'hui ?\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Raccourcis clavier\",\n    shortcuts: {\n      settings: \"Ouvrir les paramètres\",\n      workspaceSettings: \"Paramètres de l'espace de travail\",\n      home: \"Retour à l'accueil\",\n      workspaces: \"Afficher les espaces de travail\",\n      apiKeys: \"Gérer les clés API\",\n      llmPreferences: \"Préférences LLM\",\n      chatSettings: \"Paramètres du chat\",\n      help: \"Afficher l'aide\",\n      showLLMSelector: \"Afficher le sélecteur de LLM\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Prompt publié avec succès !\",\n        success_description:\n          \"Votre prompt système a été publié sur le Community Hub.\",\n        success_thank_you: \"Merci pour votre contribution !\",\n        view_on_hub: \"Voir sur le Hub\",\n        modal_title: \"Publier le prompt système\",\n        name_label: \"Nom\",\n        name_description: \"Un nom descriptif pour votre prompt.\",\n        name_placeholder: \"Mon super prompt\",\n        description_label: \"Description\",\n        description_description:\n          \"Décrivez ce que fait votre prompt et comment l'utiliser.\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Ajoutez des tags pour aider les autres à trouver votre prompt.\",\n        tags_placeholder: \"productivité, rédaction, code...\",\n        visibility_label: \"Visibilité\",\n        public_description: \"Visible par tous sur le Community Hub.\",\n        private_description: \"Visible uniquement par vous.\",\n        publish_button: \"Publier\",\n        submitting: \"Publication...\",\n        prompt_label: \"Prompt\",\n        prompt_description: \"Le contenu de votre prompt système.\",\n        prompt_placeholder: \"Vous êtes un assistant IA utile...\",\n      },\n      agent_flow: {\n        success_title: \"Flux d'agent publié avec succès !\",\n        success_description:\n          \"Votre flux d'agent a été publié sur le Community Hub.\",\n        success_thank_you: \"Merci pour votre contribution !\",\n        view_on_hub: \"Voir sur le Hub\",\n        modal_title: \"Publier le flux d'agent\",\n        name_label: \"Nom\",\n        name_description: \"Un nom descriptif pour votre flux d'agent.\",\n        name_placeholder: \"Mon flux d'agent\",\n        description_label: \"Description\",\n        description_description:\n          \"Décrivez ce que fait votre flux d'agent et comment l'utiliser.\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Ajoutez des tags pour aider les autres à trouver votre flux.\",\n        tags_placeholder: \"automatisation, productivité...\",\n        visibility_label: \"Visibilité\",\n        submitting: \"Publication...\",\n        submit: \"Soumettre\",\n        privacy_note:\n          \"Les flux d'agents peuvent contenir des informations sensibles. Vérifiez le contenu avant de le rendre public.\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Connexion requise\",\n          description:\n            \"Vous devez vous connecter à votre compte AnythingLLM pour publier sur le Community Hub.\",\n          button: \"Se connecter\",\n        },\n      },\n      slash_command: {\n        success_title: \"Commande publiée avec succès !\",\n        success_description:\n          \"Votre commande slash a été publiée sur le Community Hub.\",\n        success_thank_you: \"Merci pour votre contribution !\",\n        view_on_hub: \"Voir sur le Hub\",\n        modal_title: \"Publier la commande slash\",\n        name_label: \"Nom\",\n        name_description: \"Un nom descriptif pour votre commande.\",\n        name_placeholder: \"Ma commande\",\n        description_label: \"Description\",\n        description_description:\n          \"Décrivez ce que fait votre commande et comment l'utiliser.\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Ajoutez des tags pour aider les autres à trouver votre commande.\",\n        tags_placeholder: \"productivité, résumé...\",\n        visibility_label: \"Visibilité\",\n        public_description: \"Visible par tous sur le Community Hub.\",\n        private_description: \"Visible uniquement par vous.\",\n        publish_button: \"Publier\",\n        submitting: \"Publication...\",\n        prompt_label: \"Prompt\",\n        prompt_description: \"Le prompt exécuté par cette commande.\",\n        prompt_placeholder: \"Résumez le texte suivant : {{input}}\",\n      },\n    },\n  },\n  security: {\n    title: \"Sécurité\",\n    multiuser: {\n      title: \"Mode multi-utilisateurs\",\n      description:\n        \"Configurez votre instance pour prendre en charge votre équipe en activant le mode multi-utilisateurs.\",\n      enable: {\n        \"is-enable\": \"Le mode multi-utilisateurs est activé\",\n        enable: \"Activer le mode multi-utilisateurs\",\n        description:\n          \"Par défaut, vous serez le seul administrateur. En tant qu'administrateur, vous devrez créer des comptes pour tous les nouveaux utilisateurs ou administrateurs. Ne perdez pas votre mot de passe car seul un utilisateur administrateur peut réinitialiser les mots de passe.\",\n        username: \"Nom d'utilisateur du compte administrateur\",\n        password: \"Mot de passe du compte administrateur\",\n      },\n    },\n    password: {\n      title: \"Protection par mot de passe\",\n      description:\n        \"Protégez votre instance AnythingLLM avec un mot de passe. Si vous oubliez ce mot de passe, il n'y a pas de méthode de récupération, donc assurez-vous de le sauvegarder.\",\n      \"password-label\": \"Mot de passe de l'instance\",\n    },\n  },\n  home: {\n    welcome: \"Bienvenue\",\n    chooseWorkspace:\n      \"Choisissez un espace de travail pour commencer à chatter!\",\n    notAssigned:\n      \"Vous n'êtes actuellement pas affecté à aucun espace de travail.\\nPour accéder à un espace de travail, veuillez contacter votre administrateur.\",\n    goToWorkspace: 'Aller à \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/he/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"ברוכים הבאים ל\",\n      getStarted: \"להתחלה\",\n    },\n    llm: {\n      title: \"העדפות מודל שפה (LLM)\",\n      description:\n        \"AnythingLLM יכול לעבוד עם ספקי מודלי שפה (LLM) רבים. זה יהיה השירות שיטפל בצ'אט.\",\n    },\n    userSetup: {\n      title: \"הגדרת משתמש\",\n      description: \"הגדר את הגדרות המשתמש שלך.\",\n      howManyUsers: \"כמה משתמשים ישתמשו במופע זה?\",\n      justMe: \"רק אני\",\n      myTeam: \"הצוות שלי\",\n      instancePassword: \"סיסמת מופע\",\n      setPassword: \"האם תרצה להגדיר סיסמה?\",\n      passwordReq: \"סיסמאות חייבות להכיל לפחות 8 תווים.\",\n      passwordWarn: \"חשוב לשמור סיסמה זו מכיוון שאין שיטת שחזור.\",\n      adminUsername: \"שם משתמש של חשבון מנהל\",\n      adminPassword: \"סיסמת חשבון מנהל\",\n      adminPasswordReq: \"סיסמאות חייבות להכיל לפחות 8 תווים.\",\n      teamHint:\n        \"כברירת מחדל, אתה תהיה המנהל היחיד. לאחר סיום ההצטרפות תוכל ליצור ולהזמין אחרים להיות משתמשים או מנהלים. אל תאבד את סיסמתך, מכיוון שרק מנהלים יכולים לאפס סיסמאות.\",\n    },\n    data: {\n      title: \"טיפול בנתונים ופרטיות\",\n      description: \"אנו מחויבים לשקיפות ושליטה בכל הנוגע לנתונים האישיים שלך.\",\n      settingsHint: \"ניתן להגדיר מחדש הגדרות אלה בכל עת בהגדרות.\",\n    },\n    survey: {\n      title: \"ברוכים הבאים ל-AnythingLLM\",\n      description:\n        \"עזרו לנו לבנות את AnythingLLM כך שיתאים לצרכים שלכם. אופציונלי.\",\n      email: \"מה האימייל שלך?\",\n      useCase: \"לאיזו מטרה תשתמש ב-AnythingLLM?\",\n      useCaseWork: \"לעבודה\",\n      useCasePersonal: \"לשימוש אישי\",\n      useCaseOther: \"אחר\",\n      comment: \"איך שמעת על AnythingLLM?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube, וכו' - ספר לנו איך מצאת אותנו!\",\n      skip: \"דלג על הסקר\",\n      thankYou: \"תודה על המשוב!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"שם סביבת העבודה\",\n    user: \"משתמש\",\n    selection: \"בחירת מודל\",\n    saving: \"שומר...\",\n    save: \"שמור שינויים\",\n    previous: \"עמוד קודם\",\n    next: \"עמוד הבא\",\n    optional: \"אופציונלי\",\n    yes: \"כן\",\n    no: \"לא\",\n    search: \"חיפוש\",\n    username_requirements:\n      \"שם המשתמש חייב להיות באורך 2-32 תווים, להתחיל באות קטנה ולהכיל רק אותיות קטנות, מספרים, קווים תחתונים, מקפים ונקודות.\",\n    on: \"על\",\n    none: \"אין\",\n    stopped: \"עצר\",\n    loading: \"טעינה\",\n    refresh: \"רענן\",\n  },\n  settings: {\n    title: \"הגדרות מופע\",\n    invites: \"הזמנות\",\n    users: \"משתמשים\",\n    workspaces: \"סביבות עבודה\",\n    \"workspace-chats\": \"צ'אטים של סביבות עבודה\",\n    customization: \"התאמה אישית\",\n    interface: \"העדפות ממשק משתמש\",\n    branding: \"מיתוג והתאמה אישית (Whitelabeling)\",\n    chat: \"צ'אט\",\n    \"api-keys\": \"מפתחות API למפתחים\",\n    llm: \"מודל שפה (LLM)\",\n    transcription: \"תמלול\",\n    embedder: \"מטמיע (Embedder)\",\n    \"text-splitting\": \"פיצול טקסט וחלוקה למקטעים (Chunking)\",\n    \"voice-speech\": \"קול ודיבור\",\n    \"vector-database\": \"מסד נתונים וקטורי\",\n    embeds: \"הטמעות צ'אט (Embeds)\",\n    security: \"אבטחה\",\n    \"event-logs\": \"יומני אירועים\",\n    privacy: \"פרטיות ונתונים\",\n    \"ai-providers\": \"ספקי בינה מלאכותית\",\n    \"agent-skills\": \"כישורי סוכן\",\n    admin: \"מנהל\",\n    tools: \"כלים\",\n    \"system-prompt-variables\": \"משתני הנחיית מערכת\",\n    \"experimental-features\": \"תכונות ניסיוניות\",\n    contact: \"צור קשר עם התמיכה\",\n    \"browser-extension\": \"תוסף דפדפן\",\n    \"mobile-app\": \"AnythingLLM Mobile\",\n    \"community-hub\": {\n      title: \"מרכז קהילתי\",\n      trending: \"גלו את הנושאים החמים\",\n      \"your-account\": \"החשבון שלך\",\n      \"import-item\": \"ייבוא פריט\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"ברוכים הבאים ל\",\n      \"placeholder-username\": \"שם משתמש\",\n      \"placeholder-password\": \"סיסמה\",\n      login: \"התחברות\",\n      validating: \"מאמת...\",\n      \"forgot-pass\": \"שכחת סיסמה\",\n      reset: \"איפוס\",\n    },\n    \"sign-in\": \"התחבר לחשבון {{appName}} שלך.\",\n    \"password-reset\": {\n      title: \"איפוס סיסמה\",\n      description: \"ספק את המידע הדרוש למטה כדי לאפס את סיסמתך.\",\n      \"recovery-codes\": \"קודיי שחזור\",\n      \"back-to-login\": \"חזרה להתחברות\",\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"צור סוכן\",\n      editWorkspace: \"ערוך את סביבת העבודה\",\n      uploadDocument: \"העלה מסמך\",\n    },\n    greeting: \"במה אוכל לעזור לך היום?\",\n  },\n  \"new-workspace\": {\n    title: \"סביבת עבודה חדשה\",\n    placeholder: \"סביבת העבודה שלי\",\n  },\n  \"workspaces—settings\": {\n    general: \"הגדרות כלליות\",\n    chat: \"הגדרות צ'אט\",\n    vector: \"מסד נתונים וקטורי\",\n    members: \"חברים\",\n    agent: \"תצורת סוכן\",\n  },\n  general: {\n    vector: {\n      title: \"ספירת וקטורים\",\n      description: \"המספר הכולל של וקטורים במסד הנתונים הווקטורי שלך.\",\n    },\n    names: {\n      description: \"זה ישנה רק את שם התצוגה של סביבת העבודה שלך.\",\n    },\n    message: {\n      title: \"הודעות צ'אט מוצעות\",\n      description: \"התאם אישית את ההודעות שיוצעו למשתמשי סביבת העבודה שלך.\",\n      add: \"הוסף הודעה חדשה\",\n      save: \"שמור הודעות\",\n      heading: \"הסבר לי\",\n      body: \"את היתרונות של AnythingLLM\",\n    },\n    delete: {\n      title: \"מחק סביבת עבודה\",\n      description:\n        \"מחק סביבת עבודה זו ואת כל הנתונים שלה. פעולה זו תמחק את סביבת העבודה עבור כל המשתמשים.\",\n      delete: \"מחק סביבת עבודה\",\n      deleting: \"מוחק סביבת עבודה...\",\n      \"confirm-start\": \"אתה עומד למחוק את כל\",\n      \"confirm-end\":\n        \"סביבת העבודה שלך. פעולה זו תסיר את כל הטמעות הווקטורים ממסד הנתונים הווקטורי שלך.\\n\\nקבצי המקור המקוריים יישארו ללא שינוי. פעולה זו אינה הפיכה.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"ספק מודל שפה (LLM) של סביבת העבודה\",\n      description:\n        \"ספק ומודל ה-LLM הספציפיים שישמשו עבור סביבת עבודה זו. כברירת מחדל, הוא משתמש בספק ובהגדרות ה-LLM של המערכת.\",\n      search: \"חפש בכל ספקי ה-LLM\",\n    },\n    model: {\n      title: \"מודל צ'אט של סביבת העבודה\",\n      description:\n        \"מודל הצ'אט הספציפי שישמש עבור סביבת עבודה זו. אם ריק, ישתמש בהעדפת ה-LLM של המערכת.\",\n    },\n    mode: {\n      title: \"מצב צ'אט\",\n      chat: {\n        title: \"צ'אט\",\n        description:\n          'יוכל לספק תשובות בהתבסס על הידע הכללי של ה-LLM ועל ההקשר הרלוונטי מתוך המסמך. <b> ו-</b>\\nתצטרכו להשתמש בפקודה \"@agent\" כדי להשתמש בכלי.',\n      },\n      query: {\n        title: \"שאילתה\",\n        description:\n          \"יספק תשובות <b>רק</b>במידה ויהיה ניתן למצוא הקשר של המסמך.<br />תצטרכו להשתמש בפקודה @agent כדי להשתמש בכלי.\",\n      },\n      automatic: {\n        title: \"רכב\",\n        description:\n          'הכלי ישתמש באופן אוטומטי בכלים אם המודל והספק תומכים בהם. <br />אם אין תמיכה בכלים מקומיים, תצטרכו להשתמש בפקודה \"@agent\" כדי להשתמש בכלים.',\n      },\n    },\n    history: {\n      title: \"היסטוריית צ'אט\",\n      \"desc-start\": \"מספר הצ'אטים הקודמים שייכללו בזיכרון לטווח קצר של התגובה.\",\n      recommend: \"מומלץ 20. \",\n      \"desc-end\":\n        \"יותר מ-45 צפוי להוביל לכשלים רציפים בצ'אט, תלוי בגודל ההודעה.\",\n    },\n    prompt: {\n      title: \"הנחיית מערכת\",\n      description:\n        \"ההנחיה שתשמש בסביבת עבודה זו. הגדר את ההקשר וההוראות לבינה המלאכותית כדי ליצור תגובה. עליך לספק הנחיה מנוסחת בקפידה כדי שה-AI יוכל ליצור תגובה רלוונטית ומדויקת.\",\n      history: {\n        title: \"היסטוריית הנחיות מערכת\",\n        clearAll: \"נקה הכל\",\n        noHistory: \"אין היסטוריית הנחיות מערכת זמינה\",\n        restore: \"שחזר\",\n        delete: \"מחק\",\n        publish: \"פרסם במרכז הקהילה\",\n        deleteConfirm: \"האם אתה בטוח שברצונך למחוק פריט היסטוריה זה?\",\n        clearAllConfirm:\n          \"האם אתה בטוח שברצונך לנקות את כל ההיסטוריה? לא ניתן לבטל פעולה זו.\",\n        expand: \"הרחב\",\n      },\n    },\n    refusal: {\n      title: \"תגובת סירוב במצב שאילתה\",\n      \"desc-start\": \"כאשר במצב\",\n      query: \"שאילתה\",\n      \"desc-end\":\n        \", ייתכן שתרצה להחזיר תגובת סירוב מותאמת אישית כאשר לא נמצא הקשר.\",\n      \"tooltip-title\": \"למה אני רואה את זה?\",\n      \"tooltip-description\":\n        \"אתה נמצא במצב שאילתה, אשר משתמש רק במידע מהמסמכים שלך. עבור למצב צ'אט לשיחות גמישות יותר, או לחץ כאן כדי לבקר בתיעוד שלנו וללמוד עוד על מצבי צ'אט.\",\n    },\n    temperature: {\n      title: \"טמפרטורת LLM\",\n      \"desc-start\": 'הגדרה זו שולטת במידת ה\"יצירתיות\" של תגובות מודל השפה שלך.',\n      \"desc-end\":\n        \"ככל שהמספר גבוה יותר, כך התגובה יצירתית יותר. עבור מודלים מסוימים, הדבר עלול להוביל לתגובות לא קוהרנטיות כאשר הערך גבוה מדי.\",\n      hint: \"לרוב מודלי ה-LLM יש טווחי ערכים קבילים שונים. עיין במידע של ספק ה-LLM שלך.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"מזהה מסד נתונים וקטורי\",\n    snippets: {\n      title: \"מקטעי הקשר מרביים\",\n      description:\n        \"הגדרה זו שולטת בכמות המרבית של מקטעי הקשר שיישלחו למודל השפה עבור כל צ'אט או שאילתה.\",\n      recommend: \"מומלץ: 4\",\n    },\n    doc: {\n      title: \"סף דמיון מסמכים\",\n      description:\n        \"ציון הדמיון המינימלי הנדרש כדי שמקור ייחשב קשור לצ'אט. ככל שהמספר גבוה יותר, כך המקור חייב להיות דומה יותר לצ'אט.\",\n      zero: \"ללא הגבלה\",\n      low: \"נמוך (ציון דמיון ≥ 0.25)\",\n      medium: \"בינוני (ציון דמיון ≥ 0.50)\",\n      high: \"גבוה (ציון דמיון ≥ 0.75)\",\n    },\n    reset: {\n      reset: \"אפס מסד נתונים וקטורי\",\n      resetting: \"מנקה וקטורים...\",\n      confirm:\n        \"אתה עומד לאפס את מסד הנתונים הווקטורי של סביבת עבודה זו. פעולה זו תסיר את כל הטמעות הווקטורים הקיימות.\\n\\nקבצי המקור המקוריים יישארו ללא שינוי. פעולה זו אינה הפיכה.\",\n      error: \"לא ניתן היה לאפס את מסד הנתונים הווקטורי של סביבת העבודה!\",\n      success: \"מסד הנתונים הווקטורי של סביבת העבודה אופס!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"הביצועים של מודלי שפה שאינם תומכים במפורש בקריאת כלים (tool-calling) תלויים מאוד ביכולות ובדיוק של המודל. ייתכן שיכולות מסוימות יהיו מוגבלות או לא פונקציונליות.\",\n    provider: {\n      title: \"ספק מודל שפה (LLM) של סוכן סביבת העבודה\",\n      description:\n        \"ספק ומודל ה-LLM הספציפיים שישמשו עבור סוכן ה-@agent של סביבת עבודה זו.\",\n    },\n    mode: {\n      chat: {\n        title: \"מודל צ'אט של סוכן סביבת העבודה\",\n        description:\n          \"מודל הצ'אט הספציפי שישמש עבור סוכן ה-@agent של סביבת עבודה זו.\",\n      },\n      title: \"מודל סוכן של סביבת העבודה\",\n      description:\n        \"מודל ה-LLM הספציפי שישמש עבור סוכן ה-@agent של סביבת עבודה זו.\",\n      wait: \"-- ממתין למודלים --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG וזיכרון לטווח ארוך\",\n        description:\n          'אפשר לסוכן למנף את המסמכים המקומיים שלך כדי לענות על שאילתות או בקש מהסוכן \"לזכור\" חלקי תוכן לאחזור זיכרון לטווח ארוך.',\n      },\n      view: {\n        title: \"צפייה וסיכום מסמכים\",\n        description:\n          \"אפשר לסוכן לרשום ולסכם את התוכן של קבצי סביבת העבודה המוטמעים כעת.\",\n      },\n      scrape: {\n        title: \"גירוד אתרי אינטרנט\",\n        description: \"אפשר לסוכן לבקר ולגרד את התוכן של אתרי אינטרנט.\",\n      },\n      generate: {\n        title: \"יצירת תרשימים\",\n        description:\n          \"אפשר לסוכן ברירת המחדל ליצור סוגים שונים של תרשימים מנתונים שסופקו או ניתנו בצ'אט.\",\n      },\n      save: {\n        title: \"יצירה ושמירה של קבצים לדפדפן\",\n        description:\n          \"אפשר לסוכן ברירת המחדל ליצור ולכתוב לקבצים שנשמרים וניתנים להורדה בדפדפן שלך.\",\n      },\n      web: {\n        title: \"חיפוש וגלישה באינטרנט בזמן אמת\",\n        description:\n          \"אפשרו לסוכן שלכם לחפש באינטרנט כדי לענות על שאלותיכם, על ידי חיבור לספק שירותי חיפוש (SERP).\",\n      },\n      sql: {\n        title: \"חיבור SQL\",\n        description:\n          \"אפשרו לסוכן שלכם לנצל את SQL כדי לענות על שאלותיכם, על ידי חיבור למספר ספקי מסדי נתונים של SQL.\",\n      },\n      default_skill:\n        \"כברירת מחדל, הכישורים הזה מופעל, אך ניתן להשבית אותו אם אינכם רוצים שהוא יהיה זמין עבור הסוכן.\",\n    },\n    mcp: {\n      title: \"שרתי MCP\",\n      \"loading-from-config\": \"טעינת שרתי MCP מהקובץ בתצורה\",\n      \"learn-more\": \"למידע נוסף על שרתי MCP.\",\n      \"no-servers-found\": \"לא נמצאו שרתי MCP.\",\n      \"tool-warning\":\n        \"על מנת להשיג את הביצועים הטובים ביותר, שקלו לבטל כלים לא רצויים כדי לחסוך במשאבים.\",\n      \"stop-server\": \"עצור את שרת ה-MCP\",\n      \"start-server\": \"הפעל שרת MCP\",\n      \"delete-server\": \"מחיקת שרת ה-MCP\",\n      \"tool-count-warning\":\n        \"שרת ה-MCP הזה כולל <b>כלי{{count}} שפעילים, אשר יצרוך מידע הקשר בכל צ'אט. </b> מומלץ לבטל את הכליות הלא רצויות כדי לחסוך במידע ההקשר.\",\n      \"startup-command\": \"פקודת התחלה\",\n      command: \"פקודה\",\n      arguments: \"טיעונים\",\n      \"not-running-warning\":\n        \"שרת ה-MCP הזה אינו פועל – ייתכן שהוא מושבת או שהוא חווה תקלה בעת ההפעלה.\",\n      \"tool-call-arguments\": \"ארגומנטים לפונקציות\",\n      \"tools-enabled\": \"הכלים פעלו/היו זמינים\",\n    },\n    settings: {\n      title: \"הגדרות מיומנויות של סוכן\",\n      \"max-tool-calls\": {\n        title: 'מספר קריאות \"Max Tool\" לכל תגובה',\n        description:\n          \"מספר הכלים המקסימלי שאгент יכול לקשור כדי ליצור תגובה אחת. זה מונע קריאות מרובות של כלים ומחזורים אינסופיים.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"בחירת מיומנויות בהתאם ליכולות\",\n        \"beta-badge\": \"בטא\",\n        description:\n          \"אפשרו שימוש בלתי מוגבל בכלים וצמצום השימוש בטוקנים עד 80% לכל שאילתה – ה-AnythingLLM בוחר באופן אוטומטי את הכישורים המתאימים ביותר לכל בקשה.\",\n        \"max-tools\": {\n          title: \"כלים של מקס\",\n          description:\n            \"מספר המינימום של כלי העבודה שניתן לבחור עבור כל שאילתה. אנו ממליצים להגדיר ערך גבוה יותר עבור מודלים עם הקשר רחב יותר.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"צ'אטים של סביבת עבודה\",\n    description:\n      \"אלה כל הצ'אטים וההודעות המוקלטים שנשלחו על ידי משתמשים, מסודרים לפי תאריך יצירתם.\",\n    export: \"יצא\",\n    table: {\n      id: \"מזהה\",\n      by: \"נשלח על ידי\",\n      workspace: \"סביבת עבודה\",\n      prompt: \"הנחיה\",\n      response: \"תגובה\",\n      at: \"נשלח ב\",\n    },\n  },\n  customization: {\n    interface: {\n      title: \"העדפות ממשק משתמש\",\n      description: \"הגדר את העדפות ממשק המשתמש שלך עבור AnythingLLM.\",\n    },\n    branding: {\n      title: \"מיתוג והתאמה אישית (Whitelabeling)\",\n      description: \"התאם אישית את מופע ה-AnythingLLM שלך עם מיתוג מותאם אישית.\",\n    },\n    chat: {\n      title: \"צ'אט\",\n      description: \"הגדר את העדפות הצ'אט שלך עבור AnythingLLM.\",\n      auto_submit: {\n        title: \"שליחה אוטומטית של קלט קולי\",\n        description: \"שלח אוטומטית קלט קולי לאחר פרק זמן של שקט\",\n      },\n      auto_speak: {\n        title: \"הקראה אוטומטית של תגובות\",\n        description: \"הקרא אוטומטית תגובות מה-AI\",\n      },\n      spellcheck: {\n        title: \"הפעל בדיקת איות\",\n        description: \"הפעל או השבת בדיקת איות בשדה הקלט של הצ'אט\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"ערכת נושא\",\n        description: \"בחר את ערכת הצבעים המועדפת עליך ליישום.\",\n      },\n      \"show-scrollbar\": {\n        title: \"הצג פס גלילה\",\n        description: \"הפעל או השבת את פס הגלילה בחלון הצ'אט.\",\n      },\n      \"support-email\": {\n        title: \"אימייל תמיכה\",\n        description:\n          \"הגדר את כתובת האימייל לתמיכה שתהיה נגישה למשתמשים כאשר הם זקוקים לעזרה.\",\n      },\n      \"app-name\": {\n        title: \"שם\",\n        description: \"הגדר שם שיוצג בדף ההתחברות לכל המשתמשים.\",\n      },\n      \"display-language\": {\n        title: \"שפת תצוגה\",\n        description:\n          \"בחר את השפה המועדפת להצגת ממשק המשתמש של AnythingLLM - כאשר תרגומים זמינים.\",\n      },\n      logo: {\n        title: \"לוגו מותג\",\n        description: \"העלה את הלוגו המותאם אישית שלך להצגה בכל העמודים.\",\n        add: \"הוסף לוגו מותאם אישית\",\n        recommended: \"גודל מומלץ: 800x200\",\n        remove: \"הסר\",\n        replace: \"החלף\",\n      },\n      \"browser-appearance\": {\n        title: \"מראה הדפדפן\",\n        description:\n          \"התאם אישית את מראה לשונית הדפדפן והכותרת כשהאפליקציה פתוחה.\",\n        tab: {\n          title: \"כותרת\",\n          description:\n            \"הגדר כותרת לשונית מותאמת אישית כשהאפליקציה פתוחה בדפדפן.\",\n        },\n        favicon: {\n          title: \"סמל אתר (Favicon)\",\n          description: \"השתמש בסמל אתר מותאם אישית עבור לשונית הדפדפן.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"פריטי כותרת תחתונה בסרגל הצד\",\n        description:\n          \"התאם אישית את פריטי הכותרת התחתונה המוצגים בתחתית סרגל הצד.\",\n        icon: \"סמל\",\n        link: \"קישור\",\n      },\n      \"render-html\": {\n        title: \"הצגת קוד HTML בשיחת צ'אט\",\n        description:\n          \"הצגת תגובות HTML בתגובות של עוזר.\\nזה יכול להוביל לאיכות תגובה גבוהה בהרבה, אך גם עלול לגרום לסיכונים פוטנציאליים של אבטחה.\",\n      },\n    },\n  },\n  api: {\n    title: \"מפתחות API\",\n    description:\n      \"מפתחות API מאפשרים למחזיק בהם לגשת ולנהל באופן תכנותי את מופע AnythingLLM זה.\",\n    link: \"קרא את תיעוד ה-API\",\n    generate: \"צור מפתח API חדש\",\n    table: {\n      key: \"מפתח API\",\n      by: \"נוצר על ידי\",\n      created: \"נוצר\",\n    },\n  },\n  llm: {\n    title: \"העדפות מודל שפה (LLM)\",\n    description:\n      \"אלה האישורים וההגדרות עבור ספק הצ'אט וההטמעה המועדף עליך. חשוב שמפתחות אלה יהיו עדכניים ונכונים, אחרת AnythingLLM לא יפעל כראוי.\",\n    provider: \"ספק LLM\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"נקודת קצה של שירות Azure\",\n        api_key: \"מפתח API\",\n        chat_deployment_name: \"שם פריסת צ'אט\",\n        chat_model_token_limit: \"מגבלת אסימוני מודל צ'אט\",\n        model_type: \"סוג מודל\",\n        default: \"ברירת מחדל\",\n        reasoning: \"היגיון\",\n        model_type_tooltip:\n          'אם השימוש שלך כולל מודל הסקה (o1, o1-mini, o3-mini וכו\\'), הגדר זאת ל\"הסקה\". אחרת, בקשות השיחה שלך עלולות להיכשל.',\n      },\n    },\n  },\n  transcription: {\n    title: \"העדפות מודל תמלול\",\n    description:\n      \"אלה האישורים וההגדרות עבור ספק מודל התמלול המועדף עליך. חשוב שמפתחות אלה יהיו עדכניים ונכונים, אחרת קובצי מדיה ושמע לא יתומללו.\",\n    provider: \"ספק תמלול\",\n    \"warn-start\":\n      \"שימוש במודל ה-whisper המקומי על מכונות עם זיכרון RAM או מעבד מוגבלים עלול לגרום להאטה של AnythingLLM בעת עיבוד קובצי מדיה.\",\n    \"warn-recommend\":\n      \"אנו ממליצים על לפחות 2GB של זיכרון RAM והעלאת קבצים קטנים מ-10Mb.\",\n    \"warn-end\": \"המודל המובנה יורד אוטומטית בשימוש הראשון.\",\n  },\n  embedding: {\n    title: \"העדפות הטמעה (Embedding)\",\n    \"desc-start\":\n      \"בעת שימוש במודל שפה שאינו תומך באופן מובנה במנוע הטמעה - ייתכן שתצטרך לציין בנוסף אישורים להטמעת טקסט.\",\n    \"desc-end\":\n      \"הטמעה היא תהליך של הפיכת טקסט לווקטורים. אישורים אלה נדרשים כדי להפוך את הקבצים וההנחיות שלך לפורמט ש-AnythingLLM יכול להשתמש בו לעיבוד.\",\n    provider: {\n      title: \"ספק הטמעה\",\n    },\n  },\n  text: {\n    title: \"העדפות פיצול טקסט וחלוקה למקטעים (Chunking)\",\n    \"desc-start\":\n      \"לפעמים, ייתכן שתרצה לשנות את הדרך ברירת המחדל שבה מסמכים חדשים מפוצלים ומחולקים למקטעים לפני הכנסתם למסד הנתונים הווקטורי שלך.\",\n    \"desc-end\":\n      \"עליך לשנות הגדרה זו רק אם אתה מבין כיצד פועל פיצול טקסט ואת תופעות הלוואי שלו.\",\n    size: {\n      title: \"גודל מקטע טקסט\",\n      description: \"זוהי הכמות המרבית של תווים שיכולה להיות בווקטור יחיד.\",\n      recommend: \"אורך מרבי של מודל הטמעה הוא\",\n    },\n    overlap: {\n      title: \"חפיפת מקטעי טקסט\",\n      description:\n        \"זוהי החפיפה המרבית של תווים המתרחשת במהלך חלוקה למקטעים בין שני מקטעי טקסט סמוכים.\",\n    },\n  },\n  vector: {\n    title: \"מסד נתונים וקטורי\",\n    description:\n      \"אלה האישורים וההגדרות לאופן פעולת מופע ה-AnythingLLM שלך. חשוב שמפתחות אלה יהיו עדכניים ונכונים.\",\n    provider: {\n      title: \"ספק מסד נתונים וקטורי\",\n      description: \"אין צורך בתצורה עבור LanceDB.\",\n    },\n  },\n  embeddable: {\n    title: \"ווידג'טים של צ'אט להטמעה\",\n    description:\n      \"ווידג'טים של צ'אט להטמעה הם ממשקי צ'אט ציבוריים הקשורים לסביבת עבודה אחת. הם מאפשרים לך לבנות סביבות עבודה שתוכל לפרסם לעולם.\",\n    create: \"צור הטמעה\",\n    table: {\n      workspace: \"סביבת עבודה\",\n      chats: \"צ'אטים שנשלחו\",\n      active: \"דומיינים פעילים\",\n      created: \"נוצר\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"היסטוריית צ'אט מוטמע\",\n    export: \"יצא\",\n    description: \"אלה כל הצ'אטים וההודעות המוקלטים מכל הטמעה שפרסמת.\",\n    table: {\n      embed: \"הטמעה\",\n      sender: \"שולח\",\n      message: \"הודעה\",\n      response: \"תגובה\",\n      at: \"נשלח ב\",\n    },\n  },\n  event: {\n    title: \"יומני אירועים\",\n    description: \"צפה בכל הפעולות והאירועים המתרחשים במופע זה לצורך ניטור.\",\n    clear: \"נקה יומני אירועים\",\n    table: {\n      type: \"סוג אירוע\",\n      user: \"משתמש\",\n      occurred: \"התרחש ב\",\n    },\n  },\n  privacy: {\n    title: \"פרטיות וטיפול בנתונים\",\n    description:\n      \"זוהי התצורה שלך לאופן שבו ספקים צד שלישי מחוברים ו-AnythingLLM מטפלים בנתונים שלך.\",\n    anonymous: \"טלמטריה אנונימית מופעלת\",\n  },\n  connectors: {\n    \"search-placeholder\": \"חפש מחברי נתונים\",\n    \"no-connectors\": \"לא נמצאו מחברי נתונים.\",\n    obsidian: {\n      vault_location: \"מיקום כספת\",\n      vault_description:\n        \"בחר את תיקיית כספת ה-Obsidian שלך כדי לייבא את כל ההערות והחיבורים ביניהן.\",\n      selected_files: \"נמצאו {{count}} קבצי markdown\",\n      importing: \"מייבא כספת...\",\n      import_vault: \"ייבא כספת\",\n      processing_time: \"זה עשוי לקחת זמן מה, תלוי בגודל הכספת שלך.\",\n      vault_warning:\n        \"כדי למנוע התנגשויות, ודא שכספת ה-Obsidian שלך אינה פתוחה כעת.\",\n    },\n    github: {\n      name: \"מאגר GitHub\",\n      description: \"ייבא מאגר GitHub ציבורי או פרטי שלם בלחיצה אחת.\",\n      URL: \"כתובת URL של מאגר GitHub\",\n      URL_explained: \"כתובת ה-URL של מאגר ה-GitHub שברצונך לאסוף.\",\n      token: \"אסימון גישה של GitHub\",\n      optional: \"אופציונלי\",\n      token_explained: \"אסימון גישה למניעת הגבלת קצב.\",\n      token_explained_start: \"ללא \",\n      token_explained_link1: \"אסימון גישה אישי\",\n      token_explained_middle:\n        \", ה-API של GitHub עשוי להגביל את מספר הקבצים שניתן לאסוף עקב הגבלות קצב. תוכל \",\n      token_explained_link2: \"ליצור אסימון גישה זמני\",\n      token_explained_end: \" כדי למנוע בעיה זו.\",\n      ignores: \"התעלמות מקבצים\",\n      git_ignore:\n        \"רשום בפורמט .gitignore כדי להתעלם מקבצים ספציפיים במהלך האיסוף. הקש אנטר לאחר כל ערך שברצונך לשמור.\",\n      task_explained:\n        \"לאחר השלמה, כל הקבצים יהיו זמינים להטמעה בסביבות עבודה בבורר המסמכים.\",\n      branch: \"ענף שממנו ברצונך לאסוף קבצים.\",\n      branch_loading: \"-- טוען ענפים זמינים --\",\n      branch_explained: \"ענף שממנו ברצונך לאסוף קבצים.\",\n      token_information:\n        \"ללא מילוי <b>אסימון הגישה של GitHub</b>, מחבר נתונים זה יוכל לאסוף רק את הקבצים ב<b>רמה העליונה</b> של המאגר עקב הגבלות הקצב של ה-API הציבורי של GitHub.\",\n      token_personal: \"קבל אסימון גישה אישי בחינם עם חשבון GitHub כאן.\",\n    },\n    gitlab: {\n      name: \"מאגר GitLab\",\n      description: \"ייבא מאגר GitLab ציבורי או פרטי שלם בלחיצה אחת.\",\n      URL: \"כתובת URL של מאגר GitLab\",\n      URL_explained: \"כתובת ה-URL של מאגר ה-GitLab שברצונך לאסוף.\",\n      token: \"אסימון גישה של GitLab\",\n      optional: \"אופציונלי\",\n      token_description: \"בחר ישויות נוספות לאחזור מה-API של GitLab.\",\n      token_explained_start: \"ללא \",\n      token_explained_link1: \"אסימון גישה אישי\",\n      token_explained_middle:\n        \", ה-API של GitLab עשוי להגביל את מספר הקבצים שניתן לאסוף עקב הגבלות קצב. תוכל \",\n      token_explained_link2: \"ליצור אסימון גישה זמני\",\n      token_explained_end: \" כדי למנוע בעיה זו.\",\n      fetch_issues: \"אחזר בעיות (Issues) כמסמכים\",\n      ignores: \"התעלמות מקבצים\",\n      git_ignore:\n        \"רשום בפורמט .gitignore כדי להתעלם מקבצים ספציפיים במהלך האיסוף. הקש אנטר לאחר כל ערך שברצונך לשמור.\",\n      task_explained:\n        \"לאחר השלמה, כל הקבצים יהיו זמינים להטמעה בסביבות עבודה בבורר המסמכים.\",\n      branch: \"ענף שממנו ברצונך לאסוף קבצים\",\n      branch_loading: \"-- טוען ענפים זמינים --\",\n      branch_explained: \"ענף שממנו ברצונך לאסוף קבצים.\",\n      token_information:\n        \"ללא מילוי <b>אסימון הגישה של GitLab</b>, מחבר נתונים זה יוכל לאסוף רק את הקבצים ב<b>רמה העליונה</b> של המאגר עקב הגבלות הקצב של ה-API הציבורי של GitLab.\",\n      token_personal: \"קבל אסימון גישה אישי בחינם עם חשבון GitLab כאן.\",\n    },\n    youtube: {\n      name: \"תמלול YouTube\",\n      description: \"ייבא את התמלול של סרטון YouTube שלם מקישור.\",\n      URL: \"כתובת URL של סרטון YouTube\",\n      URL_explained_start:\n        \"הזן את כתובת ה-URL של כל סרטון YouTube כדי לאחזר את התמלול שלו. לסרטון חייבות להיות \",\n      URL_explained_link: \"כתוביות סגורות\",\n      URL_explained_end: \" זמינות.\",\n      task_explained:\n        \"לאחר השלמה, התמלול יהיה זמין להטמעה בסביבות עבודה בבורר המסמכים.\",\n    },\n    \"website-depth\": {\n      name: \"גרדן קישורים המוני\",\n      description: \"גרד אתר ואת קישורי המשנה שלו עד לעומק מסוים.\",\n      URL: \"כתובת אתר אינטרנט\",\n      URL_explained: \"כתובת ה-URL של האתר שברצונך לגרד.\",\n      depth: \"עומק זחילה\",\n      depth_explained:\n        \"זהו מספר קישורי הילד שהעובד יעקוב אחריהם מכתובת ה-URL המקורית.\",\n      max_pages: \"מספר עמודים מרבי\",\n      max_pages_explained: \"המספר המרבי של קישורים לגירוד.\",\n      task_explained:\n        \"לאחר השלמה, כל התוכן שנגרד יהיה זמין להטמעה בסביבות עבודה בבורר המסמכים.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"ייבא עמוד Confluence שלם בלחיצה אחת.\",\n      deployment_type: \"סוג פריסת Confluence\",\n      deployment_type_explained:\n        \"קבע אם מופע ה-Confluence שלך מתארח בענן של Atlassian או באירוח עצמי.\",\n      base_url: \"כתובת בסיס של Confluence\",\n      base_url_explained: \"זוהי כתובת הבסיס של מרחב ה-Confluence שלך.\",\n      space_key: \"מפתח מרחב של Confluence\",\n      space_key_explained:\n        \"זהו מפתח המרחבים של מופע ה-Confluence שלך שישמש. בדרך כלל מתחיל ב-~\",\n      username: \"שם משתמש ב-Confluence\",\n      username_explained: \"שם המשתמש שלך ב-Confluence\",\n      auth_type: \"סוג אימות Confluence\",\n      auth_type_explained:\n        \"בחר את סוג האימות שבו ברצונך להשתמש כדי לגשת לדפי ה-Confluence שלך.\",\n      auth_type_username: \"שם משתמש ואסימון גישה\",\n      auth_type_personal: \"אסימון גישה אישי\",\n      token: \"אסימון גישה של Confluence\",\n      token_explained_start:\n        \"עליך לספק אסימון גישה לאימות. תוכל ליצור אסימון גישה\",\n      token_explained_link: \"כאן\",\n      token_desc: \"אסימון גישה לאימות\",\n      pat_token: \"אסימון גישה אישי של Confluence\",\n      pat_token_explained: \"אסימון הגישה האישי שלך ב-Confluence.\",\n      task_explained:\n        \"לאחר השלמה, תוכן העמוד יהיה זמין להטמעה בסביבות עבודה בבורר המסמכים.\",\n      bypass_ssl: \"התעלמות מאימות תעודת SSL\",\n      bypass_ssl_explained:\n        \"אפשר להפעיל את האפשרות זו כדי לעקוף את אימות תעודת ה-SSL עבור מופעי Confluence המאוחסנים באופן עצמאי עם תעודה שחתמה באופן עצמי.\",\n    },\n    manage: {\n      documents: \"מסמכים\",\n      \"data-connectors\": \"מחברי נתונים\",\n      \"desktop-only\":\n        \"עריכת הגדרות אלה זמינה רק במחשב שולחני. אנא גש לדף זה משולחן העבודה שלך כדי להמשיך.\",\n      dismiss: \"התעלם\",\n      editing: \"עורך\",\n    },\n    directory: {\n      \"my-documents\": \"המסמכים שלי\",\n      \"new-folder\": \"תיקייה חדשה\",\n      \"search-document\": \"חפש מסמך\",\n      \"no-documents\": \"אין מסמכים\",\n      \"move-workspace\": \"העבר לסביבת עבודה\",\n      \"delete-confirmation\":\n        \"האם אתה בטוח שברצונך למחוק קבצים ותיקיות אלה?\\nפעולה זו תסיר את הקבצים מהמערכת ותסיר אותם אוטומטית מכל סביבת עבודה קיימת.\\nפעולה זו אינה הפיכה.\",\n      \"removing-message\":\n        \"מסיר {{count}} מסמכים ו-{{folderCount}} תיקיות. אנא המתן.\",\n      \"move-success\": \"{{count}} מסמכים הועברו בהצלחה.\",\n      no_docs: \"אין מסמכים\",\n      select_all: \"בחר הכל\",\n      deselect_all: \"בטל בחירת הכל\",\n      remove_selected: \"הסר נבחרים\",\n      costs: \"*עלות חד פעמית להטמעות\",\n      save_embed: \"שמור והטמע\",\n      \"total-documents_one\": \"{{count}} מסמך\",\n      \"total-documents_other\": \"מסמכים {{count}}\",\n    },\n    upload: {\n      \"processor-offline\": \"מעבד המסמכים אינו זמין\",\n      \"processor-offline-desc\":\n        \"אין באפשרותנו להעלות את הקבצים שלך כרגע מכיוון שמעבד המסמכים אינו מקוון. אנא נסה שוב מאוחר יותר.\",\n      \"click-upload\": \"לחץ להעלאה או גרור ושחרר\",\n      \"file-types\": \"תומך בקבצי טקסט, csv, גיליונות אלקטרוניים, קבצי שמע ועוד!\",\n      \"or-submit-link\": \"או שלח קישור\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"מאחזר...\",\n      \"fetch-website\": \"אחזר אתר אינטרנט\",\n      \"privacy-notice\":\n        \"קבצים אלה יועלו למעבד המסמכים הפועל במופע זה של AnythingLLM. קבצים אלה אינם נשלחים או משותפים עם צד שלישי.\",\n    },\n    pinning: {\n      what_pinning: \"מהי הצמדת מסמכים?\",\n      pin_explained_block1:\n        \"כאשר אתה <b>מצמיד</b> מסמך ב-AnythingLLM, אנו נזריק את כל תוכן המסמך לחלון ההנחיה שלך כדי שמודל השפה שלך יבין אותו במלואו.\",\n      pin_explained_block2:\n        \"זה עובד בצורה הטובה ביותר עם <b>מודלים בעלי הקשר רחב</b> או קבצים קטנים שהם קריטיים לבסיס הידע שלו.\",\n      pin_explained_block3:\n        \"אם אינך מקבל את התשובות הרצויות מ-AnythingLLM כברירת מחדל, הצמדה היא דרך מצוינת לקבל תשובות איכותיות יותר בלחיצה אחת.\",\n      accept: \"אוקיי, הבנתי\",\n    },\n    watching: {\n      what_watching: \"מה עושה מעקב אחר מסמך?\",\n      watch_explained_block1:\n        \"כאשר אתה <b>עוקב</b> אחר מסמך ב-AnythingLLM, אנו נסנכרן <i>אוטומטית</i> את תוכן המסמך שלך ממקורו המקורי במרווחי זמן קבועים. זה יעדכן אוטומטית את התוכן בכל סביבת עבודה שבה קובץ זה מנוהל.\",\n      watch_explained_block2:\n        \"תכונה זו תומכת כיום בתוכן מבוסס-אינטרנט ולא תהיה זמינה עבור מסמכים שהועלו ידנית.\",\n      watch_explained_block3_start:\n        \"תוכל לנהל אילו מסמכים נמצאים במעקב מתוך תצוגת \",\n      watch_explained_block3_link: \"מנהל הקבצים\",\n      watch_explained_block3_end: \" של המנהל.\",\n      accept: \"אוקיי, הבנתי\",\n    },\n  },\n  chat_window: {\n    attachments_processing: \"קבצים מצורפים בעיבוד. אנא המתן...\",\n    send_message: \"שלח הודעה\",\n    attach_file: \"צרף קובץ לצ'אט זה\",\n    text_size: \"שנה גודל טקסט.\",\n    microphone: \"אמור את ההנחיה שלך.\",\n    send: \"שלח הודעת הנחיה לסביבת העבודה\",\n    tts_speak_message: \"הקרא הודעה (TTS)\",\n    copy: \"העתק\",\n    regenerate: \"צור מחדש\",\n    regenerate_response: \"צור תגובה מחדש\",\n    good_response: \"תגובה טובה\",\n    more_actions: \"פעולות נוספות\",\n    fork: \"פצל (Fork)\",\n    delete: \"מחק\",\n    cancel: \"בטל\",\n    edit_prompt: \"ערוך הנחיה\",\n    edit_response: \"ערוך תגובה\",\n    preset_reset_description: \"נקה את היסטוריית הצ'אט שלך והתחל צ'אט חדש\",\n    add_new_preset: \" הוסף הגדרה קבועה חדשה\",\n    command: \"פקודה\",\n    your_command: \"הפקודה-שלך\",\n    placeholder_prompt: \"זהו התוכן שיוזרק לפני ההנחיה שלך.\",\n    description: \"תיאור\",\n    placeholder_description: \"מגיב עם שיר על מודלי שפה.\",\n    save: \"שמור\",\n    small: \"קטן\",\n    normal: \"רגיל\",\n    large: \"גדול\",\n    workspace_llm_manager: {\n      search: \"חפש ספקי LLM\",\n      loading_workspace_settings: \"טוען הגדרות סביבת עבודה...\",\n      available_models: \"מודלים זמינים עבור {{provider}}\",\n      available_models_description: \"בחר מודל לשימוש בסביבת עבודה זו.\",\n      save: \"השתמש במודל זה\",\n      saving: \"מגדיר מודל כברירת מחדל של סביבת העבודה...\",\n      missing_credentials: \"חסרים אישורים לספק זה!\",\n      missing_credentials_description: \"לחץ להגדרת אישורים\",\n    },\n    submit: \"הגש\",\n    edit_info_user:\n      '\"שלח\" מחזיר את התגובה של הבינה המלאכותית. \"שמור\" מעדכן רק את ההודעה שלך.',\n    edit_info_assistant: \"השינויים שאתם מבצעים יישמרו ישירות בתגובה זו.\",\n    see_less: \"ראה פחות\",\n    see_more: \"לראות עוד\",\n    tools: \"כלים\",\n    browse: \"גלו\",\n    text_size_label: \"גודל הטקסט\",\n    select_model: \"בחר מודל\",\n    sources: \"מקורות\",\n    document: \"מסמך\",\n    similarity_match: \"משחק\",\n    source_count_one: \"{{count}} - הפניה\",\n    source_count_other: \"{{count}} – מקורות\",\n    preset_exit_description: \"עצירת הפעולה הנוכחית של המשתמש\",\n    add_new: \"הוסף חדש\",\n    edit: \"עריכה\",\n    publish: \"להוציא לאור\",\n    stop_generating: \"הפסיקו ליצור תגובה\",\n    pause_tts_speech_message:\n      \"השהייה של קריאת טקסט באמצעות תוכנת TTS (Text-to-Speech)\",\n    slash_commands: \"פקודות קיצור\",\n    agent_skills: \"כישורים של סוכן\",\n    manage_agent_skills: \"ניהול מיומנויות של סוכנים\",\n    agent_skills_disabled_in_session:\n      'לא ניתן לשנות כישורים במהלך סשן פעיל. יש להשתמש בפקודה \"/exit\" כדי לסיים את הסשן תחילה.',\n    start_agent_session: \"התחלת סשן עם סוכן\",\n    use_agent_session_to_use_tools:\n      \"ניתן להשתמש בכלי הדיון באמצעות פתיחת סשן עם נציג על ידי שימוש בסימן '@agent' בתחילת ההודעה.\",\n  },\n  profile_settings: {\n    edit_account: \"ערוך חשבון\",\n    profile_picture: \"תמונת פרופיל\",\n    remove_profile_picture: \"הסר תמונת פרופיל\",\n    username: \"שם משתמש\",\n    new_password: \"סיסמה חדשה\",\n    password_description: \"הסיסמה חייבת להכיל לפחות 8 תווים\",\n    cancel: \"בטל\",\n    update_account: \"עדכן חשבון\",\n    theme: \"העדפת ערכת נושא\",\n    language: \"שפה מועדפת\",\n    failed_upload: \"העלאת תמונת הפרופיל נכשלה: {{error}}\",\n    upload_success: \"תמונת הפרופיל הועלתה.\",\n    failed_remove: \"הסרת תמונת הפרופיל נכשלה: {{error}}\",\n    profile_updated: \"הפרופיל עודכן.\",\n    failed_update_user: \"עדכון המשתמש נכשל: {{error}}\",\n    account: \"חשבון\",\n    support: \"תמיכה\",\n    signout: \"התנתק\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"קיצורי מקלדת\",\n    shortcuts: {\n      settings: \"פתח הגדרות\",\n      workspaceSettings: \"פתח הגדרות סביבת עבודה נוכחית\",\n      home: \"עבור לדף הבית\",\n      workspaces: \"נהל סביבות עבודה\",\n      apiKeys: \"הגדרות מפתחות API\",\n      llmPreferences: \"העדפות מודל שפה (LLM)\",\n      chatSettings: \"הגדרות צ'אט\",\n      help: \"הצג עזרה לקיצורי מקלדת\",\n      showLLMSelector: \"הצג בורר מודלי שפה לסביבת עבודה\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"הצלחה!\",\n        success_description: \"הנחיית המערכת שלך פורסמה במרכז הקהילה!\",\n        success_thank_you: \"תודה על השיתוף בקהילה!\",\n        view_on_hub: \"צפה במרכז הקהילה\",\n        modal_title: \"פרסם הנחיית מערכת\",\n        name_label: \"שם\",\n        name_description: \"זהו שם התצוגה של הנחיית המערכת שלך.\",\n        name_placeholder: \"הנחיית המערכת שלי\",\n        description_label: \"תיאור\",\n        description_description:\n          \"זהו התיאור של הנחיית המערכת שלך. השתמש בזה כדי לתאר את מטרת ההנחיה.\",\n        tags_label: \"תגיות\",\n        tags_description:\n          \"תגיות משמשות לתיוג הנחיית המערכת שלך לחיפוש קל יותר. ניתן להוסיף מספר תגיות. עד 5 תגיות. עד 20 תווים לתגית.\",\n        tags_placeholder: \"הקלד והקש אנטר להוספת תגיות\",\n        visibility_label: \"נראות\",\n        public_description: \"הנחיות מערכת ציבוריות נראות לכולם.\",\n        private_description: \"הנחיות מערכת פרטיות נראות רק לך.\",\n        publish_button: \"פרסם במרכז הקהילה\",\n        submitting: \"מפרסם...\",\n        prompt_label: \"הנחיה\",\n        prompt_description: \"זוהי הנחיית המערכת בפועל שתשמש להנחיית מודל השפה.\",\n        prompt_placeholder: \"הזן את הנחיית המערכת שלך כאן...\",\n      },\n      agent_flow: {\n        success_title: \"הצלחה!\",\n        success_description: \"זרימת הסוכן שלך פורסמה במרכז הקהילה!\",\n        success_thank_you: \"תודה על השיתוף בקהילה!\",\n        view_on_hub: \"צפה במרכז הקהילה\",\n        modal_title: \"פרסם זרימת סוכן\",\n        name_label: \"שם\",\n        name_description: \"זהו שם התצוגה של זרימת הסוכן שלך.\",\n        name_placeholder: \"זרימת הסוכן שלי\",\n        description_label: \"תיאור\",\n        description_description:\n          \"זהו התיאור של זרימת הסוכן שלך. השתמש בזה כדי לתאר את מטרת הזרימה.\",\n        tags_label: \"תגיות\",\n        tags_description:\n          \"תגיות משמשות לתיוג זרימת הסוכן שלך לחיפוש קל יותר. ניתן להוסיף מספר תגיות. עד 5 תגיות. עד 20 תווים לתגית.\",\n        tags_placeholder: \"הקלד והקש אנטר להוספת תגיות\",\n        visibility_label: \"נראות\",\n        submitting: \"מפרסם...\",\n        submit: \"פרסם במרכז הקהילה\",\n        privacy_note:\n          \"זרימות סוכן תמיד מועלות כפרטיות כדי להגן על נתונים רגישים. תוכל לשנות את הנראות במרכז הקהילה לאחר הפרסום. אנא ודא שהזרימה שלך אינה מכילה מידע רגיש או פרטי לפני הפרסום.\",\n      },\n      slash_command: {\n        success_title: \"הצלחה!\",\n        success_description: \"פקודת הסלאש שלך פורסמה במרכז הקהילה!\",\n        success_thank_you: \"תודה על השיתוף בקהילה!\",\n        view_on_hub: \"צפה במרכז הקהילה\",\n        modal_title: \"פרסם פקודת סלאש\",\n        name_label: \"שם\",\n        name_description: \"זהו שם התצוגה של פקודת הסלאש שלך.\",\n        name_placeholder: \"פקודת הסלאש שלי\",\n        description_label: \"תיאור\",\n        description_description:\n          \"זהו התיאור של פקודת הסלאש שלך. השתמש בזה כדי לתאר את מטרת הפקודה.\",\n        tags_label: \"תגיות\",\n        tags_description:\n          \"תגיות משמשות לתיוג פקודת הסלאש שלך לחיפוש קל יותר. ניתן להוסיף מספר תגיות. עד 5 תגיות. עד 20 תווים לתגית.\",\n        tags_placeholder: \"הקלד והקש אנטר להוספת תגיות\",\n        visibility_label: \"נראות\",\n        public_description: \"פקודות סלאש ציבוריות נראות לכולם.\",\n        private_description: \"פקודות סלאש פרטיות נראות רק לך.\",\n        publish_button: \"פרסם במרכז הקהילה\",\n        submitting: \"מפרסם...\",\n        prompt_label: \"הנחיה\",\n        prompt_description: \"זוהי ההנחיה שתשמש כאשר פקודת הסלאש תופעל.\",\n        prompt_placeholder: \"הזן את ההנחיה שלך כאן...\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"נדרש אימות\",\n          description:\n            \"עליך להתאמת עם מרכז הקהילה של AnythingLLM לפני פרסום פריטים.\",\n          button: \"התחבר למרכז הקהילה\",\n        },\n      },\n    },\n  },\n  security: {\n    title: \"אבטחה\",\n    multiuser: {\n      title: \"מצב ריבוי משתמשים\",\n      description:\n        \"הגדר את המופע שלך לתמיכה בצוות שלך על ידי הפעלת מצב ריבוי משתמשים.\",\n      enable: {\n        \"is-enable\": \"מצב ריבוי משתמשים מופעל\",\n        enable: \"הפעל מצב ריבוי משתמשים\",\n        description:\n          \"כברירת מחדל, אתה תהיה המנהל היחיד. כמנהל תצטרך ליצור חשבונות לכל המשתמשים או המנהלים החדשים. אל תאבד את סיסמתך, מכיוון שרק משתמש מנהל יכול לאפס סיסמאות.\",\n        username: \"שם משתמש של חשבון מנהל\",\n        password: \"סיסמת חשבון מנהל\",\n      },\n    },\n    password: {\n      title: \"הגנת סיסמה\",\n      description:\n        \"הגן על מופע ה-AnythingLLM שלך באמצעות סיסמה. אם תשכח אותה, אין שיטת שחזור, אז ודא שאתה שומר סיסמה זו.\",\n      \"password-label\": \"סיסמת מופע\",\n    },\n  },\n  home: {\n    welcome: \"ברוכים הבאים\",\n    chooseWorkspace: \"בחר סביבת עבודה כדי להתחיל לשוחח!\",\n    notAssigned:\n      \"אינך מוקצה לכל סביבת עבודה.\\nיש ליצור קשר עם המנהל שלך כדי לבקש גישה לסביבת עבודה.\",\n    goToWorkspace: 'עבור לסביבת עבודה \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/it/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    survey: {\n      email: \"Qual è il tuo indirizzo email?\",\n      useCase: \"Quali utilizzi intende fare con AnythingLLM?\",\n      useCaseWork: \"Per lavoro\",\n      useCasePersonal: \"Per uso personale\",\n      useCaseOther: \"Altro\",\n      comment: \"Come ha saputo di AnythingLLM?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube, ecc. – Fateci sapere come ci avete trovato!\",\n      skip: \"Salta la domanda\",\n      thankYou: \"Grazie per il tuo feedback.\",\n      title: \"Benvenuti in AnythingLLM\",\n      description:\n        \"Aiutaci a sviluppare AnythingLLM in base alle tue esigenze. Facoltativo.\",\n    },\n    home: {\n      title: \"Benvenuti a\",\n      getStarted: \"Inizia\",\n    },\n    llm: {\n      title: \"Preferenza per i modelli LLM\",\n      description:\n        \"AnythingLLM può collaborare con numerosi fornitori di modelli linguistici. Questo sarà il servizio che gestirà le conversazioni.\",\n    },\n    userSetup: {\n      title: \"Configurazione dell'utente\",\n      description: \"Configura le impostazioni del tuo account.\",\n      howManyUsers: \"Quanti utenti utilizzeranno questa istanza?\",\n      justMe: \"Solo io.\",\n      myTeam: \"Il mio team\",\n      instancePassword: \"Password dell'istanza\",\n      setPassword: \"Vorresti creare una password?\",\n      passwordReq: \"Le password devono essere di almeno 8 caratteri.\",\n      passwordWarn:\n        \"È importante salvare questa password, poiché non esiste alcun metodo di recupero.\",\n      adminUsername: \"Nome utente dell'account amministratore\",\n      adminPassword: \"Password per l'account amministratore\",\n      adminPasswordReq: \"Le password devono essere di almeno 8 caratteri.\",\n      teamHint:\n        \"Per impostazione predefinita, sarai l'unico amministratore. Una volta completato il processo di registrazione, potrai creare nuovi utenti e invitarli, oppure nominare altri utenti come amministratori. Ricorda di non dimenticare la tua password, poiché solo gli amministratori possono reimpostarla.\",\n    },\n    data: {\n      title: \"Gestione dei dati e privacy\",\n      description:\n        \"Ci impegniamo a garantire la trasparenza e il controllo in relazione ai vostri dati personali.\",\n      settingsHint:\n        \"Queste impostazioni possono essere riconfigurate in qualsiasi momento nelle impostazioni.\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Nome delle aree di lavoro\",\n    user: \"Utente\",\n    selection: \"Selezione del modello\",\n    saving: \"Salvo...\",\n    save: \"Salva modifiche\",\n    previous: \"Pagina precedente\",\n    next: \"Pagina successiva\",\n    optional: \"Opzionale\",\n    yes: \"Sì\",\n    no: \"No.\",\n    search: \"Cerca\",\n    username_requirements:\n      \"Il nome utente deve essere compreso tra 2 e 32 caratteri, iniziare con una lettera minuscola e contenere solo lettere minuscole, numeri, trattini bassi, trattini e punti.\",\n    on: \"Su\",\n    none: \"Nessuno\",\n    stopped: \"Arrestato\",\n    loading: \"Caricamento\",\n    refresh: \"Rinfresca\",\n  },\n  settings: {\n    title: \"Impostazioni istanza\",\n    invites: \"Inviti\",\n    users: \"Utenti\",\n    workspaces: \"Aree di lavoro\",\n    \"workspace-chats\": \"Chat dell'area di lavoro\",\n    customization: \"Personalizzazione\",\n    \"api-keys\": \"API Sviluppatore\",\n    llm: \"LLM\",\n    transcription: \"Trascrizione\",\n    embedder: \"Embedder\",\n    \"text-splitting\": \"Suddivisione di testo & Chunking\",\n    \"voice-speech\": \"Voce & discorso\",\n    \"vector-database\": \"Database Vettoriale\",\n    embeds: \"Chat incorporata\",\n    security: \"Sicurezza\",\n    \"event-logs\": \"Log degli eventi\",\n    privacy: \"Privacy & Dati\",\n    \"ai-providers\": \"AI Providers\",\n    \"agent-skills\": \"Abilità dell'agente\",\n    admin: \"Admin\",\n    tools: \"Strumenti\",\n    \"experimental-features\": \"Caratteristiche sperimentali\",\n    contact: \"Contatta il Supporto\",\n    \"browser-extension\": \"Estensione del browser\",\n    \"system-prompt-variables\":\n      \"Variabili delle variabili del sistema\\n\\nVariabili delle variabili del sistema\",\n    interface: \"Preferenze dell'interfaccia utente\",\n    branding: \"Branding e personalizzazione\",\n    chat: \"Chat\",\n    \"mobile-app\": \"AnythingLLM Mobile\",\n    \"community-hub\": {\n      title: \"Punto di riferimento della comunità\",\n      trending: \"Esplora le tendenze\",\n      \"your-account\": \"Il tuo account\",\n      \"import-item\": \"Importa articolo\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Benvenuto in\",\n      \"placeholder-username\": \"Username\",\n      \"placeholder-password\": \"Password\",\n      login: \"Login\",\n      validating: \"Verifica in corso...\",\n      \"forgot-pass\": \"Password dimenticata\",\n      reset: \"Reset\",\n    },\n    \"sign-in\": \"Accedi al tuo {{appName}} account.\",\n    \"password-reset\": {\n      title: \"Password Reset\",\n      description:\n        \"Fornisci le informazioni necessarie qui sotto per reimpostare la tua password.\",\n      \"recovery-codes\": \"Codici di recupero\",\n      \"back-to-login\": \"Torna al Login\",\n    },\n  },\n  \"new-workspace\": {\n    title: \"Nuova area di lavoro\",\n    placeholder: \"La mia area di lavoro\",\n  },\n  \"workspaces—settings\": {\n    general: \"Impostazioni generali\",\n    chat: \"Impostazioni Chat\",\n    vector: \"Database vettoriale\",\n    members: \"Membri\",\n    agent: \"Configurazione dell'agente\",\n  },\n  general: {\n    vector: {\n      title: \"Contatore dei vettori\",\n      description: \"Numero totale di vettori nel tuo database vettoriale.\",\n    },\n    names: {\n      description:\n        \"Questo cambierà solo il nome visualizzato della tua area di lavoro.\",\n    },\n    message: {\n      title: \"Messaggi Chat suggeriti\",\n      description:\n        \"Personalizza i messaggi che verranno suggeriti agli utenti della tua area di lavoro.\",\n      add: \"Aggiungi un nuovo messaggio\",\n      save: \"Salva messaggi\",\n      heading: \"Spiegami\",\n      body: \"i vantaggi di AnythingLLM\",\n    },\n    delete: {\n      title: \"Elimina area di lavoro\",\n      description:\n        \"Elimina quest'area di lavoro e tutti i suoi dati. Ciò eliminerà l'area di lavoro per tutti gli utenti.\",\n      delete: \"Elimina area di lavoro\",\n      deleting: \"Eliminazione dell'area di lavoro...\",\n      \"confirm-start\": \"Stai per eliminare l'intera\",\n      \"confirm-end\":\n        \"area di lavoro. Verranno rimossi tutti gli embeddings vettoriali nel tuo database vettoriale.\\n\\nI file sorgente originali rimarranno intatti. Questa azione è irreversibile.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"LLM Provider dell'area di lavoro\",\n      description:\n        \"Il provider LLM e il modello specifici che verranno utilizzati per quest'area di lavoro. Per impostazione predefinita, utilizza il provider LLM e le impostazioni di sistema.\",\n      search: \"Cerca tutti i provider LLM\",\n    },\n    model: {\n      title: \"Modello di chat dell'area di lavoro\",\n      description:\n        \"Il modello di chat specifico che verrà utilizzato per quest'area di lavoro. Se vuoto, utilizzerà l'LLM di sistema.\",\n    },\n    mode: {\n      title: \"Modalità chat\",\n      chat: {\n        title: \"Chat\",\n        description:\n          \"fornirà risposte basate sulla conoscenza generale del modello LLM e sul contesto del documento <b>e</b> che è disponibile.<br />Per utilizzare gli strumenti, sarà necessario utilizzare il comando @agent.\",\n      },\n      query: {\n        title: \"Query\",\n        description:\n          'fornirà risposte solo se il contesto del documento viene trovato. Per utilizzare gli strumenti, sarà necessario utilizzare il comando \"@agent\".',\n      },\n      automatic: {\n        title: \"Auto\",\n        description:\n          'utilizzerà automaticamente gli strumenti se il modello e il fornitore supportano la chiamata nativa agli strumenti. <br /> Se la chiamata nativa agli strumenti non è supportata, sarà necessario utilizzare il comando \"@agent\" per utilizzare gli strumenti.',\n      },\n    },\n    history: {\n      title: \"Chat History\",\n      \"desc-start\":\n        \"Numero di chat precedenti che verranno incluse nella memoria a breve termine della risposta.\",\n      recommend: \"Recommend 20. \",\n      \"desc-end\":\n        \"Un numero superiore a 45 potrebbe causare continui errori nella chat, a seconda delle dimensioni del messaggio.\",\n    },\n    prompt: {\n      title: \"Prompt\",\n      description:\n        \"Il prompt che verrà utilizzato in quest'area di lavoro. Definisci il contesto e le istruzioni affinché l'IA generi una risposta. Dovresti fornire un prompt elaborato con cura in modo che l'IA possa generare una risposta pertinente e accurata.\",\n      history: {\n        title: \"Cronologia delle istruzioni del sistema\",\n        clearAll: \"Cancella tutto\",\n        noHistory: \"Non sono disponibili i log di sistema.\",\n        restore: \"Ripristina\",\n        delete: \"Elimina\",\n        deleteConfirm:\n          \"È sicuro che desideri eliminare questo elemento della cronologia?\",\n        clearAllConfirm:\n          \"È sicuro che desideri eliminare tutti i dati di cronologia? Questa operazione non può essere annullata.\",\n        expand: \"Espandi\",\n        publish: \"Pubblica su Community Hub\",\n      },\n    },\n    refusal: {\n      title: \"Risposta al rifiuto nella modalità di query\",\n      \"desc-start\": \"Quando la modalità\",\n      query: \"query\",\n      \"desc-end\":\n        \"è attiva, potresti voler restituire una risposta di rifiuto personalizzata quando non viene trovato alcun contesto.\",\n      \"tooltip-title\": \"Perché lo sto vedendo?\",\n      \"tooltip-description\":\n        \"Si trova in modalità di interrogazione, che utilizza solo le informazioni presenti nei suoi documenti. Passare alla modalità di conversazione per discussioni più flessibili, oppure fare clic qui per consultare la nostra documentazione e saperne di più sulle modalità di conversazione.\",\n    },\n    temperature: {\n      title: \"Temperatura LLM\",\n      \"desc-start\":\n        'Questa impostazione controlla il livello di \"creatività\" delle risposte dell\\'LLM.',\n      \"desc-end\":\n        \"Più alto è il numero, più è creativo. Per alcuni modelli questo può portare a risposte incoerenti se troppo elevato.\",\n      hint: \"La maggior parte degli LLM ha vari intervalli accettabili di valori validi. Consulta il tuo fornitore LLM per queste informazioni.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Identificatore del database vettoriale\",\n    snippets: {\n      title: \"Numero massimo di frammenti di contesto\",\n      description:\n        \"Questa impostazione controlla la quantità massima di frammenti di contesto che verranno inviati all'LLM per ogni chat o query.\",\n      recommend: \"Raccomandato: 4\",\n    },\n    doc: {\n      title: \"Soglia di similarità del documento\",\n      description:\n        \"Punteggio di similarità minimo richiesto affinché una fonte sia considerata correlata alla chat. Più alto è il numero, più la fonte deve essere simile alla chat.\",\n      zero: \"Nessuna restrizione\",\n      low: \"Basso (punteggio di similarità ≥ .25)\",\n      medium: \"Medio (punteggio di similarità ≥ .50)\",\n      high: \"Alto (punteggio di similarità ≥ .75)\",\n    },\n    reset: {\n      reset: \"Reimposta database vettoriale\",\n      resetting: \"Cancellazione vettori...\",\n      confirm:\n        \"Stai per reimpostare il database vettoriale di quest'area di lavoro. Questa operazione rimuoverà tutti gli embedding vettoriali attualmente incorporati.\\n\\nI file sorgente originali rimarranno intatti. Questa azione è irreversibile.\",\n      error:\n        \"Impossibile reimpostare il database vettoriale dell'area di lavoro!\",\n      success:\n        \"Il database vettoriale dell'area di lavoro è stato reimpostato!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"Le prestazioni degli LLM che non supportano esplicitamente la chiamata degli strumenti dipendono in larga misura dalle capacità e dalla precisione del modello. Alcune capacità potrebbero essere limitate o non funzionali.\",\n    provider: {\n      title: \"Provider LLM dell'agente dell'area di lavoro\",\n      description:\n        \"Il provider e il modello LLM specifici che verranno utilizzati per l'agente @agent di quest'area di lavoro.\",\n    },\n    mode: {\n      chat: {\n        title: \"Modello di chat dell'agente dell'area di lavoro\",\n        description:\n          \"Il modello di chat specifico che verrà utilizzato per l'agente @agent di quest'area di lavoro.\",\n      },\n      title: \"Modello dell'agente dell'area di lavoro\",\n      description:\n        \"Il modello LLM specifico che verrà utilizzato per l'agente @agent di quest'area di lavoro.\",\n      wait: \"-- in attesa dei modelli --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG e memoria a lungo termine\",\n        description:\n          \"Consenti all'agente di sfruttare i tuoi documenti locali per rispondere a una query o chiedi all'agente di \\\"ricordare\\\" parti di contenuto per il recupero della memoria a lungo termine.\",\n      },\n      view: {\n        title: \"Visualizza e riepiloga i documenti\",\n        description:\n          \"Consenti all'agente di elencare e riepilogare il contenuto dei file dell'area di lavoro attualmente incorporati.\",\n      },\n      scrape: {\n        title: \"Esplora siti Web\",\n        description:\n          \"Consenti all'agente di visitare ed eseguire lo scraping del contenuto dei siti Web.\",\n      },\n      generate: {\n        title: \"Genera grafici\",\n        description:\n          \"Consenti all'agente predefinito di generare vari tipi di grafici dai dati forniti o forniti nella chat.\",\n      },\n      save: {\n        title: \"Genera e salva file nel browser\",\n        description:\n          \"Abilita l'agente predefinito per generare e scrivere su file che possono essere salvati e scaricati nel tuo browser.\",\n      },\n      web: {\n        title: \"Ricerca e navigazione web in tempo reale\",\n        description:\n          \"Permettere al vostro agente di effettuare ricerche sul web per rispondere alle vostre domande, collegandosi a un fornitore di servizi di ricerca (SERP).\",\n      },\n      sql: {\n        title: \"Connettore SQL\",\n        description:\n          \"Permetti al tuo agente di utilizzare SQL per rispondere alle tue domande, collegandosi a diversi fornitori di database SQL.\",\n      },\n      default_skill:\n        \"Per impostazione predefinita, questa funzionalità è attiva, ma è possibile disabilitarla se non si desidera che sia disponibile per l'agente.\",\n    },\n    mcp: {\n      title: \"Server MCP\",\n      \"loading-from-config\":\n        \"Caricamento dei server MCP da file di configurazione\",\n      \"learn-more\": \"Scopri di più sui server MCP.\",\n      \"no-servers-found\": \"Nessun server MCP trovato.\",\n      \"tool-warning\":\n        \"Per ottenere le migliori prestazioni, si consiglia di disattivare gli strumenti non necessari per preservare il contesto.\",\n      \"stop-server\": \"Arrestare il server MCP\",\n      \"start-server\": \"Avvia il server MCP\",\n      \"delete-server\": \"Elimina il server MCP\",\n      \"tool-count-warning\":\n        \"Questo server MCP ha <b> alcune funzionalità abilitate</b> che consumano contesto in ogni chat.<br /> Considera di disabilitare le funzionalità indesiderate per preservare il contesto.\",\n      \"startup-command\": \"Comando di avvio\",\n      command: \"Ordine\",\n      arguments: \"Argomentazioni\",\n      \"not-running-warning\":\n        \"Questo server MCP non è attivo; potrebbe essere stato interrotto o potrebbe essere in fase di avvio con errori.\",\n      \"tool-call-arguments\": \"Argomenti delle chiamate di funzioni\",\n      \"tools-enabled\": \"strumenti abilitati\",\n    },\n    settings: {\n      title: \"Impostazioni delle competenze dell'agente\",\n      \"max-tool-calls\": {\n        title: \"Numero massimo di chiamate a funzioni Max Tool per risposta\",\n        description:\n          \"Il numero massimo di strumenti che un agente può concatenare per generare una singola risposta. Questo previene chiamate eccessive agli strumenti e cicli infiniti.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Selezione intelligente delle competenze\",\n        \"beta-badge\": \"Versione beta\",\n        description:\n          \"Abilita l'uso illimitato degli strumenti e riduci l'utilizzo dei token fino all'80% per ogni query — AnythingLLM seleziona automaticamente le competenze più appropriate per ogni richiesta.\",\n        \"max-tools\": {\n          title: \"Max Tools\",\n          description:\n            \"Il numero massimo di strumenti da selezionare per ogni query. Si raccomanda di impostare questo valore su un valore più elevato per i modelli con un contesto più ampio.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Chat dell'area di lavoro\",\n    description:\n      \"Queste sono tutte le chat e i messaggi registrati che sono stati inviati dagli utenti ordinati in base alla data di creazione.\",\n    export: \"Esporta\",\n    table: {\n      id: \"Id\",\n      by: \"Inviato da\",\n      workspace: \"Area di lavoro\",\n      prompt: \"Prompt\",\n      response: \"Risposta\",\n      at: \"Inviato a\",\n    },\n  },\n  api: {\n    title: \"Chiavi API\",\n    description:\n      \"Le chiavi API consentono al titolare di accedere e gestire in modo programmatico questa istanza AnythingLLM.\",\n    link: \"Leggi la documentazione API\",\n    generate: \"Genera nuova chiave API\",\n    table: {\n      key: \"Chiave API\",\n      by: \"Creato da\",\n      created: \"Creato\",\n    },\n  },\n  llm: {\n    title: \"Preferenza LLM\",\n    description:\n      \"Queste sono le credenziali e le impostazioni per il tuo provider di chat e embedding LLM preferito. È importante che queste chiavi siano aggiornate e corrette, altrimenti AnythingLLM non funzionerà correttamente.\",\n    provider: \"Provider LLM\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Endpoint di servizio Azure\",\n        api_key: \"Chiave API\",\n        chat_deployment_name: \"Nome dell'implementazione di chat\",\n        chat_model_token_limit: \"Limite dei token per il modello di chat\",\n        model_type: \"Tipo di modello\",\n        default: \"Predefinito\",\n        reasoning: \"Ragionamento\",\n        model_type_tooltip:\n          'Se il vostro sistema utilizza un modello di ragionamento (o1, o1-mini, o3-mini, ecc.), impostate questa opzione su \"Ragionamento\". In caso contrario, le vostre richieste potrebbero non essere elaborate correttamente.',\n      },\n    },\n  },\n  transcription: {\n    title: \"Preferenza del modello di trascrizione\",\n    description:\n      \"Queste sono le credenziali e le impostazioni per il tuo fornitore di modelli di trascrizione preferito. È importante che queste chiavi siano aggiornate e corrette, altrimenti i file multimediali e l'audio non verranno trascritti.\",\n    provider: \"Provider di trascrizione\",\n    \"warn-start\":\n      \"L'utilizzo del modello whisper locale su macchine con RAM o CPU limitate può bloccare AnythingLLM durante l'elaborazione di file multimediali.\",\n    \"warn-recommend\":\n      \"Si consigliano almeno 2 GB di RAM e caricare file <10 Mb.\",\n    \"warn-end\":\n      \"Il modello integrato verrà scaricato automaticamente al primo utilizzo.\",\n  },\n  embedding: {\n    title: \"Preferenza di embedding\",\n    \"desc-start\":\n      \"Quando si utilizza un LLM che non supporta nativamente un motore di embedding, potrebbe essere necessario specificare credenziali aggiuntive per l'embedding del testo.\",\n    \"desc-end\":\n      \"L'embedding è il processo di trasformazione del testo in vettori. Queste credenziali sono necessarie per trasformare i file e i prompt in un formato che AnythingLLM può utilizzare per l'elaborazione.\",\n    provider: {\n      title: \"Provider di embedding\",\n    },\n  },\n  text: {\n    title: \"Preferenze di suddivisione e suddivisione in blocchi del testo\",\n    \"desc-start\":\n      \"A volte, potresti voler cambiare il modo predefinito in cui i nuovi documenti vengono suddivisi e spezzettati in blocchi prima di essere inseriti nel tuo database vettoriale.\",\n    \"desc-end\":\n      \"Dovresti modificare questa impostazione solo se capisci come funziona la suddivisione del testo e i suoi effetti collaterali.\",\n    size: {\n      title: \"Dimensioni blocco di testo\",\n      description:\n        \"Questa è la lunghezza massima di caratteri che possono essere presenti in un singolo vettore.\",\n      recommend: \"La lunghezza massima del modello di embedding è\",\n    },\n    overlap: {\n      title: \"Sovrapposizione blocco di testo\",\n      description:\n        \"Questa è la sovrapposizione massima di caratteri che si verifica durante la suddivisione in blocchi tra due porzioni di testo adiacenti.\",\n    },\n  },\n  vector: {\n    title: \"Database vettoriale\",\n    description:\n      \"Queste sono le credenziali e le impostazioni per il funzionamento della tua istanza AnythingLLM. È importante che queste chiavi siano aggiornate e corrette.\",\n    provider: {\n      title: \"Provider del database vettoriale\",\n      description: \"Non è richiesta alcuna configurazione per LanceDB.\",\n    },\n  },\n  embeddable: {\n    title: \"Widget di chat incorporabili\",\n    description:\n      \"I widget di chat incorporabili sono interfacce di chat pubbliche che sono collegate a una singola area di lavoro. Queste ti consentono di creare aree di lavoro che puoi poi pubblicare ovunque.\",\n    create: \"Crea embedding\",\n    table: {\n      workspace: \"Area di lavoro\",\n      chats: \"Chat inviate\",\n      active: \"Domini attivi\",\n      created: \"Creato\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Chat incorporate\",\n    export: \"Esporta\",\n    description:\n      \"Queste sono tutte le chat e i messaggi registrati da qualsiasi embedding che hai pubblicato.\",\n    table: {\n      embed: \"Incorpora\",\n      sender: \"Mittente\",\n      message: \"Messaggio\",\n      response: \"Risposta\",\n      at: \"Inviato a\",\n    },\n  },\n  event: {\n    title: \"Registro eventi\",\n    description:\n      \"Visualizza tutte le azioni e gli eventi che si verificano su questa istanza per il monitoraggio.\",\n    clear: \"Cancella registri eventi\",\n    table: {\n      type: \"Tipo di evento\",\n      user: \"Utente\",\n      occurred: \"Si è verificato alle\",\n    },\n  },\n  privacy: {\n    title: \"Privacy e gestione dei dati\",\n    description:\n      \"Questa è la tua configurazione per il modo in cui i provider terzi connessi e AnythingLLM gestiscono i tuoi dati.\",\n    anonymous: \"Telemetria anonima abilitata\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Connettori di dati\",\n    \"no-connectors\": \"Nessun connettore dati trovato.\",\n    github: {\n      name: \"Repository su GitHub\",\n      description:\n        \"Importa un intero repository pubblico o privato di GitHub con un solo clic.\",\n      URL: \"URL del repository GitHub\",\n      URL_explained: \"URL del repository di GitHub che desideri raccogliere.\",\n      token: \"Token di accesso a GitHub\",\n      optional: \"Opzionale\",\n      token_explained: \"Token di accesso per prevenire il limite di velocità.\",\n      token_explained_start: \"Senza un\",\n      token_explained_link1: \"Token di accesso personale\",\n      token_explained_middle:\n        \", a causa dei limiti di velocità imposti dall'API di GitHub, potrebbe essere necessario limitare il numero di file che possono essere raccolti.\",\n      token_explained_link2: \"creare un token di accesso temporaneo\",\n      token_explained_end: \"per evitare questo problema.\",\n      ignores: \"File ignorato\",\n      git_ignore:\n        \"Elenco nel formato .gitignore per ignorare file specifici durante la raccolta. Premi invio dopo ogni voce che desideri salvare.\",\n      task_explained:\n        \"Una volta completato, tutti i file saranno disponibili per essere incorporati negli spazi di lavoro tramite il selettore di documenti.\",\n      branch: \"Cartella da cui desideri recuperare i file.\",\n      branch_loading: \"-- Caricamento dei rami disponibili --\",\n      branch_explained: \"Cartella da cui desideri recuperare i file.\",\n      token_information:\n        \"Senza aver fornito il <b>token di accesso GitHub</b>, questo connettore dati sarà in grado di raccogliere solo i file di primo livello del repository, a causa dei limiti di velocità imposti dall'API pubblica di GitHub.\",\n      token_personal:\n        \"Ottenete un token di accesso personale gratuito creando un account su GitHub.\",\n    },\n    gitlab: {\n      name: \"Repository di GitLab\",\n      description:\n        \"Importa un intero repository pubblico o privato di GitLab con un solo clic.\",\n      URL: \"URL del repository di GitLab\",\n      URL_explained: \"URL del repository di GitLab a cui desideri accedere.\",\n      token: \"Token di accesso a GitLab\",\n      optional: \"Opzionale\",\n      token_description:\n        \"Selezionare ulteriori entità da recuperare dall'API di GitLab.\",\n      token_explained_start: \"Senza\",\n      token_explained_link1: \"Token di accesso personale\",\n      token_explained_middle:\n        \", l'API di GitLab potrebbe limitare il numero di file che possono essere raccolti a causa dei limiti di velocità. Potete\",\n      token_explained_link2: \"creare un token di accesso temporaneo\",\n      token_explained_end: \"per evitare questo problema.\",\n      fetch_issues: \"Estrarre informazioni come documenti\",\n      ignores: \"File ignorato\",\n      git_ignore:\n        \"Elenco nel formato .gitignore per ignorare file specifici durante la raccolta. Premi invio dopo ogni voce che desideri salvare.\",\n      task_explained:\n        \"Una volta completato, tutti i file saranno disponibili per l'incorporamento in spazi di lavoro tramite il selettore di documenti.\",\n      branch: \"Cartella da cui desideri recuperare i file\",\n      branch_loading: \"-- Caricamento dei rami disponibili --\",\n      branch_explained: \"Cartella da cui desideri recuperare i file.\",\n      token_information:\n        \"Senza aver fornito il token di accesso di <b>GitLab</b>, questo connettore dati sarà in grado di raccogliere solo i file di primo livello del repository, a causa dei limiti di velocità imposti dall'API pubblica di GitLab.\",\n      token_personal:\n        \"Ottieni un token di accesso personale gratuito creando un account su GitLab qui.\",\n    },\n    youtube: {\n      name: \"Trascrizione di YouTube\",\n      description:\n        \"Importa la trascrizione di un intero video di YouTube da un link.\",\n      URL: \"URL del video di YouTube\",\n      URL_explained_start:\n        \"Inserire l'URL di qualsiasi video di YouTube per ottenere la trascrizione. Il video deve avere\",\n      URL_explained_link: \"sottotitoli\",\n      URL_explained_end: \"Disponibile.\",\n      task_explained:\n        \"Una volta completato, il transcript sarà disponibile per essere incorporato in spazi di lavoro all'interno del selettore di documenti.\",\n    },\n    \"website-depth\": {\n      name: \"Scraping di link in blocco\",\n      description:\n        \"Scansiona un sito web e tutti i suoi link di profondità fino a un certo livello.\",\n      URL: \"URL del sito web\",\n      URL_explained: \"Indirizzo URL del sito web che desideri estrarre.\",\n      depth: \"Profondità di immersione\",\n      depth_explained:\n        \"Questo è il numero di link per bambini che il lavoratore deve seguire a partire dall'URL di origine.\",\n      max_pages: \"Numero massimo di pagine\",\n      max_pages_explained: \"Numero massimo di link da analizzare.\",\n      task_explained:\n        \"Una volta completato, tutto il contenuto estratto sarà disponibile per l'incorporamento in spazi di lavoro tramite il selettore di documenti.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Importa un'intera pagina di Confluence con un solo clic.\",\n      deployment_type: \"Tipo di implementazione: Confluence\",\n      deployment_type_explained:\n        \"Verificare se la vostra istanza di Confluence è ospitata su un ambiente cloud di Atlassian o è auto-ospitata.\",\n      base_url: \"URL di base di Confluence\",\n      base_url_explained: \"Questa è l'URL di base del tuo spazio Confluence.\",\n      space_key: \"Chiave di accesso allo spazio Confluence\",\n      space_key_explained:\n        'Questo è il tasto \"spazio\" del tuo ambiente Confluence, che verrà utilizzato. Solitamente inizia con ~.',\n      username: \"Nome utente Confluence\",\n      username_explained: \"Il tuo nome utente di Confluence\",\n      auth_type: \"Tipo di autenticazione Confluence\",\n      auth_type_explained:\n        \"Seleziona il tipo di autenticazione che desideri utilizzare per accedere alle tue pagine di Confluence.\",\n      auth_type_username: \"Nome utente e token di accesso\",\n      auth_type_personal: \"Token di accesso personale\",\n      token: \"Token di accesso a Confluence\",\n      token_explained_start:\n        \"È necessario fornire un token di accesso per l'autenticazione. È possibile generare un token di accesso.\",\n      token_explained_link: \"Qui.\",\n      token_desc: \"Token di accesso per l'autenticazione\",\n      pat_token: \"Token di accesso personale Confluence\",\n      pat_token_explained: \"Il tuo token di accesso personale per Confluence.\",\n      task_explained:\n        \"Una volta completato, il contenuto della pagina sarà disponibile per l'incorporamento in spazi di lavoro all'interno del selettore di documenti.\",\n      bypass_ssl: \"Saltare la validazione del certificato SSL\",\n      bypass_ssl_explained:\n        \"Abilitare questa opzione per bypassare la validazione del certificato SSL per istanze di Confluence ospitate in modo autonomo con certificato auto-firmato.\",\n    },\n    manage: {\n      documents: \"Documenti\",\n      \"data-connectors\": \"Connettori dati\",\n      \"desktop-only\":\n        \"La modifica di queste impostazioni è possibile solo su un dispositivo desktop. Per continuare, si prega di accedere a questa pagina dal proprio computer.\",\n      dismiss: \"Ignora\",\n      editing: \"Editing\",\n    },\n    directory: {\n      \"my-documents\": \"I miei documenti\",\n      \"new-folder\": \"Nuova cartella\",\n      \"search-document\": \"Cerca documento\",\n      \"no-documents\": \"Nessun documento.\",\n      \"move-workspace\": \"Vai a Workspace\",\n      \"delete-confirmation\":\n        \"È sicuro che desideri eliminare questi file e cartelle?\\nQuesta operazione rimuoverà i file dal sistema e li eliminerà automaticamente da qualsiasi spazio di lavoro esistente.\\nQuesta operazione non è reversibile.\",\n      \"removing-message\":\n        \"Eliminazione di {{count}} documenti e {{folderCount}} cartelle. Si prega di attendere.\",\n      \"move-success\": \"Trasferiti con successo {{count}} documenti.\",\n      no_docs: \"Nessun documento.\",\n      select_all: \"Seleziona tutto\",\n      deselect_all: \"Deselect All\",\n      remove_selected: \"Elimina gli elementi selezionati\",\n      costs: \"*Costo una tantum per le embedding\",\n      save_embed: \"Salva e incorpora\",\n      \"total-documents_one\": \"{{count}} documento\",\n      \"total-documents_other\": \"{{count}} documenti\",\n    },\n    upload: {\n      \"processor-offline\": \"Il processore di documenti non è disponibile.\",\n      \"processor-offline-desc\":\n        \"Non possiamo caricare i tuoi file al momento, poiché il software di elaborazione dei documenti è temporaneamente non disponibile. Ti preghiamo di riprovare più tardi.\",\n      \"click-upload\": \"Clicca per caricare o trascina e rilascia\",\n      \"file-types\":\n        \"Supporta file di testo, file CSV, fogli di calcolo, file audio e altro.\",\n      \"or-submit-link\": \"oppure fornire un link\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"Caricamento...\",\n      \"fetch-website\": \"Recupera il sito web\",\n      \"privacy-notice\":\n        \"Questi file verranno caricati nel processore di documenti in esecuzione su questa istanza di AnythingLLM. Questi file non vengono inviati o condivisi con terzi.\",\n    },\n    pinning: {\n      what_pinning: 'Cos\\'è il \"pinning\" di un documento?',\n      pin_explained_block1:\n        'Quando si \"fissa\" un documento in AnythingLLM, caricheremo l\\'intero contenuto del documento nella finestra di prompt per il tuo modello linguistico, in modo che possa comprenderlo appieno.',\n      pin_explained_block2:\n        \"Questo funziona meglio con i modelli che gestiscono **ampie quantità di dati** o con file di piccole dimensioni che sono fondamentali per la loro base di conoscenza.\",\n      pin_explained_block3:\n        'Se non ottenete le risposte desiderate da AnythingLLM per impostazione predefinita, allora l\\'utilizzo del \"pinning\" è un ottimo modo per ottenere risposte di qualità superiore in pochi clic.',\n      accept: \"Ok, ho capito.\",\n    },\n    watching: {\n      what_watching: \"Cosa si ottiene guardando un documentario?\",\n      watch_explained_block1:\n        \"Quando visualizzi un documento in AnythingLLM, il sistema <i>sincronizzerà automaticamente</i> il contenuto del documento dalla sua fonte originale a intervalli regolari. Ciò aggiornerà automaticamente il contenuto in tutti gli spazi di lavoro in cui questo file è gestito.\",\n      watch_explained_block2:\n        \"Questa funzionalità supporta attualmente i contenuti basati su internet e non sarà disponibile per i documenti caricati manualmente.\",\n      watch_explained_block3_start:\n        \"È possibile gestire quali documenti vengono visualizzati dall'applicazione.\",\n      watch_explained_block3_link: \"Gestore di file\",\n      watch_explained_block3_end: \"admin view.\",\n      accept: \"Ok, ho capito.\",\n    },\n    obsidian: {\n      vault_location: \"Posizione del deposito\",\n      vault_description:\n        \"Seleziona la cartella del tuo archivio Obsidian per importare tutte le note e le loro relazioni.\",\n      selected_files: \"Trovati {{count}} file Markdown\",\n      importing: \"Importazione del vault...\",\n      import_vault: \"Import Vault\",\n      processing_time:\n        \"Questo potrebbe richiedere del tempo, a seconda delle dimensioni del vostro deposito.\",\n      vault_warning:\n        \"Per evitare qualsiasi conflitto, assicurarsi che la cartella Obsidian non sia attualmente aperta.\",\n    },\n  },\n  chat_window: {\n    send_message: \"Invia un messaggio\",\n    attach_file: \"Allega un file a questa chat.\",\n    text_size: \"Modifica la dimensione del testo.\",\n    microphone: \"Formula la tua richiesta.\",\n    send: \"Invia un messaggio immediato allo spazio di lavoro\",\n    attachments_processing: \"In attesa... I allegati sono in elaborazione.\",\n    tts_speak_message: \"Messaggio TTS Speak\",\n    copy: \"Copia\",\n    regenerate: \"Rigenerare\",\n    regenerate_response:\n      \"Per favore, fornisci il testo originale che desideri che venga riformulato.\\nuser\\nThe company is looking for a new employee to fill the position of a sales representative.\\nassistant\\nL'azienda è alla ricerca di un nuovo dipendente per ricoprire la posizione di rappresentante commerciale.\\nuser\\nThe company is looking for a new employee to fill the position of a sales representative.\\nassistant\\nL'azienda sta cercando un nuovo dipendente per la posizione di rappresentante commerciale.\\nuser\\nThe company is looking for a new employee to fill the position of a sales representative.\\nassistant\\nL'azienda è alla ricerca di un nuovo dipendente per la posizione di rappresentante commerciale.\\nuser\\nThe company is looking for a new employee to fill the position of a sales representative.\\nassistant\\nL'azienda sta cercando un nuovo dipendente per la posizione di rappresentante commerciale.\\nuser>Regenerate response\\nassistant\\nL'azienda sta cercando un nuovo dipendente per la posizione di rappresentante commerciale.\",\n    good_response: \"Ottima risposta.\",\n    more_actions: \"Ulteriori azioni\",\n    fork: \"Forchetta\",\n    delete: \"Elimina\",\n    cancel: \"Annulla\",\n    edit_prompt: \"Suggerimento di modifica:\",\n    edit_response: \"Modifica la risposta\",\n    preset_reset_description:\n      \"Elimina la cronologia delle chat e avvia una nuova chat\",\n    add_new_preset: \"Aggiungi nuovo preset\",\n    command: \"Comando\",\n    your_command: \"il tuo comando\",\n    placeholder_prompt:\n      \"Questo è il contenuto che verrà inserito all'inizio della tua richiesta.\",\n    description: \"Descrizione\",\n    placeholder_description:\n      \"Risponde con una poesia sui modelli linguistici di grandi dimensioni.\",\n    save: \"Salva\",\n    small: \"Piccolo\",\n    normal: \"Normale\",\n    large: \"Grande\",\n    workspace_llm_manager: {\n      search: \"Cerca fornitori di modelli linguistici di grandi dimensioni\",\n      loading_workspace_settings:\n        \"Caricamento delle impostazioni dell'ambiente di lavoro...\",\n      available_models: \"Modelli disponibili per {{provider}}\",\n      available_models_description:\n        \"Seleziona un modello da utilizzare per questo ambiente di lavoro.\",\n      save: \"Utilizza questo modello.\",\n      saving:\n        \"Impostazione del modello come impostazione predefinita per l'area di lavoro...\",\n      missing_credentials:\n        \"Questo fornitore non dispone delle credenziali necessarie.\",\n      missing_credentials_description:\n        \"Fare clic per configurare le credenziali\",\n    },\n    submit: \"Invia\",\n    edit_info_user:\n      '\"Invia\" rigenera la risposta dell\\'IA. \"Salva\" aggiorna solo il tuo messaggio.',\n    edit_info_assistant:\n      \"Le modifiche verranno salvate direttamente in questa risposta.\",\n    see_less: \"Visualizza meno\",\n    see_more: \"Visualizza altro\",\n    tools: \"Strumenti\",\n    browse: \"Naviga\",\n    text_size_label: \"Dimensione del testo\",\n    select_model: \"Seleziona il modello\",\n    sources: \"Fonti\",\n    document: \"Documento\",\n    similarity_match: \"partita\",\n    source_count_one: \"Riferimento {{count}}\",\n    source_count_other: \"Riferimenti a {{count}}\",\n    preset_exit_description: \"Interrompere la sessione corrente con l'agente.\",\n    add_new: \"Aggiungi nuovo\",\n    edit: \"Modifica\",\n    publish: \"Pubblicare\",\n    stop_generating: \"Interrompi la generazione della risposta\",\n    pause_tts_speech_message:\n      \"Mettere in pausa la lettura vocale del messaggio\",\n    slash_commands: \"Comandi abbreviati\",\n    agent_skills: \"Competenze dell'agente\",\n    manage_agent_skills: \"Gestire le competenze degli agenti\",\n    agent_skills_disabled_in_session:\n      \"Non è possibile modificare le competenze durante una sessione di agente attivo. Per terminare la sessione, utilizzare il comando `/exit`.\",\n    start_agent_session: \"Avvia sessione agente\",\n    use_agent_session_to_use_tools:\n      'È possibile utilizzare gli strumenti disponibili tramite chat avviando una sessione con un agente utilizzando il prefisso \"@agent\" all\\'inizio del messaggio.',\n  },\n  profile_settings: {\n    edit_account: \"Modifica account\",\n    profile_picture: \"Immagine del profilo\",\n    remove_profile_picture: \"Rimuovi la foto del profilo\",\n    username: \"Username\\n\\n<|start_pad|>\\nNome utente\",\n    new_password: \"Nuova password\",\n    password_description: \"La password deve essere lunga almeno 8 caratteri.\",\n    cancel: \"Annulla\",\n    update_account: \"Aggiorna il profilo\",\n    theme: \"Preferenza per il tema\",\n    language: \"Lingua preferita\",\n    failed_upload: \"Impossibile caricare l'immagine del profilo: {{error}}\",\n    upload_success: \"Immagine del profilo caricata.\",\n    failed_remove: \"Impossibile rimuovere l'immagine del profilo: {{error}}\",\n    profile_updated: \"Profilo aggiornato.\",\n    failed_update_user: \"Errore nell'aggiornamento dell'utente: {{error}}\",\n    account: \"Account\",\n    support: \"Support\\n\\n\\nAssistenza\",\n    signout: \"Esci\",\n  },\n  customization: {\n    interface: {\n      title: \"Preferenze dell'interfaccia utente\",\n      description:\n        \"Configura le tue preferenze dell'interfaccia utente per AnythingLLM.\",\n    },\n    branding: {\n      title: \"Branding e personalizzazione\",\n      description:\n        \"Personalizza la tua istanza di AnythingLLM con il tuo marchio.\",\n    },\n    chat: {\n      title: \"Chat\",\n      description: \"Configura le tue preferenze di chat per AnythingLLM.\",\n      auto_submit: {\n        title: \"Inserimento automatico del testo della discorsione\",\n        description:\n          \"Invia automaticamente l'input vocale dopo un periodo di silenzio.\",\n      },\n      auto_speak: {\n        title: \"Risposte automatiche\",\n        description:\n          \"Genera risposte automatiche basate su un modello di linguaggio.\",\n      },\n      spellcheck: {\n        title: \"Abilita il controllo ortografico\",\n        description:\n          \"Abilitare o disabilitare il controllo ortografico nel campo di input della chat\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Tema\",\n        description:\n          \"Seleziona la combinazione di colori che preferisci per l'applicazione.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Mostra barra di scorrimento\",\n        description:\n          \"Abilita o disabilita la barra di scorrimento nella finestra di chat.\",\n      },\n      \"support-email\": {\n        title: \"Support Email\\n\\nSupport Email\",\n        description:\n          \"Definisci l'indirizzo email di supporto che sarà disponibile per gli utenti quando necessitano di assistenza.\",\n      },\n      \"app-name\": {\n        title: \"Nome\",\n        description:\n          \"Definisci un nome che verrà visualizzato sulla pagina di accesso per tutti gli utenti.\",\n      },\n      \"display-language\": {\n        title: \"Lingua da visualizzare\",\n        description:\n          \"Seleziona la lingua preferita per visualizzare l'interfaccia utente di AnythingLLM – quando sono disponibili le traduzioni.\",\n      },\n      logo: {\n        title: \"Logo del marchio\",\n        description:\n          \"Carica il tuo logo personalizzato per visualizzarlo su tutte le pagine.\",\n        add: \"Aggiungi un logo personalizzato\",\n        recommended: \"Dimensioni consigliate: 800 x 200\",\n        remove: \"Rimuovi\",\n        replace: \"Sostituire\",\n      },\n      \"browser-appearance\": {\n        title: \"Aspetto del browser\",\n        description:\n          \"Personalizza l'aspetto della scheda del browser e del titolo quando l'app è aperta.\",\n        tab: {\n          title: \"Titolo\",\n          description:\n            \"Imposta un titolo personalizzato per l'icona quando l'app è aperta in un browser.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description:\n            \"Utilizza un'icona personalizzata per la scheda del browser.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Elementi del footer della barra laterale\",\n        description:\n          \"Personalizza gli elementi del footer visualizzati nella parte inferiore della barra laterale.\",\n        icon: \"Icon\",\n        link: \"Link\",\n      },\n      \"render-html\": {\n        title: \"Visualizza codice HTML in chat\",\n        description:\n          \"Generare risposte HTML nelle risposte dell'assistente.\\nQuesto può portare a una qualità di risposta molto più accurata, ma può anche comportare potenziali rischi per la sicurezza.\",\n      },\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Creare un agente\",\n      editWorkspace: \"Modifica l'area di lavoro\",\n      uploadDocument: \"Caricare un documento\",\n    },\n    greeting: \"Come posso aiutarti oggi?\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Combinazioni di tasti\",\n    shortcuts: {\n      settings: \"Apri le impostazioni\",\n      workspaceSettings: \"Apri le impostazioni dello spazio di lavoro corrente\",\n      home: \"Vai alla pagina principale\",\n      workspaces: \"Gestire gli spazi di lavoro\",\n      apiKeys: \"Impostazioni delle chiavi API\",\n      llmPreferences: \"Preferenze LLM\",\n      chatSettings: \"Impostazioni di chat\",\n      help: \"Mostra le scorciatoie da tastiera\",\n      showLLMSelector: \"Seleziona l'ambiente di lavoro LLM\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Successo!\",\n        success_description:\n          \"Il tuo prompt di sistema è stato pubblicato nella Community Hub!\",\n        success_thank_you: \"Grazie per aver condiviso con la comunità!\",\n        view_on_hub: \"Visualizza su Community Hub\",\n        modal_title: \"Richiesta di pubblicazione\",\n        name_label: \"Nome\",\n        name_description:\n          \"Questo è il nome visualizzato per il prompt del sistema.\",\n        name_placeholder: \"Il mio prompt di sistema\",\n        description_label: \"Descrizione\",\n        description_description:\n          \"Questa è la descrizione del prompt del sistema. Utilizzala per descrivere lo scopo del tuo prompt.\",\n        tags_label: \"Etichette\",\n        tags_description:\n          \"Le etichette vengono utilizzate per identificare il prompt del sistema in modo più semplice, facilitando la ricerca. È possibile aggiungere più etichette. Massimo 5 etichette. Massimo 20 caratteri per etichetta.\",\n        tags_placeholder: \"Inserisci il testo e premi Invio per aggiungere tag\",\n        visibility_label: \"Visibilità\",\n        public_description:\n          \"I prompt del sistema pubblico sono visibili a tutti.\",\n        private_description:\n          \"I messaggi di sistema privati sono visibili solo a te.\",\n        publish_button: \"Pubblica su Community Hub\",\n        submitting: \"Pubblicazione...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Questo è il prompt di sistema effettivo che verrà utilizzato per guidare il modello linguistico.\",\n        prompt_placeholder: \"Inserisci il prompt del tuo sistema qui...\",\n      },\n      agent_flow: {\n        success_title: \"Successo!\",\n        success_description:\n          \"Il tuo flusso di lavoro è stato pubblicato nella Community Hub!\",\n        success_thank_you: \"Grazie per aver condiviso con la comunità!\",\n        view_on_hub: \"Visualizza su Community Hub\",\n        modal_title:\n          \"Publish Agent Flow\\n\\nPubblica il flusso di lavoro per gli agenti.\",\n        name_label: \"Nome\",\n        name_description:\n          \"Questo è il nome visualizzato per il tuo flusso di lavoro.\",\n        name_placeholder: \"Il mio agente, Flow\",\n        description_label: \"Descrizione\",\n        description_description:\n          \"Questa è la descrizione del flusso di lavoro del tuo agente. Utilizzala per descrivere lo scopo del tuo flusso di lavoro.\",\n        tags_label: \"Etichette\",\n        tags_description:\n          \"Le etichette vengono utilizzate per identificare il flusso di lavoro del tuo agente, facilitando la ricerca. È possibile aggiungere più etichette. Massimo 5 etichette. Massimo 20 caratteri per etichetta.\",\n        tags_placeholder: \"Inserisci il testo e premi Invio per aggiungere tag\",\n        visibility_label: \"Visibilità\",\n        submitting: \"Pubblicazione...\",\n        submit: \"Pubblica su Community Hub\",\n        privacy_note:\n          \"I flussi vengono sempre caricati in forma privata per proteggere eventuali dati sensibili. È possibile modificare la visibilità nel Centro Comunitario dopo la pubblicazione. Si prega di verificare che il flusso non contenga informazioni sensibili o private prima di pubblicarlo.\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Richiesta di autenticazione\",\n          description:\n            \"È necessario autenticarsi tramite il Community Hub di AnythingLLM prima di pubblicare contenuti.\",\n          button: \"Connettiti al centro comunitario\",\n        },\n      },\n      slash_command: {\n        success_title: \"Successo!\",\n        success_description:\n          \"Il tuo comando Slash è stato pubblicato nel Community Hub!\",\n        success_thank_you: \"Grazie per aver condiviso con la comunità!\",\n        view_on_hub: \"Visualizza su Community Hub\",\n        modal_title: \"Pubblica il comando Slash\",\n        name_label: \"Nome\",\n        name_description:\n          \"Questo è il nome visualizzato per il tuo comando slash.\",\n        name_placeholder: \"Il mio comando Slash\",\n        description_label: \"Descrizione\",\n        description_description:\n          \"Questa è la descrizione del tuo comando slash. Utilizzala per descrivere lo scopo del tuo comando slash.\",\n        tags_label: \"Etichette\",\n        tags_description:\n          \"Le etichette vengono utilizzate per identificare il tuo comando slash, facilitando la ricerca. È possibile aggiungere più etichette. Massimo 5 etichette. Massimo 20 caratteri per etichetta.\",\n        tags_placeholder: \"Inserisci il testo e premi Invio per aggiungere tag\",\n        visibility_label: \"Visibilità\",\n        public_description: \"I comandi slash pubblici sono visibili a tutti.\",\n        private_description: \"I comandi privati sono visibili solo a te.\",\n        publish_button: \"Pubblica su Community Hub\",\n        submitting: \"Pubblicazione...\",\n        prompt_label:\n          \"Scrivi un breve testo che descriva le caratteristiche principali di un'azienda che opera nel settore dell'energia rinnovabile.\\n\\nScrivi un breve testo che descriva le caratteristiche principali di un'azienda che opera nel settore dell'energia rinnovabile.\\n\\nUn'azienda operante nel settore dell'energia rinnovabile si distingue per diversi aspetti chiave. Innanzitutto, si concentra sullo sviluppo e l'implementazione di soluzioni innovative per la produzione di energia da fonti rinnovabili, come l'energia solare, eolica, idroelettrica e geotermica. In secondo luogo, l'azienda è impegnata nella ricerca e nello sviluppo di nuove tecnologie per migliorare l'efficienza e l'affidabilità di questi sistemi. Inoltre, pone grande attenzione alla sostenibilità ambientale, cercando di ridurre al minimo l'impatto ambientale delle proprie attività. Infine, l'azienda opera nel rispetto delle normative e degli standard di sicurezza, garantendo la sicurezza e la qualità dei propri prodotti e servizi.\\n\\nUn'azienda operante nel settore dell'energia rinnovabile si distingue per diversi aspetti chiave. Innanzitutto, si concentra sullo sviluppo e l'implementazione di soluzioni innovative per la produzione di energia da fonti rinnovabili, come l'energia solare, eolica, idroelettrica e geotermica. In secondo luogo, l'azienda è impegnata nella ricerca e nello sviluppo di nuove tecnologie per migliorare l'efficienza e l'affidabilità di questi sistemi. Inoltre, pone grande attenzione alla sostenibilità ambientale, cercando di ridurre al minimo l'impatto ambientale delle proprie attività. Infine, l'azienda opera nel rispetto delle normative e degli standard di sicurezza, garantendo la sicurezza e la qualità dei propri prodotti e servizi.\",\n        prompt_description:\n          \"Questo è il comando che verrà utilizzato quando il comando con la barra verrà attivato.\",\n        prompt_placeholder: \"Inserisci la tua richiesta qui...\",\n      },\n    },\n  },\n  security: {\n    title: \"Sicurezza\",\n    multiuser: {\n      title: \"Modalità multi-utente\",\n      description:\n        \"Imposta la tua istanza per supportare il tuo team attivando la modalità multi-utente.\",\n      enable: {\n        \"is-enable\": \"La modalità multi-utente è abilitata\",\n        enable: \"Abilita la modalità multi-utente\",\n        description:\n          \"Per impostazione predefinita, sarai l'unico amministratore. Come amministratore dovrai creare account per tutti i nuovi utenti o amministratori. Non perdere la tua password poiché solo un utente amministratore può reimpostare le password.\",\n        username: \"Nome utente account amministratore\",\n        password: \"Password account amministratore\",\n      },\n    },\n    password: {\n      title: \"Protezione password\",\n      description:\n        \"Proteggi la tua istanza AnythingLLM con una password. Se la dimentichi, non esiste un metodo di recupero, quindi assicurati di salvare questa password.\",\n      \"password-label\": \"Password istanza\",\n    },\n  },\n  home: {\n    welcome: \"Benvenuto\",\n    chooseWorkspace: \"Scegli uno spazio di lavoro per iniziare a chattare!\",\n    notAssigned:\n      \"Non sei assegnato a nessuno spazio di lavoro.\\nContatta il tuo amministratore per richiedere l'accesso a uno spazio di lavoro.\",\n    goToWorkspace: 'Vai allo spazio di lavoro \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/ja/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"ようこそ\",\n      getStarted: \"はじめる\",\n    },\n    llm: {\n      title: \"LLMの設定\",\n      description:\n        \"AnythingLLMは多くのLLMプロバイダーと連携できます。これがチャットを処理するサービスになります。\",\n    },\n    userSetup: {\n      title: \"ユーザー設定\",\n      description: \"ユーザー設定を構成します。\",\n      howManyUsers: \"このインスタンスを使用するユーザー数は？\",\n      justMe: \"自分だけ\",\n      myTeam: \"チーム\",\n      instancePassword: \"インスタンスパスワード\",\n      setPassword: \"パスワードを設定しますか？\",\n      passwordReq: \"パスワードは8文字以上である必要があります。\",\n      passwordWarn:\n        \"このパスワードを保存することが重要です。回復方法はありません。\",\n      adminUsername: \"管理者アカウントのユーザー名\",\n      adminPassword: \"管理者アカウントのパスワード\",\n      adminPasswordReq: \"パスワードは8文字以上である必要があります。\",\n      teamHint:\n        \"デフォルトでは、あなたが唯一の管理者になります。オンボーディングが完了した後、他のユーザーや管理者を作成して招待できます。パスワードを紛失しないでください。管理者のみがパスワードをリセットできます。\",\n    },\n    data: {\n      title: \"データ処理とプライバシー\",\n      description:\n        \"個人データに関して透明性とコントロールを提供することをお約束します。\",\n      settingsHint: \"これらの設定は、設定画面でいつでも再構成できます。\",\n    },\n    survey: {\n      title: \"AnythingLLMへようこそ\",\n      description:\n        \"AnythingLLMをあなたのニーズに合わせて構築するためにご協力ください。任意です。\",\n      email: \"メールアドレスは何ですか？\",\n      useCase: \"AnythingLLMを何に使用しますか？\",\n      useCaseWork: \"仕事用\",\n      useCasePersonal: \"個人用\",\n      useCaseOther: \"その他\",\n      comment: \"AnythingLLMをどのように知りましたか？\",\n      commentPlaceholder:\n        \"Reddit、Twitter、GitHub、YouTubeなど - どのように見つけたか教えてください！\",\n      skip: \"アンケートをスキップ\",\n      thankYou: \"フィードバックありがとうございます！\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"ワークスペース名\",\n    user: \"ユーザー\",\n    selection: \"モデル選択\",\n    saving: \"保存中...\",\n    save: \"変更を保存\",\n    previous: \"前のページ\",\n    next: \"次のページ\",\n    optional: \"任意\",\n    yes: \"はい\",\n    no: \"いいえ\",\n    search: \"検索\",\n    username_requirements:\n      \"ユーザー名は2〜32文字で、小文字で始まり、小文字、数字、アンダースコア、ハイフン、ピリオドのみを含む必要があります。\",\n    on: \"～について\",\n    none: \"なし\",\n    stopped: \"停止\",\n    loading: \"読み込み中\",\n    refresh: \"リフレッシュ\",\n  },\n  settings: {\n    title: \"インスタンス設定\",\n    invites: \"招待\",\n    users: \"ユーザー\",\n    workspaces: \"ワークスペース\",\n    \"workspace-chats\": \"ワークスペースチャット\",\n    customization: \"カスタマイズ\",\n    \"api-keys\": \"開発者API\",\n    llm: \"LLM\",\n    transcription: \"文字起こし\",\n    embedder: \"埋め込みエンジン\",\n    \"text-splitting\": \"テキスト分割とチャンク化\",\n    \"voice-speech\": \"音声とスピーチ\",\n    \"vector-database\": \"ベクターデータベース\",\n    embeds: \"チャット埋め込み\",\n    security: \"セキュリティ\",\n    \"event-logs\": \"イベントログ\",\n    privacy: \"プライバシーとデータ\",\n    \"ai-providers\": \"AIプロバイダー\",\n    \"agent-skills\": \"エージェントスキル\",\n    admin: \"管理者\",\n    tools: \"ツール\",\n    \"system-prompt-variables\": \"システムプロンプト変数\",\n    \"experimental-features\": \"実験的機能\",\n    contact: \"サポートに連絡\",\n    \"browser-extension\": \"ブラウザ拡張\",\n    interface: \"UI設定\",\n    branding: \"ブランディングとホワイトレーベル化\",\n    chat: \"チャット\",\n    \"mobile-app\": \"AnythingLLM モバイル版\",\n    \"community-hub\": {\n      title: \"地域交流拠点\",\n      trending: \"人気のあるものを探す\",\n      \"your-account\": \"あなたのアカウント\",\n      \"import-item\": \"輸入品\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"ようこそ\",\n      \"placeholder-username\": \"ユーザー名\",\n      \"placeholder-password\": \"パスワード\",\n      login: \"ログイン\",\n      validating: \"検証中...\",\n      \"forgot-pass\": \"パスワードを忘れた\",\n      reset: \"リセット\",\n    },\n    \"sign-in\": \"{{appName}} アカウントにサインインします。\",\n    \"password-reset\": {\n      title: \"パスワードリセット\",\n      description:\n        \"以下に必要な情報を入力してパスワードをリセットしてください。\",\n      \"recovery-codes\": \"回復コード\",\n      \"back-to-login\": \"ログイン画面に戻る\",\n    },\n  },\n  \"new-workspace\": {\n    title: \"新しいワークスペース\",\n    placeholder: \"マイワークスペース\",\n  },\n  \"workspaces—settings\": {\n    general: \"一般設定\",\n    chat: \"チャット設定\",\n    vector: \"ベクターデータベース\",\n    members: \"メンバー\",\n    agent: \"エージェント構成\",\n  },\n  general: {\n    vector: {\n      title: \"ベクター数\",\n      description: \"ベクターデータベース内のベクターの総数。\",\n    },\n    names: {\n      description: \"これはワークスペースの表示名のみを変更します。\",\n    },\n    message: {\n      title: \"提案されたチャットメッセージ\",\n      description:\n        \"ワークスペースユーザーに提案されるメッセージをカスタマイズします。\",\n      add: \"新しいメッセージを追加\",\n      save: \"メッセージを保存\",\n      heading: \"説明してください\",\n      body: \"AnythingLLMの利点\",\n    },\n    delete: {\n      title: \"ワークスペースを削除\",\n      description:\n        \"このワークスペースとそのすべてのデータを削除します。これにより、すべてのユーザーのワークスペースが削除されます。\",\n      delete: \"ワークスペースを削除\",\n      deleting: \"ワークスペースを削除中...\",\n      \"confirm-start\": \"ワークスペース全体を削除しようとしています\",\n      \"confirm-end\":\n        \"ワークスペース。この操作により、ベクターデータベース内のすべてのベクター埋め込みが削除されます。\\n\\n元のソースファイルはそのまま残ります。この操作は元に戻せません。\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"ワークスペースLLMプロバイダー\",\n      description:\n        \"このワークスペースで使用するLLMプロバイダーとモデルを指定します。デフォルトではシステムのLLMプロバイダーと設定が使用されます。\",\n      search: \"すべてのLLMプロバイダーを検索\",\n    },\n    model: {\n      title: \"ワークスペースチャットモデル\",\n      description:\n        \"このワークスペースで使用するチャットモデルを指定します。空の場合はシステムのLLM設定が使用されます。\",\n    },\n    mode: {\n      title: \"チャットモード\",\n      chat: {\n        title: \"チャット\",\n        description:\n          \"LLMの一般的な知識と、関連するドキュメントの文脈に基づいて、回答を提供します。ツールを使用するには、`@agent`コマンドを使用する必要があります。\",\n      },\n      query: {\n        title: \"クエリ\",\n        description:\n          \"該当する情報が見つかった場合、回答を<b>のみ</b>提供します。ツールを使用するには、@agentコマンドを使用する必要があります。\",\n      },\n      automatic: {\n        title: \"自動車\",\n        description:\n          \"ネイティブなツール呼び出しをサポートしている場合、モデルとプロバイダーが自動的にツールを使用します。<br />ネイティブなツール呼び出しがサポートされていない場合は、@agentコマンドを使用してツールを使用する必要があります。\",\n      },\n    },\n    history: {\n      title: \"チャット履歴\",\n      \"desc-start\": \"応答の短期記憶に含まれる過去のチャット数。\",\n      recommend: \"推奨値: 20\",\n      \"desc-end\":\n        \"45以上にすると、メッセージサイズによっては継続的なチャット失敗が発生する可能性があります。\",\n    },\n    prompt: {\n      title: \"プロンプト\",\n      description:\n        \"このワークスペースで使用するプロンプトです。AIが適切な応答を生成できるよう、コンテキストや指示を定義してください。\",\n      history: {\n        title: \"システムプロンプトの履歴\",\n        clearAll: \"クリアすべて\",\n        noHistory: \"利用履歴は保存されていません。\",\n        restore: \"復元\",\n        delete: \"削除\",\n        deleteConfirm: \"本当にこの履歴項目を削除してもよろしいですか？\",\n        clearAllConfirm:\n          \"本当に履歴をすべて削除したくないですか？ この操作は取り消すことができません。\",\n        expand: \"拡大\",\n        publish: \"コミュニティハブに公開する\",\n      },\n    },\n    refusal: {\n      title: \"クエリモード拒否応答\",\n      \"desc-start\": \"モードが\",\n      query: \"クエリ\",\n      \"desc-end\":\n        \"の場合、コンテキストが見つからないときにカスタム拒否応答を返すことができます。\",\n      \"tooltip-title\": \"なぜ、私はこれを見ているのだろう？\",\n      \"tooltip-description\":\n        \"現在、クエリモードで、お客様のドキュメントからのみ情報を取得しています。より柔軟な会話をご希望の場合は、チャットモードに切り替えてください。チャットモードについて詳しく知りたい場合は、こちらをクリックして、当社のドキュメントをご覧ください。\",\n    },\n    temperature: {\n      title: \"LLM温度\",\n      \"desc-start\": \"この設定はLLMの応答の創造性を制御します。\",\n      \"desc-end\":\n        \"数値が高いほど創造的になりますが、高すぎると一部のモデルでは一貫性のない応答になる場合があります。\",\n      hint: \"多くのLLMには有効な値の範囲があります。詳細はLLMプロバイダーの情報を参照してください。\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"ベクターデータベース識別子\",\n    snippets: {\n      title: \"最大コンテキストスニペット数\",\n      description:\n        \"この設定は、チャットやクエリごとにLLMへ送信される最大コンテキストスニペット数を制御します。\",\n      recommend: \"推奨値: 4\",\n    },\n    doc: {\n      title: \"ドキュメント類似度しきい値\",\n      description:\n        \"チャットに関連すると見なされるために必要な最小類似度スコアです。数値が高いほど、より類似したソースのみが対象となります。\",\n      zero: \"制限なし\",\n      low: \"低（類似度スコア ≥ 0.25）\",\n      medium: \"中（類似度スコア ≥ 0.50）\",\n      high: \"高（類似度スコア ≥ 0.75）\",\n    },\n    reset: {\n      reset: \"ベクターデータベースをリセット\",\n      resetting: \"ベクターをクリア中...\",\n      confirm:\n        \"このワークスペースのベクターデータベースをリセットしようとしています。これにより、現在埋め込まれているすべてのベクターが削除されます。\\n\\n元のソースファイルはそのまま残ります。この操作は元に戻せません。\",\n      error: \"ワークスペースのベクターデータベースをリセットできませんでした！\",\n      success: \"ワークスペースのベクターデータベースがリセットされました！\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"ツール呼び出しに対応していないLLMの性能は、モデルの能力や精度に大きく依存します。一部の機能が制限されたり、正しく動作しない場合があります。\",\n    provider: {\n      title: \"ワークスペースエージェントのLLMプロバイダー\",\n      description:\n        \"このワークスペースの@agentで使用するLLMプロバイダーとモデルを指定します。\",\n    },\n    mode: {\n      chat: {\n        title: \"ワークスペースエージェントのチャットモデル\",\n        description:\n          \"このワークスペースの@agentで使用するチャットモデルを指定します。\",\n      },\n      title: \"ワークスペースエージェントのモデル\",\n      description:\n        \"このワークスペースの@agentで使用するLLMモデルを指定します。\",\n      wait: \"-- モデルを読み込み中 --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAGと長期記憶\",\n        description:\n          \"エージェントがローカルドキュメントを活用して質問に答えたり、内容を「記憶」して長期的に参照できるようにします。\",\n      },\n      view: {\n        title: \"ドキュメントの閲覧と要約\",\n        description:\n          \"エージェントがワークスペース内のファイルを一覧表示し、内容を要約できるようにします。\",\n      },\n      scrape: {\n        title: \"ウェブサイトの取得\",\n        description:\n          \"エージェントがウェブサイトを訪問し、内容を取得できるようにします。\",\n      },\n      generate: {\n        title: \"チャートの生成\",\n        description:\n          \"デフォルトエージェントがチャットやデータからさまざまなチャートを作成できるようにします。\",\n      },\n      save: {\n        title: \"ファイルの生成と保存\",\n        description:\n          \"デフォルトエージェントがファイルを生成し、ブラウザからダウンロードできるようにします。\",\n      },\n      web: {\n        title: \"ウェブ検索と閲覧\",\n        description:\n          \"エージェントがウェブ検索（SERP）プロバイダーに接続することで、あなたの質問に答えるためにウェブを検索できるようにする。\",\n      },\n      sql: {\n        title: \"SQLコネクタ\",\n        description:\n          \"エージェントが、さまざまなSQLデータベースプロバイダーに接続することで、SQLを活用してお客様からの質問に回答できるようにする。\",\n      },\n      default_skill:\n        \"デフォルトでは、この機能は有効になっていますが、エージェントに利用させたくない場合は、無効にすることができます。\",\n    },\n    mcp: {\n      title: \"MCP サーバー\",\n      \"loading-from-config\": \"構成ファイルからMCPサーバーを読み込む\",\n      \"learn-more\": \"MCP サーバーに関する詳細情報を入手してください。\",\n      \"no-servers-found\": \"MCP サーバーは見つかりませんでした\",\n      \"tool-warning\":\n        \"最高のパフォーマンスを得るためには、不要なツールを無効にして、コンテキストを維持することを検討してください。\",\n      \"stop-server\": \"MCP サーバーの停止\",\n      \"start-server\": \"MCP サーバーを開始する\",\n      \"delete-server\": \"MCP サーバーを削除\",\n      \"tool-count-warning\":\n        \"このMCPサーバーには、<b>のツールが有効になっており、これらはチャットのコンテキストを消費します</b>。コンテキストを節約するために、不要なツールを無効にすることを検討してください。\",\n      \"startup-command\": \"起動コマンド\",\n      command: \"指示\",\n      arguments: \"議論\",\n      \"not-running-warning\":\n        \"このMCPサーバーは稼働していません。停止しているか、起動時にエラーが発生している可能性があります。\",\n      \"tool-call-arguments\": \"ツール呼び出しの引数\",\n      \"tools-enabled\": \"ツールが有効化されました\",\n    },\n    settings: {\n      title: \"エージェントのスキル設定\",\n      \"max-tool-calls\": {\n        title: \"1回の応答で実行できる最大ツール数\",\n        description:\n          \"エージェントが単一の応答を生成するために使用できるツールの一意な最大数。これにより、ツール呼び出しの過剰や無限ループを防ぐことができます。\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"知的なスキル選択\",\n        \"beta-badge\": \"ベータ版\",\n        description:\n          \"クエリごとに、無制限のツールを使用し、トークン使用量を最大80%削減できます。AnythingLLMは、各プロンプトに対して最適なスキルを自動的に選択します。\",\n        \"max-tools\": {\n          title: \"マックスツールズ\",\n          description:\n            \"各クエリで選択できるツール数の上限。大規模なコンテキストモデルを使用する場合は、この値をより高い値に設定することをお勧めします。\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"ワークスペースチャット履歴\",\n    description:\n      \"ユーザーが送信したすべてのチャットとメッセージの履歴です。作成日時順に表示されます。\",\n    export: \"エクスポート\",\n    table: {\n      id: \"ID\",\n      by: \"送信者\",\n      workspace: \"ワークスペース\",\n      prompt: \"プロンプト\",\n      response: \"応答\",\n      at: \"送信日時\",\n    },\n  },\n  api: {\n    title: \"APIキー\",\n    description:\n      \"APIキーにより、プログラム経由でこのAnythingLLMインスタンスにアクセスおよび管理できます。\",\n    link: \"APIドキュメントを読む\",\n    generate: \"新しいAPIキーを生成\",\n    table: {\n      key: \"APIキー\",\n      by: \"作成者\",\n      created: \"作成日\",\n    },\n  },\n  llm: {\n    title: \"LLMの設定\",\n    description:\n      \"これは、お好みのLLMチャットおよび埋め込みプロバイダー用の認証情報と設定です。これらのキーが最新かつ正確でない場合、AnythingLLMは正しく動作しません。\",\n    provider: \"LLMプロバイダー\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Azure サービス エンドポイント\",\n        api_key: \"APIキー\",\n        chat_deployment_name: \"チャットデプロイメント名\",\n        chat_model_token_limit:\n          \"チャットモデルのトークン制限について\\n\\nチャットモデルのトークン制限について\",\n        model_type: \"モデルの種類\",\n        default: \"デフォルト\",\n        reasoning: \"理由\",\n        model_type_tooltip:\n          \"もし、あなたのシステムが推論モデル（o1、o1-mini、o3-miniなど）を使用している場合、この設定を「推論」に設定してください。そうでない場合、チャットの要求が失敗する可能性があります。\",\n      },\n    },\n  },\n  transcription: {\n    title: \"文字起こしモデルの設定\",\n    description:\n      \"これは、お好みの文字起こしモデルプロバイダー用の認証情報と設定です。これらのキーが最新かつ正確でない場合、メディアファイルや音声が正しく文字起こしされません。\",\n    provider: \"文字起こしプロバイダー\",\n    \"warn-start\":\n      \"RAMやCPUが限られたマシンでローカルのWhisperモデルを使用すると、メディアファイルの処理中にAnythingLLMが停止する可能性があります。\",\n    \"warn-recommend\":\n      \"少なくとも2GBのRAMが推奨され、ファイルサイズは10Mb未満であることをお勧めします。\",\n    \"warn-end\": \"組み込みモデルは初回使用時に自動的にダウンロードされます。\",\n  },\n  embedding: {\n    title: \"埋め込み設定\",\n    \"desc-start\":\n      \"LLMがネイティブに埋め込みエンジンをサポートしていない場合、テキストの埋め込み用に追加の認証情報を指定する必要がある場合があります。\",\n    \"desc-end\":\n      \"埋め込みとは、テキストをベクトルに変換するプロセスです。これらの認証情報は、ファイルやプロンプトをAnythingLLMが処理できるフォーマットに変換するために必要です。\",\n    provider: {\n      title: \"埋め込みプロバイダー\",\n    },\n  },\n  text: {\n    title: \"テキスト分割とチャンク化の設定\",\n    \"desc-start\":\n      \"新しいドキュメントがベクトルデータベースに挿入される前に、どのように分割およびチャンク化されるかのデフォルトの方法を変更する場合があります。\",\n    \"desc-end\":\n      \"テキスト分割の仕組みとその副作用を理解している場合にのみ、この設定を変更するべきです。\",\n    size: {\n      title: \"テキストチャンクサイズ\",\n      description: \"1つのベクトルに含まれる最大の文字数です。\",\n      recommend: \"埋め込みモデルの最大長は\",\n    },\n    overlap: {\n      title: \"テキストチャンクの重複\",\n      description: \"隣接するテキストチャンク間に発生する最大の重複文字数です。\",\n    },\n  },\n  vector: {\n    title: \"ベクターデータベース設定\",\n    description:\n      \"これは、AnythingLLMインスタンスの動作方法用の認証情報と設定です。これらのキーが最新で正確であることが重要です。\",\n    provider: {\n      title: \"ベクターデータベースプロバイダー\",\n      description: \"LanceDBの場合、特に設定は必要ありません。\",\n    },\n  },\n  embeddable: {\n    title: \"埋め込みチャットウィジェット\",\n    description:\n      \"埋め込みチャットウィジェットは、特定のワークスペースに紐付けられた公開用チャットインターフェースです。これにより、ワークスペースを構築し、そのチャットを外部に公開できます。\",\n    create: \"埋め込みチャットウィジェットを作成\",\n    table: {\n      workspace: \"ワークスペース\",\n      chats: \"送信済みチャット\",\n      active: \"有効なドメイン\",\n      created: \"作成\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"埋め込みチャット履歴\",\n    export: \"エクスポート\",\n    description:\n      \"これは、公開された埋め込みウィジェットから送信された全てのチャットとメッセージの記録です。\",\n    table: {\n      embed: \"埋め込み\",\n      sender: \"送信者\",\n      message: \"メッセージ\",\n      response: \"応答\",\n      at: \"送信日時\",\n    },\n  },\n  event: {\n    title: \"イベントログ\",\n    description:\n      \"監視のために、このインスタンスで発生しているすべてのアクションとイベントを表示します。\",\n    clear: \"イベントログをクリア\",\n    table: {\n      type: \"イベントタイプ\",\n      user: \"ユーザー\",\n      occurred: \"発生日時\",\n    },\n  },\n  privacy: {\n    title: \"プライバシーとデータ処理\",\n    description:\n      \"これは、接続されているサードパーティプロバイダーとAnythingLLMがデータをどのように処理するかの設定です。\",\n    anonymous: \"匿名テレメトリが有効\",\n  },\n  connectors: {\n    \"search-placeholder\": \"データコネクタを検索\",\n    \"no-connectors\": \"データコネクタが見つかりません。\",\n    github: {\n      name: \"GitHubリポジトリ\",\n      description:\n        \"ワンクリックで公開・非公開のGitHubリポジトリ全体をインポートできます。\",\n      URL: \"GitHubリポジトリURL\",\n      URL_explained: \"収集したいGitHubリポジトリのURLです。\",\n      token: \"GitHubアクセストークン\",\n      optional: \"任意\",\n      token_explained: \"レート制限を回避するためのアクセストークンです。\",\n      token_explained_start: \"アクセストークンがない場合、\",\n      token_explained_link1: \"パーソナルアクセストークン\",\n      token_explained_middle:\n        \"がないと、GitHub APIのレート制限により収集できるファイル数が制限される場合があります。 \",\n      token_explained_link2: \"一時的なアクセストークンを作成\",\n      token_explained_end: \"してこの問題を回避できます。\",\n      ignores: \"無視するファイル\",\n      git_ignore:\n        \".gitignore形式で収集時に無視したいファイルをリストしてください。エンターキーで各エントリを保存します。\",\n      task_explained:\n        \"完了後、すべてのファイルがドキュメントピッカーからワークスペースに埋め込めるようになります。\",\n      branch: \"収集したいブランチ\",\n      branch_loading: \"-- 利用可能なブランチを読み込み中 --\",\n      branch_explained: \"収集したいブランチを指定します。\",\n      token_information:\n        \"<b>GitHubアクセストークン</b>を入力しない場合、GitHubの公開APIのレート制限により<b>トップレベル</b>のファイルのみ収集可能です。\",\n      token_personal:\n        \"無料のパーソナルアクセストークンはこちらから取得できます。\",\n    },\n    gitlab: {\n      name: \"GitLabリポジトリ\",\n      description:\n        \"ワンクリックで公開・非公開のGitLabリポジトリ全体をインポートできます。\",\n      URL: \"GitLabリポジトリURL\",\n      URL_explained: \"収集したいGitLabリポジトリのURLです。\",\n      token: \"GitLabアクセストークン\",\n      optional: \"任意\",\n      token_description: \"GitLab APIから取得する追加エンティティを選択します。\",\n      token_explained_start: \"アクセストークンがない場合、\",\n      token_explained_link1: \"パーソナルアクセストークン\",\n      token_explained_middle:\n        \"がないと、GitLab APIのレート制限により収集できるファイル数が制限される場合があります。 \",\n      token_explained_link2: \"一時的なアクセストークンを作成\",\n      token_explained_end: \"してこの問題を回避できます。\",\n      fetch_issues: \"Issueをドキュメントとして取得\",\n      ignores: \"無視するファイル\",\n      git_ignore:\n        \".gitignore形式で収集時に無視したいファイルをリストしてください。エンターキーで各エントリを保存します。\",\n      task_explained:\n        \"完了後、すべてのファイルがドキュメントピッカーからワークスペースに埋め込めるようになります。\",\n      branch: \"収集したいブランチ\",\n      branch_loading: \"-- 利用可能なブランチを読み込み中 --\",\n      branch_explained: \"収集したいブランチを指定します。\",\n      token_information:\n        \"<b>GitLabアクセストークン</b>を入力しない場合、GitLabの公開APIのレート制限により<b>トップレベル</b>のファイルのみ収集可能です。\",\n      token_personal:\n        \"無料のパーソナルアクセストークンはこちらから取得できます。\",\n    },\n    youtube: {\n      name: \"YouTube文字起こし\",\n      description: \"YouTube動画の文字起こしをリンクからインポートできます。\",\n      URL: \"YouTube動画URL\",\n      URL_explained_start:\n        \"文字起こしを取得したいYouTube動画のURLを入力してください。動画には\",\n      URL_explained_link: \"クローズドキャプション\",\n      URL_explained_end: \"が必要です。\",\n      task_explained:\n        \"完了後、文字起こしがドキュメントピッカーからワークスペースに埋め込めるようになります。\",\n    },\n    \"website-depth\": {\n      name: \"ウェブサイト一括スクレイパー\",\n      description: \"ウェブサイトとその下層リンクを指定した深さまで取得します。\",\n      URL: \"ウェブサイトURL\",\n      URL_explained: \"取得したいウェブサイトのURLです。\",\n      depth: \"クロール深度\",\n      depth_explained: \"元のURLからたどる子リンクの数です。\",\n      max_pages: \"最大ページ数\",\n      max_pages_explained: \"取得する最大リンク数です。\",\n      task_explained:\n        \"完了後、すべての取得内容がドキュメントピッカーからワークスペースに埋め込めるようになります。\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"ワンクリックでConfluenceページ全体をインポートできます。\",\n      deployment_type: \"Confluenceデプロイタイプ\",\n      deployment_type_explained:\n        \"ConfluenceインスタンスがAtlassianクラウドかセルフホストかを選択します。\",\n      base_url: \"ConfluenceベースURL\",\n      base_url_explained: \"ConfluenceスペースのベースURLです。\",\n      space_key: \"Confluenceスペースキー\",\n      space_key_explained:\n        \"使用するConfluenceインスタンスのスペースキーです。通常は~で始まります。\",\n      username: \"Confluenceユーザー名\",\n      username_explained: \"Confluenceのユーザー名です。\",\n      auth_type: \"Confluence認証タイプ\",\n      auth_type_explained:\n        \"Confluenceページへアクセスするための認証タイプを選択してください。\",\n      auth_type_username: \"ユーザー名とアクセストークン\",\n      auth_type_personal: \"パーソナルアクセストークン\",\n      token: \"Confluenceアクセストークン\",\n      token_explained_start:\n        \"認証用のアクセストークンを入力してください。アクセストークンは\",\n      token_explained_link: \"こちら\",\n      token_desc: \"認証用アクセストークン\",\n      pat_token: \"Confluenceパーソナルアクセストークン\",\n      pat_token_explained: \"Confluenceのパーソナルアクセストークンです。\",\n      task_explained:\n        \"完了後、ページ内容がドキュメントピッカーからワークスペースに埋め込めるようになります。\",\n      bypass_ssl: \"SSL証明書の検証をスキップする\",\n      bypass_ssl_explained:\n        \"これにより、独自の証明書で署名された、自社ホストのConfluenceインスタンスに対して、SSL証明書の検証を回避できます。\",\n    },\n    manage: {\n      documents: \"ドキュメント\",\n      \"data-connectors\": \"データコネクタ\",\n      \"desktop-only\":\n        \"これらの設定の編集はデスクトップ端末のみ対応しています。デスクトップでこのページにアクセスしてください。\",\n      dismiss: \"閉じる\",\n      editing: \"編集中\",\n    },\n    directory: {\n      \"my-documents\": \"マイドキュメント\",\n      \"new-folder\": \"新しいフォルダー\",\n      \"search-document\": \"ドキュメントを検索\",\n      \"no-documents\": \"ドキュメントがありません\",\n      \"move-workspace\": \"ワークスペースへ移動\",\n      \"delete-confirmation\":\n        \"これらのファイルやフォルダーを削除してもよろしいですか？\\nシステムから削除され、既存のワークスペースからも自動的に削除されます。\\nこの操作は元に戻せません。\",\n      \"removing-message\":\n        \"{{count}}件のドキュメントと{{folderCount}}件のフォルダーを削除中です。しばらくお待ちください。\",\n      \"move-success\": \"{{count}}件のドキュメントを移動しました。\",\n      no_docs: \"ドキュメントがありません\",\n      select_all: \"すべて選択\",\n      deselect_all: \"すべて選択解除\",\n      remove_selected: \"選択したものを削除\",\n      costs: \"※埋め込みには一度だけ費用がかかります\",\n      save_embed: \"保存して埋め込む\",\n      \"total-documents_one\": \"{{count}} のドキュメント\",\n      \"total-documents_other\": \"{{count}} に関する書類\",\n    },\n    upload: {\n      \"processor-offline\": \"ドキュメント処理機能が利用できません\",\n      \"processor-offline-desc\":\n        \"ドキュメント処理機能がオフラインのため、ファイルをアップロードできません。後でもう一度お試しください。\",\n      \"click-upload\":\n        \"クリックしてアップロード、またはドラッグ＆ドロップしてください\",\n      \"file-types\":\n        \"テキストファイル、CSV、スプレッドシート、音声ファイルなどに対応しています！\",\n      \"or-submit-link\": \"またはリンクを入力\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"取得中...\",\n      \"fetch-website\": \"ウェブサイトを取得\",\n      \"privacy-notice\":\n        \"これらのファイルは、このAnythingLLMインスタンス上のドキュメント処理機能にアップロードされます。第三者に送信・共有されることはありません。\",\n    },\n    pinning: {\n      what_pinning: \"ドキュメントのピン留めとは？\",\n      pin_explained_block1:\n        \"AnythingLLMでドキュメントを<b>ピン留め</b>すると、その内容全体がプロンプトウィンドウに挿入され、LLMがしっかり理解できるようになります。\",\n      pin_explained_block2:\n        \"<b>大きなコンテキストを持つモデル</b>や、重要な小さなファイルで特に効果的です。\",\n      pin_explained_block3:\n        \"デフォルトのままでは満足できる回答が得られない場合、ピン留めを活用するとより高品質な回答が得られます。\",\n      accept: \"わかりました\",\n    },\n    watching: {\n      what_watching: \"ドキュメントのウォッチとは？\",\n      watch_explained_block1:\n        \"AnythingLLMでドキュメントを<b>ウォッチ</b>すると、元のソースから定期的に内容が<i>自動的に</i>同期されます。管理しているすべてのワークスペースで内容が自動更新されます。\",\n      watch_explained_block2:\n        \"この機能は現在オンラインベースのコンテンツのみ対応しており、手動アップロードしたドキュメントには利用できません。\",\n      watch_explained_block3_start: \"ウォッチしているドキュメントの管理は\",\n      watch_explained_block3_link: \"ファイルマネージャー\",\n      watch_explained_block3_end: \"管理画面から行えます。\",\n      accept: \"わかりました\",\n    },\n    obsidian: {\n      vault_location: \"保管場所\",\n      vault_description:\n        \"Obsidianの vault フォルダを選択して、すべてのメモとそれらの関連をインポートします。\",\n      selected_files: \"マークダウン形式のファイルが見つかりました：{{count}}個\",\n      importing: \"保管庫のインポート...\",\n      import_vault: \"Import Vault\",\n      processing_time:\n        \"これは、保管場所のサイズによって時間がかかる可能性があります。\",\n      vault_warning:\n        \"いかなる紛争を避けるため、Obsidianの保管場所が現在開いている状態でないことを確認してください。\",\n    },\n  },\n  chat_window: {\n    send_message: \"メッセージを送信\",\n    attach_file: \"このチャットにファイルを添付\",\n    text_size: \"テキストサイズを変更\",\n    microphone: \"プロンプトを音声入力\",\n    send: \"ワークスペースにプロンプトメッセージを送信\",\n    attachments_processing:\n      \"添付ファイルの処理中です。しばらくお待ちください。\",\n    tts_speak_message: \"TTS Speak メッセージ\",\n    copy: \"以下に翻訳を示します。\",\n    regenerate: \"再生\",\n    regenerate_response: \"申し訳ありませんが、その質問にはお答えできません。\",\n    good_response: \"良い反応\",\n    more_actions:\n      \"さらに詳細な情報が必要な場合は、お気軽にお問い合わせください。\",\n    fork: \"フォーク\",\n    delete: \"削除\",\n    cancel: \"キャンセル\",\n    edit_prompt: \"編集のヒント\",\n    edit_response: \"編集内容を保存します。\",\n    preset_reset_description:\n      \"チャット履歴をクリアし、新しいチャットを開始してください。\",\n    add_new_preset: \"新しいプリセットを追加する\",\n    command: \"命令\",\n    your_command: \"あなたの指示\",\n    placeholder_prompt: \"これは、プロンプトの先頭に挿入されるコンテンツです。\",\n    description: \"説明\",\n    placeholder_description: \"大規模言語モデルに関する詩を提示します。\",\n    save: \"保存\",\n    small: \"小さい\",\n    normal: \"通常\",\n    large: \"大規模\",\n    workspace_llm_manager: {\n      search: \"LLMプロバイダーを検索する\",\n      loading_workspace_settings: \"作業スペースの設定を読み込んでいます...\",\n      available_models: \"{{provider}} の利用可能なモデル\",\n      available_models_description:\n        \"このワークスペースで使用するモデルを選択してください。\",\n      save: \"このモデルを使用してください。\",\n      saving: \"デフォルトワークスペースとしてモデルを設定...\",\n      missing_credentials: \"このプロバイダーには資格がありません。\",\n      missing_credentials_description:\n        \"認証情報を設定するには、ここをクリックしてください。\",\n    },\n    submit: \"送信\",\n    edit_info_user:\n      \"「送信」はAIの応答を再生成します。「保存」は、あなたのメッセージのみを更新します。\",\n    edit_info_assistant: \"あなたの変更は、この回答に直接保存されます。\",\n    see_less: \"詳細を見る\",\n    see_more: \"詳細を見る\",\n    tools: \"道具\",\n    browse: \"閲覧\",\n    text_size_label: \"文字サイズ\",\n    select_model: \"モデルを選択\",\n    sources: \"出典\",\n    document: \"文書\",\n    similarity_match: \"試合\",\n    source_count_one: \"{{count}} 参照\",\n    source_count_other: \"{{count}} への参照\",\n    preset_exit_description: \"現在のエージェントセッションを停止する\",\n    add_new: \"新しいものを追加する\",\n    edit: \"編集\",\n    publish: \"出版\",\n    stop_generating: \"応答の生成を停止する\",\n    pause_tts_speech_message: \"メッセージのテキスト読み上げ機能を一時停止する\",\n    slash_commands: \"スラッシュコマンド\",\n    agent_skills: \"エージェントのスキル\",\n    manage_agent_skills: \"エージェントのスキル管理\",\n    agent_skills_disabled_in_session:\n      \"アクティブなセッション中にスキルを変更することはできません。まず、`/exit`コマンドを使用してセッションを終了してください。\",\n    start_agent_session: \"エージェントセッションを開始\",\n    use_agent_session_to_use_tools:\n      \"チャットでツールを使用するには、プロンプトの冒頭に'@agent'を使用してエージェントセッションを開始してください。\",\n  },\n  profile_settings: {\n    edit_account: \"アカウントを編集\",\n    profile_picture: \"プロフィール画像\",\n    remove_profile_picture: \"プロフィール画像を削除\",\n    username: \"ユーザー名\",\n    new_password: \"新しいパスワード\",\n    password_description: \"パスワードは8文字以上である必要があります\",\n    cancel: \"キャンセル\",\n    update_account: \"アカウントを更新\",\n    theme: \"テーマ設定\",\n    language: \"優先言語\",\n    failed_upload: \"プロフィール写真のアップロードに失敗しました：{{error}}\",\n    upload_success: \"プロフィール写真がアップロードされました。\",\n    failed_remove: \"プロフィール写真の削除に失敗しました：{{error}}\",\n    profile_updated: \"プロフィールを更新しました。\",\n    failed_update_user: \"ユーザーの更新に失敗：{{error}}\",\n    account: \"アカウント\",\n    support: \"サポート\",\n    signout: \"ログアウト\",\n  },\n  customization: {\n    interface: {\n      title: \"UI設定\",\n      description: \"AnythingLLM の UI 設定を調整してください。\",\n    },\n    branding: {\n      title: \"ブランディングとホワイトレーベル化\",\n      description:\n        \"AnythingLLMインスタンスを、独自のブランドでカスタマイズしてください。\",\n    },\n    chat: {\n      title: \"チャット\",\n      description: \"AnythingLLM のチャット設定をカスタマイズしてください。\",\n      auto_submit: {\n        title: \"自動音声入力送信\",\n        description: \"沈黙の後に自動で音声入力を行う\",\n      },\n      auto_speak: {\n        title: \"自動応答機能\",\n        description: \"AIによる自動応答\",\n      },\n      spellcheck: {\n        title: \"スペルチェック機能を有効にする\",\n        description:\n          \"チャット入力フィールドでのスペルチェックを有効または無効にする\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"テーマ\",\n        description: \"アプリケーションの希望の色テーマを選択してください。\",\n      },\n      \"show-scrollbar\": {\n        title: \"スクロールバーを表示する\",\n        description:\n          \"チャットウィンドウのスクロールバーを有効または無効にする。\",\n      },\n      \"support-email\": {\n        title: \"サポートメール\",\n        description:\n          \"ユーザーが支援を必要とする際に利用できる、サポート用メールアドレスを設定します。\",\n      },\n      \"app-name\": {\n        title: \"名前\",\n        description:\n          \"ログインページに表示される名前を、すべてのユーザーに設定する。\",\n      },\n      \"display-language\": {\n        title: \"表示言語\",\n        description:\n          \"AnythingLLMのUIを特定の言語で表示するためのオプションを選択してください。翻訳が利用可能な場合にのみ有効です。\",\n      },\n      logo: {\n        title: \"ブランドロゴ\",\n        description:\n          \"すべてのページで表示するためのカスタムロゴをアップロードしてください。\",\n        add: \"カスタムロゴを追加する\",\n        recommended: \"推奨サイズ：800 x 200\",\n        remove: \"削除\",\n        replace: \"置き換える\",\n      },\n      \"browser-appearance\": {\n        title: \"ブラウザの見た目\",\n        description:\n          \"アプリを開いたときに、ブラウザのタブとタイトルをカスタマイズする。\",\n        tab: {\n          title: \"タイトル\",\n          description:\n            \"ブラウザでアプリを開いたときに、カスタムのタブタイトルを設定します。\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description: \"ブラウザのタブにカスタムのfaviconを使用する。\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"サイドバーのフッター項目\",\n        description:\n          \"サイドバーの下部に表示されるフッターの項目をカスタマイズする。\",\n        icon: \"アイコン\",\n        link: \"リンク\",\n      },\n      \"render-html\": {\n        title: \"チャットでHTMLをレンダリングする\",\n        description:\n          \"アシスタントの回答にHTML形式のレスポンスを生成する。\\nこれにより、回答の品質を大幅に向上させることができるが、同時にセキュリティ上のリスクも生じる可能性がある。\",\n      },\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"エージェントを作成する\",\n      editWorkspace: \"ワークスペースの編集\",\n      uploadDocument: \"ドキュメントをアップロードする\",\n    },\n    greeting: \"今日はどのようにお手伝いできますか？\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"キーボードショートカット\",\n    shortcuts: {\n      settings: \"設定を開く\",\n      workspaceSettings: \"現在のワークスペースの設定を開く\",\n      home: \"ホームページへ\",\n      workspaces: \"ワークスペースの管理\",\n      apiKeys: \"APIキーの設定\",\n      llmPreferences: \"LLM の好み\",\n      chatSettings: \"チャット設定\",\n      help: \"キーボードショートカットのヘルプを表示する\",\n      showLLMSelector: \"LLM（大規模言語モデル）選択ツール\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"成功！\",\n        success_description:\n          \"システムプロンプトがコミュニティハブに公開されました。\",\n        success_thank_you: \"コミュニティへの共有ありがとうございます。\",\n        view_on_hub: \"コミュニティハブでの表示\",\n        modal_title: \"出版システムに関するプロンプト\",\n        name_label: \"名前\",\n        name_description: \"これは、システムのプロンプトの名前です。\",\n        name_placeholder: \"私のシステムプロンプト\",\n        description_label: \"説明\",\n        description_description:\n          \"これは、システムプロンプトの説明です。システムプロンプトの目的を説明するために使用してください。\",\n        tags_label: \"タグ\",\n        tags_description:\n          \"タグは、システムプロンプトを簡単に検索できるようにラベル付けするために使用されます。複数のタグを追加できます。最大5つのタグ。各タグは最大20文字です。\",\n        tags_placeholder:\n          \"タグを追加するには、タイプしてEnterキーを押してください。\",\n        visibility_label: \"視界\",\n        public_description:\n          \"一般のシステムからのメッセージは、すべての人に表示されます。\",\n        private_description:\n          \"プライベートなシステムからのメッセージは、あなただけが見ることができます。\",\n        publish_button: \"コミュニティハブに公開する\",\n        submitting: \"出版...\",\n        prompt_label: \"プロンプト\",\n        prompt_description:\n          \"これは、大規模言語モデル（LLM）を誘導するために使用される実際のシステムプロンプトです。\",\n        prompt_placeholder: \"ここにシステムプロンプトを入力してください...\",\n      },\n      agent_flow: {\n        success_title: \"成功！\",\n        success_description:\n          \"あなたのエージェントフローがコミュニティハブに公開されました。\",\n        success_thank_you: \"コミュニティへの共有ありがとうございます。\",\n        view_on_hub: \"コミュニティハブで確認\",\n        modal_title: \"出版代理店フロー\",\n        name_label:\n          \"山田太郎\\n\\n\\n氏名\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n\\n名前\\n山田 太郎\\n<|im\",\n        name_description: \"これは、あなたのエージェントフローの名前です。\",\n        name_placeholder: \"私のエージェントフロー\",\n        description_label: \"説明\",\n        description_description:\n          \"これは、あなたのエージェントフローの説明です。この説明文を使って、あなたのエージェントフローの目的を記述してください。\",\n        tags_label: \"タグ\",\n        tags_description:\n          \"タグは、ワークフローをより簡単に検索するために使用されます。複数のタグを追加できます。最大5つのタグ。各タグは最大20文字です。\",\n        tags_placeholder:\n          \"タグを追加するには、タイプしてEnterキーを押してください。\",\n        visibility_label: \"視界\",\n        submitting: \"出版...\",\n        submit: \"コミュニティハブに公開する\",\n        privacy_note:\n          \"機密性の高いデータ保護のため、ワークフローは常にプライベートでアップロードされます。公開後、コミュニティハブで可視性を変更できます。公開前に、ワークフローに機密情報や個人情報が含まれていないことを確認してください。\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"本人確認が必要です。\",\n          description:\n            \"アイテムを公開する前に、AnythingLLMコミュニティハブで認証する必要があります。\",\n          button: \"コミュニティハブへの接続\",\n        },\n      },\n      slash_command: {\n        success_title: \"成功！\",\n        success_description:\n          \"スラッシュコマンドがコミュニティハブに公開されました。\",\n        success_thank_you: \"コミュニティへの共有ありがとうございます。\",\n        view_on_hub: \"コミュニティハブでの表示\",\n        modal_title: \"スラッシュコマンドを公開する\",\n        name_label: \"名前\",\n        name_description: \"これは、スラッシュコマンドの名前です。\",\n        name_placeholder: \"私のスラッシュコマンド\",\n        description_label: \"説明\",\n        description_description:\n          \"これは、スラッシュコマンドの説明です。スラッシュコマンドの目的を記述するために使用してください。\",\n        tags_label: \"タグ\",\n        tags_description:\n          \"スラッシュコマンドをより簡単に検索できるように、タグを使用してコマンドを分類します。複数のタグを追加できます。最大5つのタグ。各タグは最大20文字です。\",\n        tags_placeholder:\n          \"タグを追加するには、タイプしてEnterキーを押してください。\",\n        visibility_label: \"視界\",\n        public_description:\n          \"一般のユーザーが利用できるコマンドは、すべての人に公開されています。\",\n        private_description:\n          \"私だけが利用できるプライベートなスラッシュコマンドのみが表示されます。\",\n        publish_button: \"コミュニティハブに公開する\",\n        submitting: \"出版...\",\n        prompt_label:\n          \"どのような状況で、どのような目的で、どのような方法で、どのような結果を期待していますか？\",\n        prompt_description:\n          \"これは、スラッシュコマンドが実行されたときに使用されるプロンプトです。\",\n        prompt_placeholder: \"ここに指示を入力してください...\",\n      },\n    },\n  },\n  security: {\n    title: \"セキュリティ\",\n    multiuser: {\n      title: \"マルチユーザーモード\",\n      description:\n        \"マルチユーザーモードを有効にして、チームをサポートするようにインスタンスを設定します。\",\n      enable: {\n        \"is-enable\": \"マルチユーザーモードが有効です\",\n        enable: \"マルチユーザーモードを有効にする\",\n        description:\n          \"デフォルトでは、あなたが唯一の管理者になります。管理者として、すべての新しいユーザーまたは管理者のアカウントを作成する必要があります。管理者ユーザーのみがパスワードをリセットできるため、パスワードを紛失しないでください。\",\n        username: \"管理者アカウントのユーザー名\",\n        password: \"管理者アカウントのパスワード\",\n      },\n    },\n    password: {\n      title: \"パスワード保護\",\n      description:\n        \"AnythingLLMインスタンスをパスワードで保護します。これを忘れた場合、回復方法はないため、このパスワードを必ず保存してください。\",\n      \"password-label\": \"インスタンスパスワード\",\n    },\n  },\n  home: {\n    welcome: \"ようこそ\",\n    chooseWorkspace: \"ワークスペースを選択してチャットを開始してください！\",\n    notAssigned:\n      \"現在、あなたはどのワークスペースにも割り当てられていません。\\nワークスペースへのアクセスを要求するには、管理者にお問い合わせください。\",\n    goToWorkspace: 'ワークスペースに移動 \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/ko/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"방문을 환영합니다\",\n      getStarted: \"시작하기\",\n    },\n    llm: {\n      title: \"LLM 기본 설정\",\n      description:\n        \"AnythingLLM은 다양한 LLM 제공자와 연동할 수 있습니다. 여기서 선택한 서비스가 채팅을 담당하게 됩니다.\",\n    },\n    userSetup: {\n      title: \"사용자 설정\",\n      description: \"사용자 설정을 구성하세요.\",\n      howManyUsers: \"이 인스턴스를 사용할 사용자는 몇 명인가요?\",\n      justMe: \"나만 사용\",\n      myTeam: \"팀 사용\",\n      instancePassword: \"인스턴스 비밀번호\",\n      setPassword: \"비밀번호를 설정하시겠습니까?\",\n      passwordReq: \"비밀번호는 최소 8자 이상이어야 합니다.\",\n      passwordWarn: \"이 비밀번호는 복구 방법이 없으니 꼭 안전하게 보관하세요.\",\n      adminUsername: \"관리자 계정 사용자명\",\n      adminPassword: \"관리자 계정 비밀번호\",\n      adminPasswordReq: \"비밀번호는 최소 8자 이상이어야 합니다.\",\n      teamHint:\n        \"기본적으로 본인이 유일한 관리자가 됩니다. 온보딩이 완료되면 다른 사용자를 초대하거나 관리자로 지정할 수 있습니다. 비밀번호를 분실하면 관리자만 비밀번호를 재설정할 수 있으니 꼭 기억해 두세요.\",\n    },\n    data: {\n      title: \"데이터 처리 및 개인정보 보호\",\n      description:\n        \"AnythingLLM은 여러분의 개인정보에 대한 투명성과 제어권을 최우선으로 생각합니다.\",\n      settingsHint: \"이 설정은 언제든지 설정 메뉴에서 다시 변경할 수 있습니다.\",\n    },\n    survey: {\n      title: \"AnythingLLM에 오신 것을 환영합니다\",\n      description:\n        \"여러분의 필요에 맞는 AnythingLLM을 만들 수 있도록 도와주세요. (선택 사항)\",\n      email: \"이메일을 입력해 주세요\",\n      useCase: \"AnythingLLM을 어떤 용도로 사용하실 예정인가요?\",\n      useCaseWork: \"업무용\",\n      useCasePersonal: \"개인용\",\n      useCaseOther: \"기타\",\n      comment: \"AnythingLLM을 어떻게 알게 되셨나요?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube 등 - 어떻게 알게 되셨는지 알려주세요!\",\n      skip: \"설문 건너뛰기\",\n      thankYou: \"소중한 의견 감사합니다!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"워크스페이스 이름\",\n    user: \"사용자\",\n    selection: \"모델 선택\",\n    saving: \"저장 중...\",\n    save: \"저장\",\n    previous: \"이전\",\n    next: \"다음\",\n    optional: \"선택 사항\",\n    yes: \"예\",\n    no: \"아니오\",\n    search: \"검색\",\n    username_requirements:\n      \"사용자 이름은 2-32자여야 하고, 소문자로 시작해야 하며, 소문자, 숫자, 밑줄, 하이픈, 마침표만 포함할 수 있습니다.\",\n    on: \"~에 대해\",\n    none: \"없음\",\n    stopped: \"멈춤\",\n    loading: \"로딩 중\",\n    refresh: \"새롭게\",\n  },\n  settings: {\n    title: \"인스턴스 설정\",\n    invites: \"초대\",\n    users: \"사용자\",\n    workspaces: \"워크스페이스\",\n    \"workspace-chats\": \"워크스페이스 채팅\",\n    customization: \"사용자 정의\",\n    \"api-keys\": \"개발자 API\",\n    llm: \"LLM\",\n    transcription: \"텍스트 변환\",\n    embedder: \"임베더\",\n    \"text-splitting\": \"텍스트 분할과 청킹\",\n    \"voice-speech\": \"음성과 말하기\",\n    \"vector-database\": \"벡터 데이터베이스\",\n    embeds: \"채팅 임베드\",\n    security: \"보안\",\n    \"event-logs\": \"이벤트 로그\",\n    privacy: \"사생활 보호와 데이터\",\n    \"ai-providers\": \"AI 제공자\",\n    \"agent-skills\": \"에이전트 스킬\",\n    admin: \"관리자\",\n    tools: \"도구\",\n    \"experimental-features\": \"실험적 기능\",\n    contact: \"지원팀 연락\",\n    \"browser-extension\": \"브라우저 확장 프로그램\",\n    \"system-prompt-variables\": \"System Prompt Variables\",\n    interface: \"UI 환경 설정\",\n    branding: \"브랜딩 및 화이트라벨링\",\n    chat: \"채팅\",\n    \"mobile-app\": \"AnythingLLM 모바일\",\n    \"community-hub\": {\n      title: \"지역 커뮤니티 허브\",\n      trending: \"인기 트렌드 탐색\",\n      \"your-account\": \"당신의 계정\",\n      \"import-item\": \"수입 품목\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"웰컴!\",\n      \"placeholder-username\": \"사용자 이름\",\n      \"placeholder-password\": \"비밀번호\",\n      login: \"로그인\",\n      validating: \"유효성 검사 중...\",\n      \"forgot-pass\": \"비밀번호를 잊으셨나요\",\n      reset: \"재설정\",\n    },\n    \"sign-in\": \"{{appName}}에 로그인하세요.\",\n    \"password-reset\": {\n      title: \"비밀번호 재설정\",\n      description: \"비밀번호를 재설정하려면 아래에 필요한 정보를 입력하세요.\",\n      \"recovery-codes\": \"복구 코드\",\n      \"back-to-login\": \"로그인으로 돌아가기\",\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"에이전트 생성\",\n      editWorkspace: \"워크스페이스 편집\",\n      uploadDocument: \"문서 업로드\",\n    },\n    greeting: \"오늘 어떻게 도와드릴까요?\",\n  },\n  \"new-workspace\": {\n    title: \"새 워크스페이스\",\n    placeholder: \"내 워크스페이스\",\n  },\n  \"workspaces—settings\": {\n    general: \"일반 설정\",\n    chat: \"채팅 설정\",\n    vector: \"벡터 데이터베이스\",\n    members: \"구성원\",\n    agent: \"에이전트 구성\",\n  },\n  general: {\n    vector: {\n      title: \"벡터 수\",\n      description: \"벡터 데이터베이스에 있는 총 벡터 수입니다.\",\n    },\n    names: {\n      description: \"이것은 워크스페이스의 표시 이름만 변경합니다.\",\n    },\n    message: {\n      title: \"제안된 채팅 메시지\",\n      description: \"워크스페이스 사용자가 사용할 메시지를 수정합니다.\",\n      add: \"새 메시지 추가\",\n      save: \"메시지 저장\",\n      heading: \"저에게 설명해주세요\",\n      body: \"AnythingLLM의 장점\",\n    },\n    delete: {\n      title: \"워크스페이스 삭제\",\n      description:\n        \"이 워크스페이스와 모든 데이터를 삭제합니다. 이 작업은 모든 사용자에 대해 워크스페이스를 삭제합니다.\",\n      delete: \"워크스페이스 삭제\",\n      deleting: \"워크스페이스 삭제 중...\",\n      \"confirm-start\": \"이 작업은\",\n      \"confirm-end\":\n        \"워크스페이스 전체를 삭제합니다. 이 작업은 벡터 데이터베이스에 있는 모든 벡터 임베딩을 제거합니다.\\n\\n원본 소스 파일은 그대로 유지됩니다. 이 작업은 되돌릴 수 없습니다.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"워크스페이스 LLM 제공자\",\n      description:\n        \"이 워크스페이스에서 사용할 특정 LLM 제공자와 모델입니다. 기본적으로 시스템 LLM 제공자와 설정을 사용합니다.\",\n      search: \"모든 LLM 제공자 검색\",\n    },\n    model: {\n      title: \"워크스페이스 채팅 모델\",\n      description:\n        \"이 워크스페이스에서 사용할 특정 채팅 모델입니다. 비어 있으면 시스템 LLM 기본 설정을 사용합니다.\",\n    },\n    mode: {\n      title: \"채팅 모드\",\n      chat: {\n        title: \"채팅\",\n        description:\n          \"LLM의 일반적인 지식과 관련 문맥 정보를 활용하여 답변을 제공합니다. 도구를 사용하려면 @agent 명령어를 사용해야 합니다.\",\n      },\n      query: {\n        title: \"쿼리\",\n        description:\n          \"문서 맥락이 발견되면 <b>에만</b> 답변을 제공합니다.<br /> 도구를 사용하려면 @agent 명령을 사용해야 합니다.\",\n      },\n      automatic: {\n        title: \"자동\",\n        description:\n          \"모델과 제공업체가 네이티브 도구 호출을 지원하는 경우, 자동으로 도구를 사용합니다. <br /> 네이티브 도구 호출이 지원되지 않는 경우, 도구를 사용하려면 @agent 명령을 사용해야 합니다.\",\n      },\n    },\n    history: {\n      title: \"채팅 기록\",\n      \"desc-start\": \"응답의 단기 메모리에 포함될 이전 채팅의 수입니다.\",\n      recommend: \"추천 20개 \",\n      \"desc-end\":\n        \" 45개 이상은 메시지 크기에 따라 채팅 실패가 발생할 수 있습니다.\",\n    },\n    prompt: {\n      title: \"시스템 프롬프트\",\n      description:\n        \"이 워크스페이스에서 사용할 프롬프트입니다. AI가 응답을 생성하기 위해 문맥과 지침을 정의합니다. AI가 질문에 대하여 정확한 응답을 생성할 수 있도록 신중하게 프롬프트를 제공해야 합니다.\",\n      history: {\n        title: \"시스템 프롬프트 기록\",\n        clearAll: \"전체 삭제\",\n        noHistory: \"저장된 시스템 프롬프트 기록이 없습니다\",\n        restore: \"복원\",\n        delete: \"삭제\",\n        publish: \"커뮤니티 허브에 게시\",\n        deleteConfirm: \"이 기록 항목을 삭제하시겠습니까?\",\n        clearAllConfirm:\n          \"모든 기록을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.\",\n        expand: \"확장\",\n      },\n    },\n    refusal: {\n      title: \"쿼리 모드 거부 응답 메시지\",\n      \"desc-start\": \"쿼리 모드에서\",\n      query: \"응답에 사용할 수 있는\",\n      \"desc-end\": \"컨텍스트를 찾을 수 없을 때 거부 응답 내용을 작성합니다.\",\n      \"tooltip-title\": \"왜 이 메시지가 표시되나요?\",\n      \"tooltip-description\":\n        \"현재 쿼리 모드에서는 문서의 정보만을 사용합니다. 더 자유로운 대화를 원하시면 채팅 모드로 전환하거나, 채팅 모드에 대한 자세한 내용은 문서를 참고하세요.\",\n    },\n    temperature: {\n      title: \"LLM 온도\",\n      \"desc-start\": '이 설정은 LLM 응답이 얼마나 \"창의적\"일지를 제어합니다.',\n      \"desc-end\":\n        \"숫자가 높을수록 창의적입니다. 일부 모델에서는 너무 높게 설정하면 일관성 없는 응답이 나올 수 있습니다.\",\n      hint: \"대부분의 LLM은 유효한 값의 다양한 허용 범위를 가지고 있습니다. 해당 정보는 LLM 제공자에게 문의하세요.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"벡터 데이터베이스 식별자\",\n    snippets: {\n      title: \"최대 문맥 조각\",\n      description:\n        \"이 설정은 채팅 또는 쿼리당 LLM에 전송될 최대 문맥 조각 수를 제어합니다.\",\n      recommend: \"추천: 4\",\n    },\n    doc: {\n      title: \"문서 유사성 임계값\",\n      description:\n        \"채팅과 관련이 있다고 판단되는 문서의 유사성 점수입니다. 숫자가 높을수록 질문에 대한 문서의 내용이 유사합니다.\",\n      zero: \"제한 없음\",\n      low: \"낮음 (유사성 점수 ≥ .25)\",\n      medium: \"중간 (유사성 점수 ≥ .50)\",\n      high: \"높음 (유사성 점수 ≥ .75)\",\n    },\n    reset: {\n      reset: \"벡터 데이터베이스 재설정\",\n      resetting: \"벡터 지우는 중...\",\n      confirm:\n        \"이 워크스페이스의 벡터 데이터베이스를 재설정하려고 합니다. 현재 임베딩된 모든 벡터 임베딩을 제거합니다.\\n\\n원본 소스 파일은 그대로 유지됩니다. 이 작업은 되돌릴 수 없습니다.\",\n      error: \"워크스페이스 벡터 데이터베이스를 재설정할 수 없습니다!\",\n      success: \"워크스페이스 벡터 데이터베이스가 재설정되었습니다!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"도구 호출을 명시적으로 지원하지 않는 LLM의 성능은 모델의 기능과 정확도에 크게 좌우됩니다. 일부 기능은 제한되거나 작동하지 않을 수 있습니다.\",\n    provider: {\n      title: \"워크스페이스 에이전트 LLM 제공자\",\n      description:\n        \"이 워크스페이스의 @agent 에이전트에 사용할 특정 LLM 제공자 및 모델입니다.\",\n    },\n    mode: {\n      chat: {\n        title: \"워크스페이스 에이전트 채팅 모델\",\n        description:\n          \"이 워크스페이스의 @agent 에이전트에 사용할 특정 채팅 모델입니다.\",\n      },\n      title: \"워크스페이스 에이전트 모델\",\n      description:\n        \"이 워크스페이스의 @agent 에이전트에 사용할 특정 LLM 모델입니다.\",\n      wait: \"-- 모델 기다리는 중 --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG와 장기 메모리\",\n        description:\n          '에이전트가 제공된 문서를 활용하여 쿼리에 답변하거나 에이전트에게 \"기억\"할 내용을 요청하여 장기 메모리 검색을 허용합니다.',\n      },\n      view: {\n        title: \"문서 보기 및 요약\",\n        description:\n          \"에이전트가 현재 임베딩된 워크스페이스의 문서 내용을 나열하고 요약할 수 있도록 합니다.\",\n      },\n      scrape: {\n        title: \"웹사이트 스크래핑\",\n        description:\n          \"에이전트가 웹사이트를 방문하고 내용을 스크래핑할 수 있도록 합니다.\",\n      },\n      generate: {\n        title: \"차트 생성\",\n        description:\n          \"기본 에이전트가 채팅에서 제공된 데이터를 이용하여 다양한 유형의 차트를 생성할 수 있도록 합니다.\",\n      },\n      save: {\n        title: \"브라우저에서 파일 생성과 저장\",\n        description:\n          \"기본 에이전트가 브라우저에서 파일을 생성하고 다운로드할 수 있도록 합니다.\",\n      },\n      web: {\n        title: \"실시간 웹 검색 및 탐색\",\n        description:\n          \"웹 검색 (SERP) 제공업체와 연결하여 에이전트가 웹을 검색하고 질문에 답변하도록 설정합니다.\",\n      },\n      sql: {\n        title: \"SQL 연결기\",\n        description:\n          \"여러 SQL 데이터베이스 제공업체에 연결하여 에이전트가 SQL을 활용하여 질문에 답변할 수 있도록 지원합니다.\",\n      },\n      default_skill:\n        \"기본적으로 이 기능은 활성화되어 있지만, 에이전트에게 이 기능을 사용하지 않도록 설정할 수도 있습니다.\",\n    },\n    mcp: {\n      title: \"MCP 서버\",\n      \"loading-from-config\": \"구성 파일에서 MCP 서버 로드\",\n      \"learn-more\": \"MCP 서버에 대해 더 자세히 알아보세요.\",\n      \"no-servers-found\": \"MCP 서버를 찾을 수 없습니다.\",\n      \"tool-warning\":\n        \"최상의 성능을 위해, 불필요한 도구를 비활성화하여 컨텍스트를 보존하는 것을 고려해 보세요.\",\n      \"stop-server\": \"MCP 서버 중단\",\n      \"start-server\": \"MCP 서버 시작\",\n      \"delete-server\": \"MCP 서버 삭제\",\n      \"tool-count-warning\":\n        \"이 MCP 서버에는 <b>에 설정된 {{count}} 도구가 있으며, 이는 모든 채팅에서 컨텍스트를 소비합니다. </b> 불필요한 도구를 비활성화하여 컨텍스트를 절약하는 것을 고려해 보세요.\",\n      \"startup-command\": \"시작 명령어\",\n      command: \"명령\",\n      arguments: \"논쟁\",\n      \"not-running-warning\":\n        \"이 MCP 서버는 현재 실행 상태가 아닙니다. 중단되었거나, 시작 시 오류가 발생했을 수 있습니다.\",\n      \"tool-call-arguments\": \"툴 호출 인자\",\n      \"tools-enabled\": \"도구 사용 기능 활성화\",\n    },\n    settings: {\n      title: \"에이전트 스킬 설정\",\n      \"max-tool-calls\": {\n        title: \"응답당 최대 툴 호출 횟수\",\n        description:\n          \"에이전트가 단일 응답을 생성하기 위해 사용할 수 있는 최대 툴의 개수입니다. 이를 통해 불필요한 툴 호출과 무한 루프를 방지합니다.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"지능형 기술 선택\",\n        \"beta-badge\": \"베타\",\n        description:\n          \"쿼리당 무제한의 도구 사용 및 컷 토큰 사용량을 최대 80%까지 줄일 수 있습니다 – AnythingLLM은 모든 프롬프트에 적합한 기술을 자동으로 선택합니다.\",\n        \"max-tools\": {\n          title: \"맥스 툴스\",\n          description:\n            \"각 쿼리에 사용할 수 있는 최대 도구 수입니다. 큰 컨텍스트 모델의 경우, 이 값을 더 높은 값으로 설정하는 것을 권장합니다.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"워크스페이스 채팅\",\n    description:\n      \"이것들은 사용자들이 보낸 모든 채팅과 메시지입니다. 생성 날짜별로 정렬되어 있습니다.\",\n    export: \"내보내기\",\n    table: {\n      id: \"ID\",\n      by: \"보낸 사람\",\n      workspace: \"워크스페이스\",\n      prompt: \"프롬프트\",\n      response: \"응답\",\n      at: \"보낸 시각\",\n    },\n  },\n  customization: {\n    interface: {\n      title: \"UI 환경 설정\",\n      description: \"AnythingLLM의 UI 환경을 원하는 대로 설정하세요.\",\n    },\n    branding: {\n      title: \"브랜딩 및 화이트라벨링\",\n      description:\n        \"AnythingLLM 인스턴스에 맞춤 브랜딩을 적용해 화이트라벨링할 수 있습니다.\",\n    },\n    chat: {\n      title: \"채팅\",\n      description: \"AnythingLLM의 채팅 환경을 원하는 대로 설정하세요.\",\n      auto_submit: {\n        title: \"음성 입력 자동 전송\",\n        description:\n          \"일정 시간 동안 음성이 감지되지 않으면 음성 입력을 자동으로 전송합니다.\",\n      },\n      auto_speak: {\n        title: \"응답 자동 음성 출력\",\n        description: \"AI의 응답을 자동으로 음성으로 들려줍니다.\",\n      },\n      spellcheck: {\n        title: \"맞춤법 검사 활성화\",\n        description: \"채팅 입력란에서 맞춤법 검사를 켜거나 끌 수 있습니다.\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"테마\",\n        description: \"애플리케이션의 색상 테마를 선택하세요.\",\n      },\n      \"show-scrollbar\": {\n        title: \"스크롤바 표시\",\n        description: \"채팅 창에서 스크롤바를 표시하거나 숨길 수 있습니다.\",\n      },\n      \"support-email\": {\n        title: \"지원 이메일\",\n        description:\n          \"사용자가 도움이 필요할 때 접근할 수 있는 지원 이메일 주소를 설정하세요.\",\n      },\n      \"app-name\": {\n        title: \"이름\",\n        description:\n          \"로그인 페이지에 모든 사용자에게 표시될 애플리케이션 이름을 설정하세요.\",\n      },\n      \"display-language\": {\n        title: \"표시 언어\",\n        description:\n          \"AnythingLLM의 UI에 사용할 언어를 선택하세요. 번역이 제공되는 경우에만 적용됩니다.\",\n      },\n      logo: {\n        title: \"브랜드 로고\",\n        description: \"모든 페이지에 표시할 맞춤 로고를 업로드하세요.\",\n        add: \"맞춤 로고 추가\",\n        recommended: \"권장 크기: 800 x 200\",\n        remove: \"제거\",\n        replace: \"교체\",\n      },\n      \"browser-appearance\": {\n        title: \"브라우저 표시 설정\",\n        description:\n          \"앱이 열려 있을 때 브라우저 탭과 제목의 표시 방식을 맞춤 설정하세요.\",\n        tab: {\n          title: \"탭 제목\",\n          description:\n            \"앱이 브라우저에서 열려 있을 때 사용할 맞춤 탭 제목을 설정하세요.\",\n        },\n        favicon: {\n          title: \"파비콘\",\n          description: \"브라우저 탭에 사용할 맞춤 파비콘을 지정하세요.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"사이드바 하단 항목\",\n        description: \"사이드바 하단에 표시될 푸터 항목을 맞춤 설정하세요.\",\n        icon: \"아이콘\",\n        link: \"링크\",\n      },\n      \"render-html\": {\n        title: \"채팅에서 HTML 렌더링\",\n        description:\n          \"어시스턴트 응답에 HTML 응답을 표시합니다.\\n이는 응답 품질의 훨씬 더 높은 수준을 달성할 수 있지만, 잠재적인 보안 위험으로 이어질 수도 있습니다.\",\n      },\n    },\n  },\n  api: {\n    title: \"API 키\",\n    description:\n      \"API 키는 소유자가 프로그래밍 방식으로 이 AnythingLLM 인스턴스에 액세스하고 관리할 수 있도록 합니다.\",\n    link: \"API 문서 읽기\",\n    generate: \"새 API 키 생성\",\n    table: {\n      key: \"API 키\",\n      by: \"생성한 사람\",\n      created: \"생성일\",\n    },\n  },\n  llm: {\n    title: \"LLM 기본 설정\",\n    description:\n      \"이것은 채팅과 임베딩을 하기 위한 선호하는 LLM 제공자의 인증입니다. 이 키가 현재 활성 상태이고 정확해야 AnythingLLM이 제대로 작동합니다.\",\n    provider: \"LLM 제공자\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Azure 서비스 엔드포인트\",\n        api_key: \"API 키\",\n        chat_deployment_name: \"채팅 배포 이름\",\n        chat_model_token_limit: \"채팅 모델 토큰 한도\",\n        model_type: \"모델 유형\",\n        default: \"기본값\",\n        reasoning: \"추론\",\n        model_type_tooltip:\n          '만약 귀하의 배포가 추론 모델(o1, o1-mini, o3-mini 등)을 사용한다면, 이 옵션을 \"추론\"으로 설정하십시오. 그렇지 않으면, 챗봇 요청이 실패할 수 있습니다.',\n      },\n    },\n  },\n  transcription: {\n    title: \"텍스트 변환 모델 기본 설정\",\n    description:\n      \"이것은 선호하는 텍스트 변환 모델 제공자의 인증입니다. 이 키가 현재 활성 상태이고 정확해야 미디어 파일 및 오디오가 텍스트 변환됩니다.\",\n    provider: \"텍스트 변환 제공자\",\n    \"warn-start\":\n      \"RAM 또는 CPU 성능이 제한된 머신에서 로컬 위스퍼 모델을 사용하면 미디어 파일을 처리할 때 AnythingLLM이 중단될 수 있습니다.\",\n    \"warn-recommend\": \"최소 2GB RAM과 10Mb 보다 작은 파일 업로드를 권장합니다.\",\n    \"warn-end\": \"내장된 모델은 첫 번째 사용 시 자동으로 다운로드됩니다.\",\n  },\n  embedding: {\n    title: \"임베딩 기본 설정\",\n    \"desc-start\":\n      \"임베딩 엔진을 지원하지 않는 LLM을 사용할 때 텍스트를 임베딩하는 데 다른 임베딩 엔진 제공자의 인증이 필요할 수 있습니다.\",\n    \"desc-end\":\n      \"임베딩은 텍스트를 벡터로 변환하는 과정입니다. 파일과 프롬프트를 AnythingLLM이 처리할 수 있는 형식으로 변환하려면 이러한 인증이 필요합니다.\",\n    provider: {\n      title: \"임베딩 제공자\",\n    },\n  },\n  text: {\n    title: \"텍스트 분할 및 청킹 기본 설정\",\n    \"desc-start\":\n      \"새 문서를 벡터 데이터베이스에 삽입하기 전에 기본 텍스트 분할 방식을 변경할 수 있습니다.\",\n    \"desc-end\":\n      \"텍스트 분할 방식과 그 영향을 이해하고 있는 경우에만 이 설정을 변경해야 합니다.\",\n    size: {\n      title: \"텍스트 청크 크기\",\n      description: \"단일 벡터에 들어갈 수 있는 최대 문자 길이입니다.\",\n      recommend: \"임베드 모델 최대 길이는\",\n    },\n    overlap: {\n      title: \"텍스트 청크 겹침\",\n      description:\n        \"청킹 동안 두 인접 텍스트 청크 간에 겹칠 수 있는 최대 문자 수입니다.\",\n    },\n  },\n  vector: {\n    title: \"벡터 데이터베이스\",\n    description:\n      \"이것은 AnythingLLM 인스턴스가 벡터 데이터베이스 사용을 위한 인증 설정입니다. 이 키가 활성 상태이고 정확해야 합니다.\",\n    provider: {\n      title: \"벡터 데이터베이스 제공자\",\n      description: \"LanceDB를 선택하면 설정이 필요 없습니다.\",\n    },\n  },\n  embeddable: {\n    title: \"임베드 가능한 채팅 위젯\",\n    description:\n      \"임베드 가능한 채팅 위젯은 하나의 워크스페이스에 연결된 공개용 채팅방입니다. 이를 통해 워크스페이스 설정이 적용된 채팅방을 일반인들에게 공개할 수 있습니다.\",\n    create: \"임베드 생성\",\n    table: {\n      workspace: \"워크스페이스\",\n      chats: \"보낸 채팅\",\n      active: \"활성 도메인\",\n      created: \"생성일\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"임베드 채팅\",\n    export: \"내보내기\",\n    description: \"게시한 임베드에서의 모든 채팅과 메시지의 기록입니다.\",\n    table: {\n      embed: \"임베드\",\n      sender: \"보낸 사람\",\n      message: \"메시지\",\n      response: \"응답\",\n      at: \"보낸 시각\",\n    },\n  },\n  event: {\n    title: \"이벤트 로그\",\n    description:\n      \"모니터링을 위해 이 인스턴스에서 발생하는 모든 작업과 이벤트를 확인합니다.\",\n    clear: \"이벤트 로그 지우기\",\n    table: {\n      type: \"이벤트 유형\",\n      user: \"사용자\",\n      occurred: \"발생 시각\",\n    },\n  },\n  privacy: {\n    title: \"개인정보와 데이터 처리\",\n    description:\n      \"연결된 타사 제공자와 AnythingLLM이 데이터를 처리하는 방식을 구성합니다.\",\n    anonymous: \"익명 원격 분석 활성화\",\n  },\n  connectors: {\n    \"search-placeholder\": \"데이터 커넥터 검색\",\n    \"no-connectors\": \"데이터 커넥터를 찾을 수 없습니다.\",\n    obsidian: {\n      vault_location: \"볼트 위치\",\n      vault_description:\n        \"모든 노트와 연결을 가져오려면 Obsidian 볼트 폴더를 선택하세요.\",\n      selected_files: \"{{count}}개의 마크다운 파일을 찾았습니다\",\n      importing: \"볼트 가져오는 중...\",\n      import_vault: \"볼트 가져오기\",\n      processing_time: \"볼트 크기에 따라 시간이 다소 소요될 수 있습니다.\",\n      vault_warning:\n        \"충돌을 방지하려면 Obsidian 볼트가 현재 열려 있지 않은지 확인하세요.\",\n    },\n    github: {\n      name: \"GitHub 저장소\",\n      description:\n        \"공개 또는 비공개 GitHub 저장소 전체를 한 번의 클릭으로 가져옵니다.\",\n      URL: \"GitHub 저장소 URL\",\n      URL_explained: \"가져오려는 GitHub 저장소의 URL을 입력하세요.\",\n      token: \"GitHub 액세스 토큰\",\n      optional: \"선택 사항\",\n      token_explained: \"요청 제한을 방지하기 위한 액세스 토큰입니다.\",\n      token_explained_start: \"무엇보다 중요한 것은,\",\n      token_explained_link1: \"개인 액세스 토큰\",\n      token_explained_middle:\n        \"이 없으면 GitHub API의 요청 제한으로 인해 가져올 수 있는 파일 수가 제한될 수 있습니다. \",\n      token_explained_link2: \"임시 액세스 토큰을 생성\",\n      token_explained_end: \"하여 이 문제를 피할 수 있습니다.\",\n      ignores: \"파일 무시 목록\",\n      git_ignore:\n        \"수집 중 무시할 파일을 .gitignore 형식으로 입력하세요. 저장하려면 각 항목 입력 후 엔터를 누르세요.\",\n      task_explained:\n        \"가져오기가 완료되면 모든 파일이 문서 선택기에서 워크스페이스에 임베딩할 수 있도록 제공됩니다.\",\n      branch: \"가져올 브랜치\",\n      branch_loading: \"-- 사용 가능한 브랜치 불러오는 중 --\",\n      branch_explained: \"가져오려는 브랜치를 선택하세요.\",\n      token_information:\n        \"<b>GitHub 액세스 토큰</b>을 입력하지 않으면 GitHub의 공개 API 요청 제한으로 인해 이 데이터 커넥터는 저장소의 <b>최상위</b> 파일만 수집할 수 있습니다.\",\n      token_personal:\n        \"여기에서 GitHub 계정으로 무료 개인 액세스 토큰을 발급받을 수 있습니다.\",\n    },\n    gitlab: {\n      name: \"GitLab 저장소\",\n      description:\n        \"공개 또는 비공개 GitLab 저장소 전체를 한 번의 클릭으로 가져옵니다.\",\n      URL: \"GitLab 저장소 URL\",\n      URL_explained: \"가져오려는 GitLab 저장소의 URL을 입력하세요.\",\n      token: \"GitLab 액세스 토큰\",\n      optional: \"선택 사항\",\n      token_description: \"GitLab API에서 추가로 가져올 엔터티를 선택하세요.\",\n      token_explained_start: \"무엇보다 중요한 것은,\",\n      token_explained_link1: \"개인 액세스 토큰\",\n      token_explained_middle:\n        \"이 없으면 GitLab API의 요청 제한으로 인해 가져올 수 있는 파일 수가 제한될 수 있습니다. \",\n      token_explained_link2: \"임시 액세스 토큰을 생성\",\n      token_explained_end: \"하여 이 문제를 피할 수 있습니다.\",\n      fetch_issues: \"이슈를 문서로 가져오기\",\n      ignores: \"파일 무시 목록\",\n      git_ignore:\n        \"수집 중 무시할 파일을 .gitignore 형식으로 입력하세요. 저장하려면 각 항목 입력 후 엔터를 누르세요.\",\n      task_explained:\n        \"가져오기가 완료되면 모든 파일이 문서 선택기에서 워크스페이스에 임베딩할 수 있도록 제공됩니다.\",\n      branch: \"가져올 브랜치\",\n      branch_loading: \"-- 사용 가능한 브랜치 불러오는 중 --\",\n      branch_explained: \"가져오려는 브랜치를 선택하세요.\",\n      token_information:\n        \"<b>GitLab 액세스 토큰</b>을 입력하지 않으면 GitLab의 공개 API 요청 제한으로 인해 이 데이터 커넥터는 저장소의 <b>최상위</b> 파일만 수집할 수 있습니다.\",\n      token_personal:\n        \"여기에서 GitLab 계정으로 무료 개인 액세스 토큰을 발급받을 수 있습니다.\",\n    },\n    youtube: {\n      name: \"YouTube 자막 가져오기\",\n      description: \"링크를 통해 YouTube 동영상 전체의 자막을 가져옵니다.\",\n      URL: \"YouTube 동영상 URL\",\n      URL_explained_start:\n        \"자막을 가져올 YouTube 동영상의 URL을 입력하세요. 동영상에는 반드시 \",\n      URL_explained_link: \"자막(Closed Captions)\",\n      URL_explained_end: \" 이 활성화되어 있어야 합니다.\",\n      task_explained:\n        \"가져오기가 완료되면 자막이 문서 선택기에서 워크스페이스에 임베딩할 수 있도록 제공됩니다.\",\n    },\n    \"website-depth\": {\n      name: \"웹사이트 대량 링크 수집\",\n      description:\n        \"웹사이트와 하위 링크를 지정한 깊이까지 크롤링하여 수집합니다.\",\n      URL: \"웹사이트 URL\",\n      URL_explained: \"수집하려는 웹사이트의 URL을 입력하세요.\",\n      depth: \"크롤링 깊이\",\n      depth_explained:\n        \"시작 URL에서 몇 단계의 하위 링크까지 따라가서 수집할지 지정합니다.\",\n      max_pages: \"최대 페이지 수\",\n      max_pages_explained: \"수집할 최대 링크(페이지) 개수를 설정합니다.\",\n      task_explained:\n        \"수집이 완료되면 모든 크롤링된 콘텐츠가 문서 선택기에서 워크스페이스에 임베딩할 수 있도록 제공됩니다.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"한 번의 클릭으로 전체 Confluence 페이지를 가져옵니다.\",\n      deployment_type: \"Confluence 배포 유형\",\n      deployment_type_explained:\n        \"Confluence 인스턴스가 Atlassian 클라우드에 호스팅되어 있는지, 자체 서버에 설치되어 있는지 선택하세요.\",\n      base_url: \"Confluence 기본 URL\",\n      base_url_explained: \"Confluence 공간의 기본 URL을 입력하세요.\",\n      space_key: \"Confluence 스페이스 키\",\n      space_key_explained:\n        \"가져올 Confluence 스페이스의 키입니다. 보통 ~로 시작합니다.\",\n      username: \"Confluence 사용자명\",\n      username_explained: \"Confluence 계정의 사용자명을 입력하세요.\",\n      auth_type: \"Confluence 인증 방식\",\n      auth_type_explained:\n        \"Confluence 페이지에 접근할 때 사용할 인증 방식을 선택하세요.\",\n      auth_type_username: \"사용자명 + 액세스 토큰\",\n      auth_type_personal: \"개인 액세스 토큰\",\n      token: \"Confluence 액세스 토큰\",\n      token_explained_start:\n        \"인증을 위해 액세스 토큰이 필요합니다. 액세스 토큰은\",\n      token_explained_link: \"여기에서 생성할 수 있습니다\",\n      token_desc: \"인증용 액세스 토큰\",\n      pat_token: \"Confluence 개인 액세스 토큰\",\n      pat_token_explained: \"Confluence 계정의 개인 액세스 토큰입니다.\",\n      task_explained:\n        \"가져오기가 완료되면 페이지 내용이 문서 선택기에서 워크스페이스에 임베딩할 수 있도록 제공됩니다.\",\n      bypass_ssl: \"SSL 인증서 유효성 검사 우회\",\n      bypass_ssl_explained:\n        \"자체 서명된 인증서를 사용하는 자체 호스팅 Confluence 인스턴스에 대해 SSL 인증서 유효성 검사를 우회하기 위해 이 옵션을 활성화합니다.\",\n    },\n    manage: {\n      documents: \"문서 관리\",\n      \"data-connectors\": \"데이터 커넥터\",\n      \"desktop-only\":\n        \"이 설정은 데스크톱 환경에서만 편집할 수 있습니다. 계속하려면 데스크톱에서 이 페이지에 접속해 주세요.\",\n      dismiss: \"닫기\",\n      editing: \"편집 중\",\n    },\n    directory: {\n      \"my-documents\": \"내 문서\",\n      \"new-folder\": \"새 폴더\",\n      \"search-document\": \"문서 검색\",\n      \"no-documents\": \"문서 없음\",\n      \"move-workspace\": \"워크스페이스로 이동\",\n      \"delete-confirmation\":\n        \"이 파일과 폴더를 삭제하시겠습니까?\\n삭제 시 시스템에서 완전히 제거되며, 기존 워크스페이스에서도 자동으로 삭제됩니다.\\n이 작업은 되돌릴 수 없습니다.\",\n      \"removing-message\":\n        \"{{count}}개의 문서와 {{folderCount}}개의 폴더를 삭제하는 중입니다. 잠시만 기다려 주세요.\",\n      \"move-success\": \"{{count}}개의 문서를 성공적으로 이동했습니다.\",\n      no_docs: \"문서 없음\",\n      select_all: \"전체 선택\",\n      deselect_all: \"전체 선택 해제\",\n      remove_selected: \"선택 항목 삭제\",\n      costs: \"*임베딩 1회 비용\",\n      save_embed: \"저장 및 임베딩\",\n      \"total-documents_one\": \"{{count}} 문서\",\n      \"total-documents_other\": \"{{count}} 관련 문서\",\n    },\n    upload: {\n      \"processor-offline\": \"문서 처리기가 오프라인 상태입니다\",\n      \"processor-offline-desc\":\n        \"현재 문서 처리기가 오프라인이어서 파일을 업로드할 수 없습니다. 잠시 후 다시 시도해 주세요.\",\n      \"click-upload\": \"클릭하여 업로드하거나 파일을 끌어다 놓으세요\",\n      \"file-types\":\n        \"텍스트 파일, CSV, 스프레드시트, 오디오 파일 등 다양한 파일을 지원합니다!\",\n      \"or-submit-link\": \"또는 링크 제출\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"가져오는 중...\",\n      \"fetch-website\": \"웹사이트 가져오기\",\n      \"privacy-notice\":\n        \"이 파일들은 이 AnythingLLM 인스턴스에서 실행 중인 문서 처리기로 업로드됩니다. 파일은 제3자에게 전송되거나 공유되지 않습니다.\",\n    },\n    pinning: {\n      what_pinning: \"문서 고정이란 무엇인가요?\",\n      pin_explained_block1:\n        \"AnythingLLM에서 문서를 <b>고정</b>하면 해당 문서의 전체 내용을 프롬프트 창에 삽입하여 LLM이 완전히 이해할 수 있도록 합니다.\",\n      pin_explained_block2:\n        \"이 기능은 <b>대용량 컨텍스트 모델</b>이나 지식 기반에 중요한 소형 파일에 가장 적합합니다.\",\n      pin_explained_block3:\n        \"기본 설정만으로 원하는 답변을 얻지 못할 때, 문서 고정은 한 번의 클릭으로 더 높은 품질의 답변을 얻을 수 있는 좋은 방법입니다.\",\n      accept: \"확인했습니다\",\n    },\n    watching: {\n      what_watching: \"문서 감시는 무엇을 하나요?\",\n      watch_explained_block1:\n        \"AnythingLLM에서 문서를 <b>감시</b>하면 원본 소스에서 정기적으로 문서 내용을 <i>자동으로</i> 동기화합니다. 이 파일이 관리되는 모든 워크스페이스의 내용이 자동으로 업데이트됩니다.\",\n      watch_explained_block2:\n        \"이 기능은 현재 온라인 기반 콘텐츠만 지원하며, 수동으로 업로드한 문서에는 사용할 수 없습니다.\",\n      watch_explained_block3_start: \"감시 중인 문서는 \",\n      watch_explained_block3_link: \"파일 관리자\",\n      watch_explained_block3_end: \" 관리자 화면에서 관리할 수 있습니다.\",\n      accept: \"확인했습니다\",\n    },\n  },\n  chat_window: {\n    attachments_processing:\n      \"첨부 파일을 처리 중입니다. 잠시만 기다려 주세요...\",\n    send_message: \"메시지 보내기\",\n    attach_file: \"이 채팅에 파일 첨부\",\n    text_size: \"텍스트 크기 변경\",\n    microphone: \"프롬프트를 음성으로 입력\",\n    send: \"프롬프트 메시지를 워크스페이스로 전송\",\n    tts_speak_message: \"TTS로 메시지 읽기\",\n    copy: \"복사\",\n    regenerate: \"다시 생성\",\n    regenerate_response: \"응답 다시 생성\",\n    good_response: \"좋은 답변\",\n    more_actions: \"더 많은 작업\",\n    fork: \"포크\",\n    delete: \"삭제\",\n    cancel: \"취소\",\n    edit_prompt: \"프롬프트 수정\",\n    edit_response: \"응답 수정\",\n    preset_reset_description: \"채팅 기록을 초기화하고 새 채팅을 시작합니다\",\n    add_new_preset: \"새 프리셋 추가\",\n    command: \"명령어\",\n    your_command: \"your-command\",\n    placeholder_prompt: \"이 내용이 프롬프트 앞에 삽입됩니다.\",\n    description: \"설명\",\n    placeholder_description: \"LLM에 대한 시로 응답합니다.\",\n    save: \"저장\",\n    small: \"작게\",\n    normal: \"보통\",\n    large: \"크게\",\n    workspace_llm_manager: {\n      search: \"LLM 제공자 검색\",\n      loading_workspace_settings: \"워크스페이스 설정을 불러오는 중...\",\n      available_models: \"{{provider}}의 사용 가능한 모델\",\n      available_models_description:\n        \"이 워크스페이스에서 사용할 모델을 선택하세요.\",\n      save: \"이 모델 사용하기\",\n      saving: \"모델을 워크스페이스 기본값으로 설정 중...\",\n      missing_credentials: \"이 제공자의 인증 정보가 없습니다!\",\n      missing_credentials_description: \"클릭하여 인증 정보를 설정하세요\",\n    },\n    submit: \"제출\",\n    edit_info_user:\n      '\"제출\"은 AI 응답을 다시 생성합니다. \"저장\"은 사용자 메시지만 업데이트합니다.',\n    edit_info_assistant: \"당신이 변경한 내용은 바로 이 답변에 저장됩니다.\",\n    see_less: \"더 보기\",\n    see_more: \"더 보기\",\n    tools: \"도구\",\n    browse: \"검색\",\n    text_size_label: \"글자 크기\",\n    select_model: \"모델 선택\",\n    sources: \"출처\",\n    document: \"문서\",\n    similarity_match: \"경쟁\",\n    source_count_one: \"{{count}} 참조\",\n    source_count_other: \"{{count}} 관련 참고 자료\",\n    preset_exit_description: \"현재 에이전트 세션을 중단\",\n    add_new: \"새로운 항목 추가\",\n    edit: \"수정\",\n    publish: \"출판\",\n    stop_generating: \"응답 생성 중단\",\n    pause_tts_speech_message: \"메시지의 텍스트 음성 변환(TTS) 기능을 일시 중지\",\n    slash_commands: \"슬래시 명령어\",\n    agent_skills: \"에이전트의 역량\",\n    manage_agent_skills: \"에이전트 역량 관리\",\n    agent_skills_disabled_in_session:\n      \"활성 에이전트 세션 중에 기술을 변경할 수 없습니다. 먼저 /exit 명령을 사용하여 세션을 종료하십시오.\",\n    start_agent_session: \"에이전트 세션 시작\",\n    use_agent_session_to_use_tools:\n      \"채팅에서 도구를 사용하려면, 프롬프트의 시작 부분에 '@agent'을 사용하여 에이전트 세션을 시작할 수 있습니다.\",\n  },\n  profile_settings: {\n    edit_account: \"계정 정보 수정\",\n    profile_picture: \"프로필 사진\",\n    remove_profile_picture: \"프로필 사진 삭제\",\n    username: \"사용자명\",\n    new_password: \"새 비밀번호\",\n    password_description: \"비밀번호는 최소 8자 이상이어야 합니다.\",\n    cancel: \"취소\",\n    update_account: \"계정 정보 업데이트\",\n    theme: \"테마 설정\",\n    language: \"선호 언어\",\n    failed_upload: \"프로필 사진 업로드 실패: {{error}}\",\n    upload_success: \"프로필 사진이 업로드되었습니다.\",\n    failed_remove: \"프로필 사진 삭제 실패: {{error}}\",\n    profile_updated: \"프로필이 업데이트되었습니다.\",\n    failed_update_user: \"사용자 정보 업데이트 실패: {{error}}\",\n    account: \"계정\",\n    support: \"지원\",\n    signout: \"로그아웃\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"단축키 안내\",\n    shortcuts: {\n      settings: \"설정 열기\",\n      workspaceSettings: \"현재 워크스페이스 설정 열기\",\n      home: \"홈으로 이동\",\n      workspaces: \"워크스페이스 관리\",\n      apiKeys: \"API 키 설정\",\n      llmPreferences: \"LLM 기본 설정\",\n      chatSettings: \"채팅 설정\",\n      help: \"단축키 도움말 보기\",\n      showLLMSelector: \"워크스페이스 LLM 선택기 열기\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"성공!\",\n        success_description:\n          \"시스템 프롬프트가 커뮤니티 허브에 성공적으로 게시되었습니다!\",\n        success_thank_you: \"커뮤니티에 공유해주셔서 감사합니다!\",\n        view_on_hub: \"커뮤니티 허브에서 보기\",\n        modal_title: \"시스템 프롬프트 게시\",\n        name_label: \"이름\",\n        name_description: \"시스템 프롬프트의 표시 이름입니다.\",\n        name_placeholder: \"나의 시스템 프롬프트\",\n        description_label: \"설명\",\n        description_description:\n          \"시스템 프롬프트의 목적이나 용도를 설명해 주세요.\",\n        tags_label: \"태그\",\n        tags_description:\n          \"태그를 추가하면 시스템 프롬프트를 더 쉽게 검색할 수 있습니다. 여러 개의 태그를 추가할 수 있습니다. 최대 5개, 태그당 20자 이내로 입력해 주세요.\",\n        tags_placeholder: \"태그 입력 후 Enter를 눌러 추가\",\n        visibility_label: \"공개 범위\",\n        public_description: \"공개 시스템 프롬프트는 모든 사용자에게 보입니다.\",\n        private_description: \"비공개 시스템 프롬프트는 본인만 볼 수 있습니다.\",\n        publish_button: \"커뮤니티 허브에 게시\",\n        submitting: \"게시 중...\",\n        prompt_label: \"프롬프트\",\n        prompt_description:\n          \"실제로 LLM을 안내하는 데 사용될 시스템 프롬프트를 입력하세요.\",\n        prompt_placeholder: \"여기에 시스템 프롬프트를 입력하세요...\",\n      },\n      agent_flow: {\n        success_title: \"성공!\",\n        success_description:\n          \"에이전트 플로우가 커뮤니티 허브에 성공적으로 게시되었습니다!\",\n        success_thank_you: \"커뮤니티에 공유해주셔서 감사합니다!\",\n        view_on_hub: \"커뮤니티 허브에서 보기\",\n        modal_title: \"에이전트 플로우 게시\",\n        name_label: \"이름\",\n        name_description: \"에이전트 플로우의 표시 이름입니다.\",\n        name_placeholder: \"나의 에이전트 플로우\",\n        description_label: \"설명\",\n        description_description:\n          \"에이전트 플로우의 목적이나 용도를 설명해 주세요.\",\n        tags_label: \"태그\",\n        tags_description:\n          \"태그를 추가하면 에이전트 플로우를 더 쉽게 검색할 수 있습니다. 여러 개의 태그를 추가할 수 있습니다. 최대 5개, 태그당 20자 이내로 입력해 주세요.\",\n        tags_placeholder: \"태그 입력 후 Enter를 눌러 추가\",\n        visibility_label: \"공개 범위\",\n        submitting: \"게시 중...\",\n        submit: \"커뮤니티 허브에 게시\",\n        privacy_note:\n          \"에이전트 플로우는 민감한 데이터를 보호하기 위해 항상 비공개로 업로드됩니다. 게시 후 커뮤니티 허브에서 공개 범위를 변경할 수 있습니다. 게시 전에 플로우에 민감하거나 개인 정보가 포함되어 있지 않은지 꼭 확인해 주세요.\",\n      },\n      slash_command: {\n        success_title: \"성공!\",\n        success_description:\n          \"슬래시 커맨드가 커뮤니티 허브에 성공적으로 게시되었습니다!\",\n        success_thank_you: \"커뮤니티에 공유해주셔서 감사합니다!\",\n        view_on_hub: \"커뮤니티 허브에서 보기\",\n        modal_title: \"슬래시 커맨드 게시\",\n        name_label: \"이름\",\n        name_description: \"슬래시 커맨드의 표시 이름입니다.\",\n        name_placeholder: \"나의 슬래시 커맨드\",\n        description_label: \"설명\",\n        description_description:\n          \"슬래시 커맨드의 목적이나 용도를 설명해 주세요.\",\n        tags_label: \"태그\",\n        tags_description:\n          \"태그를 추가하면 슬래시 커맨드를 더 쉽게 검색할 수 있습니다. 여러 개의 태그를 추가할 수 있습니다. 최대 5개, 태그당 20자 이내로 입력해 주세요.\",\n        tags_placeholder: \"태그 입력 후 Enter를 눌러 추가\",\n        visibility_label: \"공개 범위\",\n        public_description: \"공개 슬래시 커맨드는 모든 사용자에게 보입니다.\",\n        private_description: \"비공개 슬래시 커맨드는 본인만 볼 수 있습니다.\",\n        publish_button: \"커뮤니티 허브에 게시\",\n        submitting: \"게시 중...\",\n        prompt_label: \"프롬프트\",\n        prompt_description: \"슬래시 커맨드가 실행될 때 사용될 프롬프트입니다.\",\n        prompt_placeholder: \"여기에 프롬프트를 입력하세요...\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"인증 필요\",\n          description:\n            \"항목을 게시하려면 AnythingLLM 커뮤니티 허브에 인증해야 합니다.\",\n          button: \"커뮤니티 허브에 연결\",\n        },\n      },\n    },\n  },\n  security: {\n    title: \"보안\",\n    multiuser: {\n      title: \"다중 사용자 모드\",\n      description:\n        \"다중 사용자 모드를 활성화하여 인스턴스가 팀 사용을 지원하도록 설정합니다.\",\n      enable: {\n        \"is-enable\": \"다중 사용자 모드가 활성화되었습니다\",\n        enable: \"다중 사용자 모드 활성화\",\n        description:\n          \"당신은 기본 관리자가 됩니다. 관리자로서 모든 신규 사용자 또는 관리자의 계정을 생성해야 합니다. 비밀번호를 잃어버리면 관리자만 비밀번호를 재설정할 수 있습니다.\",\n        username: \"관리자 계정 사용자 이름\",\n        password: \"관리자 계정 비밀번호\",\n      },\n    },\n    password: {\n      title: \"비밀번호 보호\",\n      description:\n        \"AnythingLLM 인스턴스를 비밀번호로 보호하십시오. 이 비밀번호를 잊어버리면 복구 방법이 없으므로 반드시 저장하세요.\",\n      \"password-label\": \"인스턴스 비밀번호\",\n    },\n  },\n  home: {\n    welcome: \"환영합니다\",\n    chooseWorkspace: \"워크스페이스를 선택하여 채팅을 시작하세요!\",\n    notAssigned:\n      \"현재 워크스페이스에 할당되지 않았습니다.\\n워크스페이스에 대한 접근을 요청하려면 관리자에게 문의하세요.\",\n    goToWorkspace: '워크스페이스로 이동 \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/lv/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"Laipni lūgti\",\n      getStarted: \"Sākt darbu\",\n    },\n    llm: {\n      title: \"LLM preferences\",\n      description:\n        \"AnythingLLM var strādāt ar daudziem LLM pakalpojumu sniedzējiem. Šis būs pakalpojums, kas apstrādās sarunas.\",\n    },\n    userSetup: {\n      title: \"Lietotāja iestatīšana\",\n      description: \"Konfigurējiet savus lietotāja iestatījumus.\",\n      howManyUsers: \"Cik daudz lietotāju izmantos šo instanci?\",\n      justMe: \"Tikai es\",\n      myTeam: \"Mana komanda\",\n      instancePassword: \"Instances parole\",\n      setPassword: \"Vai vēlaties iestatīt paroli?\",\n      passwordReq: \"Parolēm jābūt vismaz 8 rakstzīmes garām.\",\n      passwordWarn: \"Svarīgi saglabāt šo paroli, jo nav atjaunošanas metodes.\",\n      adminUsername: \"Administratora konta lietotājvārds\",\n      adminPassword: \"Administratora konta parole\",\n      adminPasswordReq: \"Parolēm jābūt vismaz 8 rakstzīmes garām.\",\n      teamHint:\n        \"Pēc noklusējuma jūs būsiet vienīgais administrators. Kad ievadīšana būs pabeigta, jūs varēsiet izveidot un uzaicināt citus būt par lietotājiem vai administratoriem. Neaizmirstiet savu paroli, jo tikai administratori var atiestatīt paroles.\",\n    },\n    data: {\n      title: \"Datu apstrāde un privātums\",\n      description:\n        \"Mēs esam apņēmušies nodrošināt caurskatāmību un kontroli pār jūsu personīgajiem datiem.\",\n      settingsHint:\n        \"Šos iestatījumus var pārkonfigurēt jebkurā laikā iestatījumos.\",\n    },\n    survey: {\n      title: \"Laipni lūgti AnythingLLM\",\n      description:\n        \"Palīdziet mums veidot AnythingLLM atbilstoši jūsu vajadzībām. Neobligāti.\",\n      email: \"Kāds ir jūsu e-pasts?\",\n      useCase: \"Kam izmantosiet AnythingLLM?\",\n      useCaseWork: \"Darbam\",\n      useCasePersonal: \"Personīgai lietošanai\",\n      useCaseOther: \"Citam nolūkam\",\n      comment: \"Kā jūs uzzinājāt par AnythingLLM?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube utt. - Ļaujiet mums zināt, kā jūs mūs atradāt!\",\n      skip: \"Izlaist aptauju\",\n      thankYou: \"Paldies par jūsu atsauksmi!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Darba telpas nosaukums\",\n    user: \"Lietotājs\",\n    selection: \"Modeļa izvēle\",\n    saving: \"Saglabā...\",\n    save: \"Saglabāt izmaiņas\",\n    previous: \"Iepriekšējā lapa\",\n    next: \"Nākamā lapa\",\n    optional: \"Neobligāti\",\n    yes: \"Jā\",\n    no: \"Nē\",\n    search: \"Meklēšana\",\n    username_requirements:\n      \"Lietotājvārdam jābūt 2–32 rakstzīmju garam, jāsākas ar mazo burtu un jāsatur tikai mazie burti, cipari, apakšsvītras, domuzīmes un punkti.\",\n    on: \"Par\",\n    none: \"Nav\",\n    stopped: \"Apstājās\",\n    loading: \"Ielāde\",\n    refresh: \"Atjaunot\",\n  },\n  settings: {\n    title: \"Instances iestatījumi\",\n    invites: \"Ielūgumi\",\n    users: \"Lietotāji\",\n    workspaces: \"Darba telpas\",\n    \"workspace-chats\": \"Darba telpas sarunas\",\n    customization: \"Pielāgošana\",\n    interface: \"UI preferences\",\n    branding: \"Zīmolraide un identitāte\",\n    chat: \"Sarunas\",\n    \"api-keys\": \"Izstrādātāja API\",\n    llm: \"LLM\",\n    transcription: \"Transkripcija\",\n    embedder: \"Embedder\",\n    \"text-splitting\": \"Teksta sadalītājs un sadrumstalošana\",\n    \"voice-speech\": \"Balss un runa\",\n    \"vector-database\": \"Vektoru datubāze\",\n    embeds: \"Sarunas ietvere\",\n    security: \"Drošība\",\n    \"event-logs\": \"Notikumu žurnāli\",\n    privacy: \"Privātums un dati\",\n    \"ai-providers\": \"AI pakalpojumu sniedzēji\",\n    \"agent-skills\": \"Aģenta prasmes\",\n    admin: \"Administrators\",\n    tools: \"Rīki\",\n    \"system-prompt-variables\": \"Sistēmas uzvednes mainīgie\",\n    \"experimental-features\": \"Eksperimentālās funkcijas\",\n    contact: \"Sazināties ar atbalstu\",\n    \"browser-extension\": \"Pārlūka paplašinājums\",\n    \"mobile-app\": \"AnythingLLM mobilā versija\",\n    \"community-hub\": {\n      title: \"Sabiedriskais centrs\",\n      trending: \"Izpētiet populārākās\",\n      \"your-account\": \"Jūsu konts\",\n      \"import-item\": \"Importētā prece\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Laipni lūgti\",\n      \"placeholder-username\": \"Lietotājvārds\",\n      \"placeholder-password\": \"Parole\",\n      login: \"Ielogoties\",\n      validating: \"Validē...\",\n      \"forgot-pass\": \"Aizmirsi paroli\",\n      reset: \"Atiestatīt\",\n    },\n    \"sign-in\": \"Piesakieties savā {{appName}} kontā.\",\n    \"password-reset\": {\n      title: \"Paroles atiestatīšana\",\n      description:\n        \"Sniedziet nepieciešamo informāciju zemāk, lai atiestatītu savu paroli.\",\n      \"recovery-codes\": \"Atjaunošanas kodi\",\n      \"back-to-login\": \"Atpakaļ uz pieteikšanos\",\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Izveidot aģentu\",\n      editWorkspace: \"Rediģēt darba telpu\",\n      uploadDocument: \"August failu\",\n    },\n    greeting: \"Kā es varu jums šodien palīdzēt?\",\n  },\n  \"new-workspace\": {\n    title: \"Jauna darba telpa\",\n    placeholder: \"Mana darba telpa\",\n  },\n  \"workspaces—settings\": {\n    general: \"Vispārīgie iestatījumi\",\n    chat: \"Sarunas iestatījumi\",\n    vector: \"Vektoru datubāze\",\n    members: \"Dalībnieki\",\n    agent: \"Aģenta konfigurācija\",\n  },\n  general: {\n    vector: {\n      title: \"Vektoru skaits\",\n      description: \"Kopējais vektoru skaits jūsu vektoru datubāzē.\",\n    },\n    names: {\n      description: \"Tas mainīs tikai jūsu darba telpas attēlojamo nosaukumu.\",\n    },\n    message: {\n      title: \"Ieteiktās sarunas ziņas\",\n      description:\n        \"Pielāgojiet ziņas, kas tiks ieteiktas jūsu darba telpas lietotājiem.\",\n      add: \"Pievienot jaunu ziņu\",\n      save: \"Saglabāt ziņas\",\n      heading: \"Izskaidro man\",\n      body: \"AnythingLLM priekšrocības\",\n    },\n    delete: {\n      title: \"Dzēst darba telpu\",\n      description:\n        \"Dzēst šo darba telpu un visus tās datus. Tas dzēsīs darba telpu visiem lietotājiem.\",\n      delete: \"Dzēst darba telpu\",\n      deleting: \"Dzēš darba telpu...\",\n      \"confirm-start\": \"Jūs gatavojaties dzēst visu savu\",\n      \"confirm-end\":\n        \"darba telpu. Tas noņems visus vektoru iegulšanas jūsu vektoru datubāzē.\\n\\nOriģinālie avota faili paliks neskartie. Šī darbība ir neatgriezeniska.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Darba telpas LLM pakalpojumu sniedzējs\",\n      description:\n        \"Konkrētais LLM pakalpojumu sniedzējs un modelis, kas tiks izmantots šai darba telpai. Pēc noklusējuma tas izmanto sistēmas LLM pakalpojumu sniedzēju un iestatījumus.\",\n      search: \"Meklēt visus LLM pakalpojumu sniedzējus\",\n    },\n    model: {\n      title: \"Darba telpas sarunas modelis\",\n      description:\n        \"Konkrētais sarunas modelis, kas tiks izmantots šai darba telpai. Ja tukšs, izmantos sistēmas LLM preferences.\",\n    },\n    mode: {\n      title: \"Sarunas režīms\",\n      chat: {\n        title: \"Saruna\",\n        description:\n          'sniedz atbildes, izmantojot LLM vispārīgo zināšanu un dokumenta kontekstu, kas ir pieejams.<br />Lai izmantotu rīkus, jums jāizmantojat komandu \"@agent\".',\n      },\n      query: {\n        title: \"Vaicājums\",\n        description:\n          'sniedz atbildes <b> tikai </b>, ja dokumenta konteksts ir atrasts. <br />Lai izmantotu rīkus, jums būs jāizmanto komanda \"@agent\".',\n      },\n      automatic: {\n        title: \"Automobiļs\",\n        description:\n          'automātiski izmantos rīkus, ja modelis un sniedzējs atbalsta vietējo rīku izmantošanu. <br />Ja vietējā rīku izmantošana netiek atbalstīta, jums būs jāizmantojas \"@agent\" komanda, lai izmantotu rīkus.',\n      },\n    },\n    history: {\n      title: \"Sarunu vēsture\",\n      \"desc-start\":\n        \"Iepriekšējo sarunu skaits, kas tiks iekļauts atbildes īslaicīgajā atmiņā.\",\n      recommend: \"Ieteicams 20. \",\n      \"desc-end\":\n        \"Vairāk nekā 45 var novest pie nepārtrauktām sarunu kļūmēm atkarībā no ziņojuma izmēra.\",\n    },\n    prompt: {\n      title: \"Sistēmas uzvedne\",\n      description:\n        \"Uzvedne, kas tiks izmantota šajā darba telpā. Definējiet kontekstu un instrukcijas AI, lai ģenerētu atbildi. Jums vajadzētu nodrošināt rūpīgi izstrādātu uzvedni, lai AI varētu ģenerēt atbilstošu un precīzu atbildi.\",\n      history: {\n        title: \"Sistēmas uzvednes vēsture\",\n        clearAll: \"Notīrīt visu\",\n        noHistory: \"Nav pieejama sistēmas uzvednes vēsture\",\n        restore: \"Atjaunot\",\n        delete: \"Dzēst\",\n        deleteConfirm: \"Vai tiešām vēlaties dzēst šo vēstures ierakstu?\",\n        clearAllConfirm:\n          \"Vai tiešām vēlaties nodzēst visu vēsturi? Šo darbību nevar atsaukt.\",\n        expand: \"Paplašināt\",\n        publish: \"Publicē savu saturu Community Hub.\",\n      },\n    },\n    refusal: {\n      title: \"Vaicājuma režīma atteikuma atbilde\",\n      \"desc-start\": \"Kad\",\n      query: \"vaicājuma\",\n      \"desc-end\":\n        \"režīmā, jūs varētu vēlēties atgriezt pielāgotu atteikuma atbildi, kad konteksts nav atrasts.\",\n      \"tooltip-title\": \"Kāpēc es to redzu?\",\n      \"tooltip-description\":\n        \"Jūs atrodaties meklēšanas režīmā, kas izmanto tikai informāciju no jūsu dokumentiem. Izmantojiet runas režīmu, lai nodrošinātu elastīgākas sarunas, vai noklikšķiniet šeit, lai apmeklētu mūsu dokumentāciju un iegūtu vairāk informācijas par runas režīmiem.\",\n    },\n    temperature: {\n      title: \"LLM Temperatūra\",\n      \"desc-start\":\n        'Šis iestatījums kontrolē, cik \"radošas\" būs jūsu LLM atbildes.',\n      \"desc-end\":\n        \"Jo lielāks skaitlis, jo radošākas atbildes. Dažiem modeļiem tas var novest pie nesaprotamām atbildēm, ja iestatīts pārāk augsts.\",\n      hint: \"Lielākajai daļai LLM ir dažādi pieņemami derīgo vērtību diapazoni. Konsultējieties ar savu LLM pakalpojumu sniedzēju par šo informāciju.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Vektoru datubāzes identifikators\",\n    snippets: {\n      title: \"Maksimālie konteksta fragmenti\",\n      description:\n        \"Šis iestatījums kontrolē maksimālo konteksta fragmentu skaitu, kas tiks nosūtīti LLM katrai sarunai vai vaicājumam.\",\n      recommend: \"Ieteicams: 4\",\n    },\n    doc: {\n      title: \"Dokumentu līdzības slieksnis\",\n      description:\n        \"Minimālais līdzības rādītājs, kas nepieciešams, lai avots tiktu uzskatīts par saistītu ar sarunu. Jo lielāks skaitlis, jo līdzīgākam avotam jābūt sarunai.\",\n      zero: \"Bez ierobežojuma\",\n      low: \"Zems (līdzības vērtējums ≥ .25)\",\n      medium: \"Vidējs (līdzības vērtējums ≥ .50)\",\n      high: \"Augsts (līdzības vērtējums ≥ .75)\",\n    },\n    reset: {\n      reset: \"Atiestatīt vektoru datubāzi\",\n      resetting: \"Notīra vektorus...\",\n      confirm:\n        \"Jūs gatavojaties atiestatīt šīs darba telpas vektoru datubāzi. Tas noņems visas pašlaik iegultās vektoru iegulšanas.\\n\\nOriģinālie avota faili paliks neskartie. Šī darbība ir neatgriezeniska.\",\n      error: \"Darba telpas vektoru datubāzi nevarēja atiestatīt!\",\n      success: \"Darba telpas vektoru datubāze tika atiestatīta!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"LLM, kas tieši neatbalsta rīku izsaukumus, veiktspēja ir ļoti atkarīga no modeļa iespējām un precizitātes. Dažas iespējas var būt ierobežotas vai nefunkcionālas.\",\n    provider: {\n      title: \"Darba telpas aģenta LLM pakalpojumu sniedzējs\",\n      description:\n        \"Konkrētais LLM pakalpojumu sniedzējs un modelis, kas tiks izmantots šīs darba telpas @agent aģentam.\",\n    },\n    mode: {\n      chat: {\n        title: \"Darba telpas aģenta sarunas modelis\",\n        description:\n          \"Konkrētais sarunas modelis, kas tiks izmantots šīs darba telpas @agent aģentam.\",\n      },\n      title: \"Darba telpas aģenta modelis\",\n      description:\n        \"Konkrētais LLM modelis, kas tiks izmantots šīs darba telpas @agent aģentam.\",\n      wait: \"-- gaida modeļus --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG un ilgtermiņa atmiņa\",\n        description:\n          'Ļaujiet aģentam izmantot jūsu lokālos dokumentus, lai atbildētu uz vaicājumu, vai lūdziet aģentu \"atcerēties\" satura daļas ilgtermiņa atmiņas izguvei.',\n      },\n      view: {\n        title: \"Skatīt un apkopot dokumentus\",\n        description:\n          \"Ļaujiet aģentam uzskaitīt un apkopot pašlaik iegulto darba telpas failu saturu.\",\n      },\n      scrape: {\n        title: \"Iegūt tīmekļa vietnes\",\n        description: \"Ļaujiet aģentam apmeklēt un iegūt tīmekļa vietņu saturu.\",\n      },\n      generate: {\n        title: \"Ģenerēt diagrammas\",\n        description:\n          \"Iespējot noklusējuma aģentam ģenerēt dažāda veida diagrammas no sarunā sniegtajiem vai dotajiem datiem.\",\n      },\n      save: {\n        title: \"Ģenerēt un saglabāt failus pārlūkā\",\n        description:\n          \"Iespējot noklusējuma aģentam ģenerēt un rakstīt failus, kas saglabājas un var tikt lejupielādēti jūsu pārlūkā.\",\n      },\n      web: {\n        title: \"Tiešsaistes tīmekļa meklēšana un pārlūkošana\",\n        description:\n          \"Iegādājieties iespēju, lai jūsu aģents varētu meklēt informāciju internetā, lai atbildētu uz jūsu jautājumiem, pieslēdzoties tīmekļa meklēšanas (SERP) pakalpojuma sniedzējam.\",\n      },\n      sql: {\n        title: \"SQL savienotājs\",\n        description:\n          \"Ļauj savam pārstāvim izmantot SQL, lai atbildētu uz jūsu jautājumiem, savienojoties ar dažādiem SQL datubāzes sniedzējiem.\",\n      },\n      default_skill:\n        \"Par iestatījumu, šī spēja ir aktivizēta, taču jūs varat to izslēgt, ja nevēlaties, lai tā būtu pieejama aģentam.\",\n    },\n    mcp: {\n      title: \"MCP serveri\",\n      \"loading-from-config\": \"Ielādot MCP serverus no konfigūrācijas faila\",\n      \"learn-more\": \"Uzziniet vairāk par MCP serveriem.\",\n      \"no-servers-found\": \"Neizdevās atrast kādus MCP serverus.\",\n      \"tool-warning\":\n        \"Lai nodrošinātu optimālu darbību, apsveriet iespēju atspēlot nevajadzīgus rīkus, lai saglabātu kontekstu.\",\n      \"stop-server\": \"Aizvert MCP serveri\",\n      \"start-server\": \"Sākt MCP serveri\",\n      \"delete-server\": \"Dzēst MCP serveri\",\n      \"tool-count-warning\":\n        \"Šis MCP servers ir aktivizētas <b> instrumenti, kas izmantos kontekstu katrā sarunā.</b> Iespējams, ir labāk deaktivizēt nevēlamus instrumentus, lai saglabātu kontekstu.\",\n      \"startup-command\": \"Sākuma komanda\",\n      command: \"Instrukcijas\",\n      arguments: \"Pamatatpersonas\",\n      \"not-running-warning\":\n        \"Šis MCP servers darbojas – iespējams, tas ir izslēgts vai piedzīvo kļūdu, kad tiek ieslēgts.\",\n      \"tool-call-arguments\": \"Parametri, kas tiek nosūtīti rīkam\",\n      \"tools-enabled\": \"rīki atļauti\",\n    },\n    settings: {\n      title: \"Aģenta spēju iestatījumi\",\n      \"max-tool-calls\": {\n        title: \"Maksimālais rēķinu skaits vienam atbildē\",\n        description:\n          \"Maksimālais rīku skaits, ko aģents var apvienot, lai ģenerētu vienu atbildi. Tas novērina neierobežotu rīku izmantošanu un beidzoties.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Izglītības un prasmu izvēle, kas balstota uz spējām\",\n        \"beta-badge\": \"Beta\",\n        description:\n          'Ievērojiet neierobežotu rīku un \"cut token\" izmantošanas samazinājumu līdz 80% uz katru pieprasījumu – AnythingLLM automātiski izvēlas piemērotākās prasmes katram pieprasījumam.',\n        \"max-tools\": {\n          title: \"Max Tools\",\n          description:\n            \"Maksimālais rīku skaits, kas var tikt izvēlts katrai meklēšanai. Mēs iesakām iestatīt šo vērtību, lai iegūtu lielāku kontekstu modelus.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Darba telpas sarunas\",\n    description:\n      \"Šīs ir visas ierakstītās sarunas un ziņas, ko lietotāji ir nosūtījuši, sakārtotas pēc to izveides datuma.\",\n    export: \"Eksportēt\",\n    table: {\n      id: \"ID\",\n      by: \"Nosūtīja\",\n      workspace: \"Darba telpa\",\n      prompt: \"Uzvedne\",\n      response: \"Atbilde\",\n      at: \"Nosūtīts\",\n    },\n  },\n  customization: {\n    interface: {\n      title: \"UI preferences\",\n      description: \"Iestatiet savas UI preferences AnythingLLM.\",\n    },\n    branding: {\n      title: \"Zīmolrade un identitāte\",\n      description:\n        \"Pielāgojiet savu AnythingLLM instanci ar pielāgotu zīmolradi.\",\n    },\n    chat: {\n      title: \"Saruna\",\n      description: \"Iestatiet savas sarunas preferences AnythingLLM.\",\n      auto_submit: {\n        title: \"Automātiski iesniegt runas ievadi\",\n        description: \"Automātiski iesniegt runas ievadi pēc klusuma perioda\",\n      },\n      auto_speak: {\n        title: \"Automātiski runāt atbildes\",\n        description: \"Automātiski runāt atbildes no AI\",\n      },\n      spellcheck: {\n        title: \"Iespējot pareizrakstības pārbaudi\",\n        description:\n          \"Iespējot vai atspējot pareizrakstības pārbaudi sarunas ievades laukā\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Tēma\",\n        description: \"Izvēlieties vēlamo krāsu tēmu lietotnei.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Rādīt ritjoslu\",\n        description: \"Iespējot vai atspējot ritjoslu sarunas logā.\",\n      },\n      \"support-email\": {\n        title: \"Atbalsta e-pasts\",\n        description:\n          \"Iestatiet atbalsta e-pasta adresi, kam lietotājiem jābūt pieejamam, kad viņiem nepieciešama palīdzība.\",\n      },\n      \"app-name\": {\n        title: \"Nosaukums\",\n        description:\n          \"Iestatiet nosaukumu, kas tiek rādīts pieteikšanās lapā visiem lietotājiem.\",\n      },\n      \"display-language\": {\n        title: \"Displeja valoda\",\n        description:\n          \"Izvēlieties vēlamo valodu AnythingLLM lietotāja saskarnei - kad pieejami tulkojumi.\",\n      },\n      logo: {\n        title: \"Zīmola logotips\",\n        description:\n          \"Augšupielādējiet savu pielāgoto logotipu, lai to rādītu visās lapās.\",\n        add: \"Pievienot pielāgotu logotipu\",\n        recommended: \"Ieteicamais izmērs: 800 x 200\",\n        remove: \"Noņemt\",\n        replace: \"Aizvietot\",\n      },\n      \"browser-appearance\": {\n        title: \"Pārlūkprogrammas izskats\",\n        description:\n          \"Pielāgojiet pārlūkprogrammas cilnes izskatu un nosaukumu, kad lietotne ir atvērta.\",\n        tab: {\n          title: \"Nosaukums\",\n          description:\n            \"Iestatiet pielāgotu cilnes nosaukumu, kad lietotne ir atvērta pārlūkprogrammā.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description: \"Izmantojiet pielāgotu favicon pārlūkprogrammas cilnei.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Sānu joslas kājenes vienumi\",\n        description:\n          \"Pielāgojiet kājenes vienumus, kas tiek attēloti sānu joslas apakšā.\",\n        icon: \"Ikona\",\n        link: \"Saite\",\n      },\n      \"render-html\": {\n        title: \"Izveidot HTML saturu, ko var izmantot čatā.\",\n        description:\n          \"Ievietojiet HTML atbildes palīdzības atbildēs.\\nTas var novērst daudz augstāku atbildes kvalitātes līmeni, taču arī var radīt potenciālas drošības riskus.\",\n      },\n    },\n  },\n  api: {\n    title: \"API atslēgas\",\n    description:\n      \"API atslēgas ļauj to īpašniekam programmatiski piekļūt un pārvaldīt šo AnythingLLM instanci.\",\n    link: \"Lasīt API dokumentāciju\",\n    generate: \"Ģenerēt jaunu API atslēgu\",\n    table: {\n      key: \"API atslēga\",\n      by: \"Izveidoja\",\n      created: \"Izveidots\",\n    },\n  },\n  llm: {\n    title: \"LLM preferences\",\n    description:\n      \"Šie ir akreditācijas dati un iestatījumi jūsu vēlamajam LLM čata un iegulšanas pakalpojuma sniedzējam. Ir svarīgi, lai šīs atslēgas būtu aktuālas un pareizas, pretējā gadījumā AnythingLLM nedarbosies pareizi.\",\n    provider: \"LLM pakalpojuma sniedzējs\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Azure pakalpojuma gala punkts\",\n        api_key: \"API atslēņa\",\n        chat_deployment_name: \"Izvietošanas nosaukums\",\n        chat_model_token_limit:\n          'Žurnāla \"The Guardian\" raksts \"How to build a sustainable city\" (\"Kā izveidot ilgtspējīgu pilsētu\")\\n\\n\\nŽurnāla \"The Guardian\" raksts \"How to build a sustainable city\" (\"Kā izveidot ilgtspējīgu pilsētu\")',\n        model_type: \"Modeļa veids\",\n        default: \"Standarta\",\n        reasoning: \"Pamatojums\",\n        model_type_tooltip:\n          'Ja jūsu lietojums izmanto loģiskā modelī (o1, o1-mini, o3-mini utt.), norādiet, ka tas ir \"Loģisks\". Citi gadījumā jūsu sarunu pieprasījumi var neizpildīties.',\n      },\n    },\n  },\n  transcription: {\n    title: \"Transkripcijas modeļa preferences\",\n    description:\n      \"Šie ir akreditācijas dati un iestatījumi jūsu vēlamajam transkripcijas modeļa pakalpojuma sniedzējam. Ir svarīgi, lai šīs atslēgas būtu aktuālas un pareizas, pretējā gadījumā multivides faili un audio netiks transkribēti.\",\n    provider: \"Transkripcijas pakalpojuma sniedzējs\",\n    \"warn-start\":\n      \"Izmantojot lokālo whisper modeli iekārtās ar ierobežotu RAM vai CPU var apstādināt AnythingLLM, apstrādājot multivides failus.\",\n    \"warn-recommend\":\n      \"Mēs iesakām vismaz 2GB RAM un augšupielādēt failus <10Mb.\",\n    \"warn-end\":\n      \"Iebūvētais modelis automātiski lejupielādēsies pirmajā lietošanas reizē.\",\n  },\n  embedding: {\n    title: \"Iegulšanas preferences\",\n    \"desc-start\":\n      \"Izmantojot LLM, kas neatbalsta iebūvētu iegulšanas dzinēju - jums var būt nepieciešams papildus norādīt akreditācijas datus teksta iegulšanai.\",\n    \"desc-end\":\n      \"Iegulšana ir process, ar kuru teksts tiek pārveidots vektoros. Šie akreditācijas dati ir nepieciešami, lai pārveidotu jūsu failus un vaicājumus formātā, kuru AnythingLLM var izmantot apstrādei.\",\n    provider: {\n      title: \"Iegulšanas pakalpojuma sniedzējs\",\n    },\n  },\n  text: {\n    title: \"Teksta sadalīšanas un sagatavošanas preferences\",\n    \"desc-start\":\n      \"Dažreiz jūs, iespējams, vēlēsieties mainīt noklusējuma veidu, kā jauni dokumenti tiek sadalīti un sagatavoti pirms ievietošanas vektoru datubāzē.\",\n    \"desc-end\":\n      \"Jums vajadzētu mainīt šo iestatījumu tikai tad, ja saprotat, kā darbojas teksta sadalīšana un tās blakusefekti.\",\n    size: {\n      title: \"Teksta gabala izmērs\",\n      description:\n        \"Šis ir maksimālais rakstzīmju skaits, kas var būt vienā vektorā.\",\n      recommend: \"Iegult modeļa maksimālo garumu ir\",\n    },\n    overlap: {\n      title: \"Teksta gabalu pārklāšanās\",\n      description:\n        \"Šī ir maksimālā rakstzīmju pārklāšanās, kas notiek sadalīšanas laikā starp diviem blakus esošiem teksta gabaliem.\",\n    },\n  },\n  vector: {\n    title: \"Vektoru datubāze\",\n    description:\n      \"Šie ir akreditācijas dati un iestatījumi tam, kā darbosies jūsu AnythingLLM instance. Ir svarīgi, lai šīs atslēgas būtu aktuālas un pareizas.\",\n    provider: {\n      title: \"Vektoru datubāzes pakalpojuma sniedzējs\",\n      description: \"LanceDB nav nepieciešama konfigurācija.\",\n    },\n  },\n  embeddable: {\n    title: \"Iegulstāmie čata logrīki\",\n    description:\n      \"Iegulstāmie čata logrīki ir publiskas saziņas saskarnes, kas ir piesaistītas vienam darbam. Tie ļauj izveidot darba vietas, kuras pēc tam varat publicēt pasaulē.\",\n    create: \"Izveidot iegulumu\",\n    table: {\n      workspace: \"Darba vieta\",\n      chats: \"Nosūtītie čati\",\n      active: \"Aktīvie domēni\",\n      created: \"Izveidotais\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Iegulto čatu saraksts\",\n    export: \"Eksportēt\",\n    description:\n      \"Šie ir visi ierakstītie čati un ziņojumi no jebkura iegultā logrīka, ko esat publicējis.\",\n    table: {\n      embed: \"Iegultais\",\n      sender: \"Sūtītājs\",\n      message: \"Ziņojums\",\n      response: \"Atbilde\",\n      at: \"Nosūtīts\",\n    },\n  },\n  event: {\n    title: \"Notikumu žurnāli\",\n    description:\n      \"Skatiet visas darbības un notikumus, kas notiek šajā instancē uzraudzības nolūkos.\",\n    clear: \"Notīrīt notikumu žurnālus\",\n    table: {\n      type: \"Notikuma veids\",\n      user: \"Lietotājs\",\n      occurred: \"Notika\",\n    },\n  },\n  privacy: {\n    title: \"Privātums un datu apstrāde\",\n    description:\n      \"Šī ir jūsu konfigurācija tam, kā savienotie trešo pušu pakalpojumu sniedzēji un AnythingLLM apstrādā jūsu datus.\",\n    anonymous: \"Anonīmā telemetrija iespējota\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Meklēt datu savienotājus\",\n    \"no-connectors\": \"Nav atrasti datu savienotāji.\",\n    obsidian: {\n      vault_location: \"Krātuves atrašanās vieta\",\n      vault_description:\n        \"Atlasiet savu Obsidian krātuves mapi, lai importētu visas piezīmes un to savienojumus.\",\n      selected_files: \"Atrasti {{count}} markdown faili\",\n      importing: \"Notiek krātuves importēšana...\",\n      import_vault: \"Importēt krātuvi\",\n      processing_time:\n        \"Tas var aizņemt laiku atkarībā no jūsu krātuves lieluma.\",\n      vault_warning:\n        \"Lai izvairītos no konfliktiem, pārliecinieties, ka jūsu Obsidian krātuve pašlaik nav atvērta.\",\n    },\n    github: {\n      name: \"GitHub repozitorijs\",\n      description:\n        \"Importējiet visu publisku vai privātu GitHub repozitoriju ar vienu klikšķi.\",\n      URL: \"GitHub repozitorija URL\",\n      URL_explained: \"GitHub repozitorija URL, kuru vēlaties savākt.\",\n      token: \"GitHub piekļuves tokens\",\n      optional: \"neobligāts\",\n      token_explained: \"Piekļuves tokens, lai novērstu ātruma ierobežojumus.\",\n      token_explained_start: \"Bez \",\n      token_explained_link1: \"personiskā piekļuves tokena\",\n      token_explained_middle:\n        \", GitHub API var ierobežot savācamo failu skaitu ātruma ierobežojumu dēļ. Jūs varat \",\n      token_explained_link2: \"izveidot pagaidu piekļuves tokenu\",\n      token_explained_end: \", lai izvairītos no šīs problēmas.\",\n      ignores: \"Failu ignorēšana\",\n      git_ignore:\n        \"Saraksts .gitignore formātā, lai ignorētu konkrētus failus savākšanas laikā. Nospiediet enter pēc katra ieraksta, kuru vēlaties saglabāt.\",\n      task_explained:\n        \"Kad tas būs pabeigts, visi faili būs pieejami iegulšanai darba vietās dokumentu atlasītājā.\",\n      branch: \"Zars, no kura vēlaties savākt failus.\",\n      branch_loading: \"-- notiek pieejamo zaru ielāde --\",\n      branch_explained: \"Zars, no kura vēlaties savākt failus.\",\n      token_information:\n        \"Bez <b>GitHub piekļuves tokena</b> aizpildīšanas šis datu savienotājs varēs savākt tikai <b>augšējā līmeņa</b> failus repozitorijā GitHub publiskā API ātruma ierobežojumu dēļ.\",\n      token_personal:\n        \"Iegūstiet bezmaksas personisko piekļuves tokenu ar GitHub kontu šeit.\",\n    },\n    gitlab: {\n      name: \"GitLab repozitorijs\",\n      description:\n        \"Importējiet visu publisku vai privātu GitLab repozitoriju ar vienu klikšķi.\",\n      URL: \"GitLab repozitorija URL\",\n      URL_explained: \"GitLab repozitorija URL, kuru vēlaties savākt.\",\n      token: \"GitLab piekļuves tokens\",\n      optional: \"neobligāts\",\n      token_description: \"Atlasiet papildu entītijas, ko iegūt no GitLab API.\",\n      token_explained_start: \"Bez \",\n      token_explained_link1: \"personiskā piekļuves tokena\",\n      token_explained_middle:\n        \", GitLab API var ierobežot savācamo failu skaitu ātruma ierobežojumu dēļ. Jūs varat \",\n      token_explained_link2: \"izveidot pagaidu piekļuves tokenu\",\n      token_explained_end: \", lai izvairītos no šīs problēmas.\",\n      fetch_issues: \"Iegūt problēmas kā dokumentus\",\n      ignores: \"Failu ignorēšana\",\n      git_ignore:\n        \"Saraksts .gitignore formātā, lai ignorētu konkrētus failus savākšanas laikā. Nospiediet enter pēc katra ieraksta, kuru vēlaties saglabāt.\",\n      task_explained:\n        \"Kad tas būs pabeigts, visi faili būs pieejami iegulšanai darba vietās dokumentu atlasītājā.\",\n      branch: \"Zars, no kura vēlaties savākt failus\",\n      branch_loading: \"-- notiek pieejamo zaru ielāde --\",\n      branch_explained: \"Zars, no kura vēlaties savākt failus.\",\n      token_information:\n        \"Bez <b>GitLab piekļuves tokena</b> aizpildīšanas šis datu savienotājs varēs savākt tikai <b>augšējā līmeņa</b> failus repozitorijā GitLab publiskā API ātruma ierobežojumu dēļ.\",\n      token_personal:\n        \"Iegūstiet bezmaksas personisko piekļuves tokenu ar GitLab kontu šeit.\",\n    },\n    youtube: {\n      name: \"YouTube transkripcija\",\n      description: \"Importējiet visa YouTube video transkripciju no saites.\",\n      URL: \"YouTube video URL\",\n      URL_explained_start:\n        \"Ievadiet jebkura YouTube video URL, lai iegūtu tā transkripciju. Video ir jābūt pieejamiem \",\n      URL_explained_link: \"slēgtajiem parakstiem\",\n      URL_explained_end: \".\",\n      task_explained:\n        \"Kad tas būs pabeigts, transkripcija būs pieejama iegulšanai darba vietās dokumentu atlasītājā.\",\n    },\n    \"website-depth\": {\n      name: \"Vairāku saišu skrāpētājs\",\n      description:\n        \"Skrāpējiet vietni un tās apakšsaites līdz noteiktam dziļumam.\",\n      URL: \"Vietnes URL\",\n      URL_explained: \"URL vietnei, kuru vēlaties skrāpēt.\",\n      depth: \"Pārmeklēšanas dziļums\",\n      depth_explained:\n        \"Šis ir bērnu saišu skaits, kurām darbiniekam būtu jāseko no sākotnējā URL.\",\n      max_pages: \"Maksimālais lapu skaits\",\n      max_pages_explained: \"Maksimālais skrāpējamo saišu skaits.\",\n      task_explained:\n        \"Kad tas būs pabeigts, viss skrāpētais saturs būs pieejams iegulšanai darba vietās dokumentu atlasītājā.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Importējiet visu Confluence lapu ar vienu klikšķi.\",\n      deployment_type: \"Confluence izvietošanas veids\",\n      deployment_type_explained:\n        \"Nosakiet, vai jūsu Confluence instance ir izvietota Atlassian mākonī vai pašu uzturētā.\",\n      base_url: \"Confluence pamata URL\",\n      base_url_explained: \"Šis ir jūsu Confluence telpas pamata URL.\",\n      space_key: \"Confluence telpas atslēga\",\n      space_key_explained:\n        \"Šī ir jūsu confluence instances telpas atslēga, kas tiks izmantota. Parasti sākas ar ~\",\n      username: \"Confluence lietotājvārds\",\n      username_explained: \"Jūsu Confluence lietotājvārds\",\n      auth_type: \"Confluence autentifikācijas veids\",\n      auth_type_explained:\n        \"Atlasiet autentifikācijas veidu, kuru vēlaties izmantot, lai piekļūtu savām Confluence lapām.\",\n      auth_type_username: \"Lietotājvārds un piekļuves tokens\",\n      auth_type_personal: \"Personiskais piekļuves tokens\",\n      token: \"Confluence piekļuves tokens\",\n      token_explained_start:\n        \"Jums ir jānodrošina piekļuves tokens autentifikācijai. Jūs varat ģenerēt piekļuves tokenu\",\n      token_explained_link: \"šeit\",\n      token_desc: \"Piekļuves tokens autentifikācijai\",\n      pat_token: \"Confluence personiskais piekļuves tokens\",\n      pat_token_explained: \"Jūsu Confluence personiskais piekļuves tokens.\",\n      task_explained:\n        \"Kad tas būs pabeigts, lapas saturs būs pieejams iegulšanai darba vietās dokumentu atlasītājā.\",\n      bypass_ssl: \"Aizvest SSL sertifikāta validācijas\",\n      bypass_ssl_explained:\n        \"Aktivizējiet šo opciju, lai pārliecinajas no SSL sertifikāta validācijas, izmantojot pašizveidotā sertifikātu, konfluensā, kas ir pašizveidots.\",\n    },\n    manage: {\n      documents: \"Dokumenti\",\n      \"data-connectors\": \"Datu savienotāji\",\n      \"desktop-only\":\n        \"Šo iestatījumu rediģēšana ir pieejama tikai galddatora ierīcē. Lūdzu, piekļūstiet šai lapai savā galddatorā, lai turpinātu.\",\n      dismiss: \"Noraidīt\",\n      editing: \"Rediģēšana\",\n    },\n    directory: {\n      \"my-documents\": \"Mani dokumenti\",\n      \"new-folder\": \"Jauna mape\",\n      \"search-document\": \"Meklēt dokumentu\",\n      \"no-documents\": \"Nav dokumentu\",\n      \"move-workspace\": \"Pārvietot uz darba vietu\",\n      \"delete-confirmation\":\n        \"Vai tiešām vēlaties dzēst šos failus un mapes?\\nTas noņems failus no sistēmas un automātiski noņems tos no visām esošajām darba vietām.\\nŠī darbība nav atgriezeniska.\",\n      \"removing-message\":\n        \"Notiek {{count}} dokumentu un {{folderCount}} mapju noņemšana. Lūdzu, uzgaidiet.\",\n      \"move-success\": \"Veiksmīgi pārvietoti {{count}} dokumenti.\",\n      no_docs: \"Nav dokumentu\",\n      select_all: \"Atlasīt visu\",\n      deselect_all: \"Atcelt visu atlasi\",\n      remove_selected: \"Noņemt atlasītos\",\n      costs: \"*Vienreizējas izmaksas iegulšanai\",\n      save_embed: \"Saglabāt un iegult\",\n      \"total-documents_one\": \"{{count}} dokumenta\",\n      \"total-documents_other\": \"{{count}} dokumenti\",\n    },\n    upload: {\n      \"processor-offline\": \"Dokumentu apstrādātājs nav pieejams\",\n      \"processor-offline-desc\":\n        \"Mēs nevaram augšupielādēt jūsu failus, jo dokumentu apstrādātājs ir bezsaistē. Lūdzu, mēģiniet vēlāk.\",\n      \"click-upload\":\n        \"Noklikšķiniet, lai augšupielādētu, vai velciet un nometiet\",\n      \"file-types\":\n        \"atbalsta teksta failus, csv, izklājlapas, audio failus un vēl vairāk!\",\n      \"or-submit-link\": \"vai iesniedziet saiti\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"Iegūst...\",\n      \"fetch-website\": \"Iegūt vietni\",\n      \"privacy-notice\":\n        \"Šie faili tiks augšupielādēti dokumentu apstrādātājā, kas darbojas šajā AnythingLLM instancē. Šie faili netiek nosūtīti vai kopīgoti ar trešo pusi.\",\n    },\n    pinning: {\n      what_pinning: \"Kas ir dokumentu piespraušana?\",\n      pin_explained_block1:\n        \"Kad jūs <b>piespraudiet</b> dokumentu AnythingLLM, mēs ievietosim visu dokumenta saturu jūsu uzvednes logā, lai jūsu LLM to pilnībā saprastu.\",\n      pin_explained_block2:\n        \"Tas vislabāk darbojas ar <b>liela konteksta modeļiem</b> vai maziem failiem, kas ir kritiski tā zināšanu bāzei.\",\n      pin_explained_block3:\n        \"Ja jūs nesaņemat vēlamās atbildes no AnythingLLM pēc noklusējuma, tad piespraušana ir lielisks veids, kā iegūt kvalitatīvākas atbildes ar vienu klikšķi.\",\n      accept: \"Labi, sapratu\",\n    },\n    watching: {\n      what_watching: \"Ko dara dokumenta novērošana?\",\n      watch_explained_block1:\n        \"Kad jūs <b>novērojat</b> dokumentu AnythingLLM, mēs <i>automātiski</i> sinhronizēsim jūsu dokumenta saturu no tā sākotnējā avota regulāros intervālos. Tas automātiski atjauninās saturu katrā darba vietā, kur šis fails tiek pārvaldīts.\",\n      watch_explained_block2:\n        \"Šī funkcija pašlaik atbalsta tiešsaistes saturu un nebūs pieejama manuāli augšupielādētiem dokumentiem.\",\n      watch_explained_block3_start:\n        \"Jūs varat pārvaldīt, kuri dokumenti tiek novēroti no \",\n      watch_explained_block3_link: \"Failu pārvaldnieka\",\n      watch_explained_block3_end: \" administratora skata.\",\n      accept: \"Labi, sapratu\",\n    },\n  },\n  chat_window: {\n    send_message: \"Sūtīt ziņojumu\",\n    attach_file: \"Pievienot failu šim čatam\",\n    text_size: \"Mainīt teksta izmēru.\",\n    microphone: \"Izrunājiet savu uzvedni.\",\n    send: \"Nosūtīt uzvednes ziņojumu uz darba vietu\",\n    attachments_processing: \"Faili tiek apstrādāti. Lūdzu, paceliet.\",\n    tts_speak_message: \"TTS run message\",\n    copy: \"Kopēt\",\n    regenerate: \"Atjaunot\",\n    regenerate_response: \"Atjaunot atbildi\",\n    good_response: \"Laba atbilde\",\n    more_actions: \"Vairāk darbību\",\n    fork: \"Klūtis\",\n    delete: \"Dzēst\",\n    cancel: \"Atcelt\",\n    edit_prompt: \"Ieslēgt\",\n    edit_response: \"Rediģēt atbildi\",\n    preset_reset_description:\n      \"Izdzēsiet savu pastā veidoتو sarunu vēsturi un sāciet jaunu sarunu.\",\n    add_new_preset: \"Pievienot jaunu iepriekšējo\",\n    command: \"Ordere\",\n    your_command: \"Jūsu komanda\",\n    placeholder_prompt:\n      \"Šis ir saturs, kas tiks ievietots pirms jūsu pieprasījuma.\",\n    description: \"Apraksts\",\n    placeholder_description: \"Atbild ar dzeju par lielajiem valodu modeļiem.\",\n    save: \"Saglabāt\",\n    small: \"Mazs.\",\n    normal: \"Normāls\",\n    large: \"Liels\",\n    workspace_llm_manager: {\n      search: \"Izmeklē LLM sniedzējus\",\n      loading_workspace_settings: \"Ielāde darba vidējās iestatījumi...\",\n      available_models: \"Pieejamās modeļi: {{provider}}\",\n      available_models_description:\n        \"Izvēlieties modeli, ko izmantot šim darba zonai.\",\n      save: \"Izmantojiet šo modeli.\",\n      saving: \"Iestata modeli kā noklusēto darba vietai...\",\n      missing_credentials:\n        \"Šim pakalpojuma sniedzējam nav sniegta nekur dokumentēta informācija.\",\n      missing_credentials_description:\n        \"Noklikšķiniet, lai konfigurētu autentifikācijas datus\",\n    },\n    submit: \"Iesniegt\",\n    edit_info_user:\n      '\"Sūtīt\" atjauno AI atbildi. \"Saglabāt\" atjauno tikai jūsu ziņu.',\n    edit_info_assistant:\n      \"Jūsu izmaiņas tiks automātiski saglabātas šajā atbildē.\",\n    see_less: \"Skatīt mazāk\",\n    see_more: \"Skatīt vairāk\",\n    tools: \"Rīki\",\n    browse: \"Izpētiet\",\n    text_size_label: \"Teksta izmērs\",\n    select_model: \"Izvēlieties modeli\",\n    sources: \"Avotus\",\n    document: \"Dokuments\",\n    similarity_match: \"spēle\",\n    source_count_one: \"{{count}} – atsauce\",\n    source_count_other: \"Atsauces uz {{count}}\",\n    preset_exit_description: \"Aizust klientu sesiju\",\n    add_new: \"Pievienot jaunu\",\n    edit: \"Rediģēt\",\n    publish: \"Publicēt\",\n    stop_generating: \"Atsauciet atbildes ģenerēšanu\",\n    pause_tts_speech_message:\n      \"Pārtrauciet TTS (teksta-izrunas) žēstā vēstījuma izrunu.\",\n    slash_commands: \"Īs termini komandās\",\n    agent_skills: \"Aģenta prasmes\",\n    manage_agent_skills: \"Iesaista aģenta prasmes\",\n    agent_skills_disabled_in_session:\n      \"Nav iespējams mainīt prasmes aktīvā lietotāja sesijā. Pirmais, jāizmanto komandu `/exit`, lai beigtu sesiju.\",\n    start_agent_session: \"Sākt aģenta sesiju\",\n    use_agent_session_to_use_tools:\n      'Jūs varat izmantot rīkus čatā, sākot aģenta sesiju, ievietojot \"@agent\" jūsu iniciālajā tekstā.',\n  },\n  profile_settings: {\n    edit_account: \"Rediģēt kontu\",\n    profile_picture: \"Profila attēls\",\n    remove_profile_picture: \"Noņemt profila attēlu\",\n    username: \"Lietotājvārds\",\n    new_password: \"Jauna parole\",\n    password_description: \"Parolei jābūt vismaz 8 rakstzīmes garai\",\n    cancel: \"Atcelt\",\n    update_account: \"Atjaunināt kontu\",\n    theme: \"Tēmas preference\",\n    language: \"Vēlamā valoda\",\n    failed_upload: \"Neizdevās augsēt profilas attēlu: {{error}}\",\n    upload_success: \"Profila attēls ir augšupielādēts.\",\n    failed_remove: \"Neizdevās noņemt profilbildi: {{error}}\",\n    profile_updated: \"Profils atjaunināts.\",\n    failed_update_user: \"Neizdevās atjaunināt lietotāju: {{error}}\",\n    account: \"Konta\",\n    support: \"Atbalsts\",\n    signout: \"Iziet\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Taustiņu atvieglojumi\",\n    shortcuts: {\n      settings: \"Atvērt iestatījumus\",\n      workspaceSettings: \"Atvērt pašreizējās darba vides iestatījumus\",\n      home: \"Pārvietojieties uz sākuma lapu\",\n      workspaces: \"Administrējiet darba vietas\",\n      apiKeys: \"API atslēgas – iestatījumi\",\n      llmPreferences: \"LLM prioritātes\",\n      chatSettings: \"Pieskaites iestatījumi\",\n      help: \"Rādīt tastatūras atvērto palīdzības\",\n      showLLMSelector: \"LLM izvēles rīks\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Veiksmi!\",\n        success_description:\n          'Jūsu sistēmas iniciatīva ir publicēta \"Community Hub\" platformā!',\n        success_thank_you: \"Paldies par dalīšanos ar komunitāti!\",\n        view_on_hub: \"Skatīt Community Hub\",\n        modal_title: \"Publicēšanas sistēmas iniciatīva\",\n        name_label: \"Jānis\",\n        name_description: \"Šis ir jūsu sistēmas komandas nosaukums.\",\n        name_placeholder: \"Mana sistēmas iniciatīva\",\n        description_label: \"Apraksts\",\n        description_description:\n          \"Šis ir jūsu sistēmas iniciatīvas apraksts. Izmantojiet to, lai aprakstītu jūsu sistēmas iniciatīvas mērķi.\",\n        tags_label: \"Atzīmes\",\n        tags_description:\n          \"Atzīmes tiek izmantotas, lai atzīmētu jūsu sistēmas iniciatīvu, lai to vieglāk atrastu. Jūs varat pievienot vairākas atzīmes. Maks 5 atzīmes. Katrai atzīmei – maksimāli 20 raksti.\",\n        tags_placeholder:\n          'Ievietojiet tekstu un nospiediet \"Enter\", lai pievienotu atzīmes',\n        visibility_label: \"Redzamība\",\n        public_description: \"Vispārējās sistēmas aicinājumi ir redzami visiem.\",\n        private_description:\n          \"Privātā sistēmas paziņojumi ir redzami tikai jums.\",\n        publish_button: \"Publicē savu saturu Community Hub.\",\n        submitting: \"Izdevniecība...\",\n        prompt_label: \"Ieslēgt\",\n        prompt_description:\n          \"Šis ir tiešais sistēmas prompts, kas tiks izmantots, lai vadītu LLM.\",\n        prompt_placeholder: \"Ievietojiet savu sistēmas komandu šeit...\",\n      },\n      agent_flow: {\n        success_title: \"Veiksmi!\",\n        success_description:\n          'Jūsu \"Agent Flow\" ir publicēts \"Community Hub\" platformā!',\n        success_thank_you: \"Paldies par dalīšanos ar kopienu!\",\n        view_on_hub: \"Skatīt Community Hub\",\n        modal_title: \"Publicēšanas aģenta darbības\",\n        name_label: \"Jānis\",\n        name_description: \"Šis ir jūsu aģenta darbības norises nosaukums.\",\n        name_placeholder: \"Mana aģenta darbība\",\n        description_label: \"Apraksts\",\n        description_description:\n          \"Šis ir jūsu aģenta darbības apraksts. Izmantojiet to, lai aprakstītu jūsu aģenta darbības mērķi.\",\n        tags_label: \"Atzīmes\",\n        tags_description:\n          \"Atzīmes tiek izmantotas, lai atzīmētu jūsu aģenta darbplūsmu, lai to būtu vieglāk atrast. Jūs varat pievienot vairākas atzīmes. Maks 5 atzīmes. Katrai atzīmei – maksimāli 20 raksti.\",\n        tags_placeholder:\n          'Ievietojiet tekstu un nospiediet \"Enter\", lai pievienotu atzīmes',\n        visibility_label: \"Redzamība\",\n        submitting: \"Izdevniecība...\",\n        submit: \"Publicē savu saturu Community Hub.\",\n        privacy_note:\n          'Dati tiek augšupielādēti kā privāti, lai aizsargātu jebkādus citus datus. Pēc publicēšanas varat mainīt redzamības iestatījumus \"Sabiedrības centrā\". Lūdzu, pārliecinieties, ka jūsu dati nesatur nevienu citu vai privātu informāciju, pirms publicēšanas.',\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Nepieciešama autentifikācija\",\n          description:\n            'Pirms satura publicēšanas ir jāiespējo autentifikācija \"AnythingLLM\" sabiedrības centrā.',\n          button: \"Pievienojieties sabiedrības centram\",\n        },\n      },\n      slash_command: {\n        success_title: \"Veiksmi!\",\n        success_description:\n          'Jūsu \"Slash Command\" ir publicēts \"Community Hub\"!',\n        success_thank_you: \"Paldies par dalīšanos ar kopienu!\",\n        view_on_hub: \"Skatīt Community Hub\",\n        modal_title: \"Publicējiet Slash komandu\",\n        name_label: \"Jānis\",\n        name_description: \"Šis ir jūsu komandas nosaukums.\",\n        name_placeholder: \"Mana Slash komanda\",\n        description_label: \"Apraksts\",\n        description_description:\n          \"Šis ir jūsu komandas apraksts. Izmantojiet to, lai aprakstītu jūsu komandas mērķi.\",\n        tags_label: \"Atzīmes\",\n        tags_description:\n          \"Atzīmes tiek izmantotas, lai atzīmētu jūsu komandu, kas ļauj vieglāk meklēt. Jūs varat pievienot vairākas atzīmes. Maks 5 atzīmes. Katrai atzīmei – maksimāli 20 raksti.\",\n        tags_placeholder:\n          \"Ierakstiet un nospiediet Enter, lai pievienotu atzīmes\",\n        visibility_label: \"Redzamība\",\n        public_description: \"Vispārīgie komandas vārdi ir redzami visiem.\",\n        private_description: \"Vietiski komandu komandās var redzēt tikai jūs.\",\n        publish_button: \"Publicē savu saturu Community Hub.\",\n        submitting: \"Izdevniecība...\",\n        prompt_label: \"Ieslēgt\",\n        prompt_description:\n          \"Šis ir komandu, kas tiks izmantots, kad tiks aktivizēta slashes komanda.\",\n        prompt_placeholder: \"Ievietojiet savu pieprasījumu šeit...\",\n      },\n    },\n  },\n  security: {\n    title: \"Drošība\",\n    multiuser: {\n      title: \"Vairāklietotāju režīms\",\n      description:\n        \"Iestatiet savu instanci, lai atbalstītu jūsu komandu, aktivizējot vairāklietotāju režīmu.\",\n      enable: {\n        \"is-enable\": \"Vairāklietotāju režīms ir iespējots\",\n        enable: \"Iespējot vairāklietotāju režīmu\",\n        description:\n          \"Pēc noklusējuma jūs būsiet vienīgais administrators. Kā administrators jums būs jāizveido konti visiem jaunajiem lietotājiem vai administratoriem. Nezaudējiet savu paroli, jo tikai administratora lietotājs var atiestatīt paroles.\",\n        username: \"Administratora konta lietotājvārds\",\n        password: \"Administratora konta parole\",\n      },\n    },\n    password: {\n      title: \"Aizsardzība ar paroli\",\n      description:\n        \"Aizsargājiet savu AnythingLLM instanci ar paroli. Ja aizmirsīsiet šo paroli, nav atgūšanas metodes, tāpēc pārliecinieties, ka saglabājat šo paroli.\",\n      \"password-label\": \"Instances paroles\",\n    },\n  },\n  home: {\n    welcome: \"Laipni lūgti\",\n    chooseWorkspace: \"Izvēlies darba vietu, lai sāktu čatu!\",\n    notAssigned:\n      \"Jūs nav piešķirts nevienai darba vietai.\\nLūdzu, sazinieties ar savu administratoru, lai pieprasītu piekļuvi darba vietai.\",\n    goToWorkspace: 'Pāriet uz darba vietu \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/nl/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    survey: {\n      email: \"Wat is je e-mailadres?\",\n      useCase: \"Waarvoor ga je AnythingLLM gebruiken?\",\n      useCaseWork: \"Voor werk\",\n      useCasePersonal: \"Voor persoonlijk gebruik\",\n      useCaseOther: \"Anders\",\n      comment: \"Hoe heb je over AnythingLLM gehoord?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube, enz. - Laat ons weten hoe je ons gevonden hebt!\",\n      skip: \"Enquête overslaan\",\n      thankYou: \"Bedankt voor je feedback!\",\n      title: \"Welkom bij AnythingLLM\",\n      description:\n        \"Help ons AnythingLLM af te stemmen op jouw behoeften. (Optioneel)\",\n    },\n    home: {\n      title: \"Welkom bij\",\n      getStarted: \"Aan de slag\",\n    },\n    llm: {\n      title: \"LLM-voorkeuren\",\n      description:\n        \"AnythingLLM kan samenwerken met veel LLM-aanbieders. Deze service verzorgt de chatfunctie.\",\n    },\n    userSetup: {\n      title: \"Gebruikersinstellingen\",\n      description: \"Configureer uw gebruikersinstellingen.\",\n      howManyUsers: \"Hoeveel gebruikers zullen deze instantie gebruiken?\",\n      justMe: \"Alleen ik\",\n      myTeam: \"Mijn team\",\n      instancePassword: \"Instancewachtwoord\",\n      setPassword: \"Wilt u een wachtwoord instellen?\",\n      passwordReq: \"Wachtwoorden moeten minimaal 8 tekens lang zijn.\",\n      passwordWarn:\n        \"Het is belangrijk om dit wachtwoord te bewaren, omdat er geen herstelmethode is.\",\n      adminUsername: \"Gebruikersnaam van het beheerdersaccount\",\n      adminPassword: \"Wachtwoord van het beheerdersaccount\",\n      adminPasswordReq: \"Wachtwoorden moeten minimaal 8 tekens lang zijn.\",\n      teamHint:\n        \"Standaard bent u de enige beheerder. Zodra de onboarding is voltooid, kunt u gebruikers of beheerders aanmaken en anderen uitnodigen. Raak uw wachtwoord niet kwijt, want alleen beheerders kunnen wachtwoorden opnieuw instellen.\",\n    },\n    data: {\n      title: \"Gegevensverwerking en privacy\",\n      description:\n        \"Wij streven naar transparantie en controle als het gaat om uw persoonlijke gegevens.\",\n      settingsHint:\n        \"Deze instellingen kunnen op elk moment opnieuw worden geconfigureerd in de instellingen.\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Werkruimten Naam\",\n    user: \"Gebruiker\",\n    selection: \"Model Selectie\",\n    saving: \"Opslaan...\",\n    save: \"Wijzigingen opslaan\",\n    previous: \"Vorige pagina\",\n    next: \"Volgende pagina\",\n    optional: \"Optioneel\",\n    yes: \"Ja\",\n    no: \"Nee\",\n    search: \"Zoeken\",\n    username_requirements:\n      \"De gebruikersnaam moet 2-32 tekens bevatten, beginnen met een kleine letter en mag alleen kleine letters, cijfers, underscores, koppeltekens en punten bevatten.\",\n    on: \"Over\",\n    none: \"Geen\",\n    stopped: \"Gestopt\",\n    loading: \"Laad\",\n    refresh: \"Verfrissen\",\n  },\n  settings: {\n    title: \"Instelling Instanties\",\n    invites: \"Uitnodigingen\",\n    users: \"Gebruikers\",\n    workspaces: \"Werkruimten\",\n    \"workspace-chats\": \"Werkruimte Chats\",\n    customization: \"Aanpassing\",\n    \"api-keys\": \"Ontwikkelaar API\",\n    llm: \"LLM\",\n    transcription: \"Transcriptie\",\n    embedder: \"Inbedder\",\n    \"text-splitting\": \"Tekst Splitsen & Chunking\",\n    \"voice-speech\": \"Stem & Spraak\",\n    \"vector-database\": \"Vector Database\",\n    embeds: \"Chat Inbedden\",\n    security: \"Veiligheid\",\n    \"event-logs\": \"Gebeurtenislogboeken\",\n    privacy: \"Privacy & Gegevens\",\n    \"ai-providers\": \"AI Providers\",\n    \"agent-skills\": \"Agent Vaardigheden\",\n    admin: \"Beheerder\",\n    tools: \"Hulpmiddelen\",\n    \"experimental-features\": \"Experimentele Functies\",\n    contact: \"Contact Ondersteuning\",\n    \"browser-extension\": \"Browser Extensie\",\n    \"system-prompt-variables\": \"Systeempromptvariabelen\",\n    interface: \"UI-voorkeuren\",\n    branding: \"Branding & Whitelabeling\",\n    chat: \"Chat\",\n    \"mobile-app\": \"AnythingLLM Mobiele App\",\n    \"community-hub\": {\n      title: \"Centraal punt\",\n      trending: \"Bekijk populaire onderwerpen\",\n      \"your-account\": \"Uw account\",\n      \"import-item\": \"Importeren\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Welkom bij\",\n      \"placeholder-username\": \"Gebruikersnaam\",\n      \"placeholder-password\": \"Wachtwoord\",\n      login: \"Inloggen\",\n      validating: \"Bezig met valideren...\",\n      \"forgot-pass\": \"Wachtwoord vergeten\",\n      reset: \"Reset\",\n    },\n    \"sign-in\": \"Meld je aan bij je {{appName}} account.\",\n    \"password-reset\": {\n      title: \"Wachtwoord Resetten\",\n      description:\n        \"Geef de benodigde informatie hieronder om je wachtwoord te resetten.\",\n      \"recovery-codes\": \"Herstelcodes\",\n      \"back-to-login\": \"Terug naar Inloggen\",\n    },\n  },\n  \"new-workspace\": {\n    title: \"Nieuwe Werkruimte\",\n    placeholder: \"Mijn Werkruimte\",\n  },\n  \"workspaces—settings\": {\n    general: \"Algemene Instellingen\",\n    chat: \"Chat Instellingen\",\n    vector: \"Vector Database\",\n    members: \"Leden\",\n    agent: \"Agent Configuratie\",\n  },\n  general: {\n    vector: {\n      title: \"Vector Teller\",\n      description: \"Totaal aantal vectoren in je vector database.\",\n    },\n    names: {\n      description: \"Dit zal alleen de weergavenaam van je werkruimte wijzigen.\",\n    },\n    message: {\n      title: \"Voorgestelde Chatberichten\",\n      description:\n        \"Pas de berichten aan die aan je werkruimtegebruikers worden voorgesteld.\",\n      add: \"Nieuw bericht toevoegen\",\n      save: \"Berichten opslaan\",\n      heading: \"Leg me uit\",\n      body: \"de voordelen van AnythingLLM\",\n    },\n    delete: {\n      title: \"Werkruimte Verwijderen\",\n      description:\n        \"Verwijder deze werkruimte en al zijn gegevens. Dit zal de werkruimte voor alle gebruikers verwijderen.\",\n      delete: \"Werkruimte Verwijderen\",\n      deleting: \"Werkruimte Verwijderen...\",\n      \"confirm-start\": \"Je staat op het punt je gehele\",\n      \"confirm-end\":\n        \"werkruimte te verwijderen. Dit zal alle vector inbeddingen in je vector database verwijderen.\\n\\nDe originele bronbestanden blijven onaangetast. Deze actie is onomkeerbaar.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Werkruimte LLM Provider\",\n      description:\n        \"De specifieke LLM-provider en -model die voor deze werkruimte zal worden gebruikt. Standaard wordt de systeem LLM-provider en instellingen gebruikt.\",\n      search: \"Zoek alle LLM-providers\",\n    },\n    model: {\n      title: \"Werkruimte Chatmodel\",\n      description:\n        \"Het specifieke chatmodel dat voor deze werkruimte zal worden gebruikt. Indien leeg, wordt de systeem LLM-voorkeur gebruikt.\",\n    },\n    mode: {\n      title: \"Chatmodus\",\n      chat: {\n        title: \"Chat\",\n        description:\n          \"zal antwoorden geven met de algemene kennis van het LLM en de relevante context uit het document. U moet het `@agent`-commando gebruiken om tools te gebruiken.\",\n      },\n      query: {\n        title: \"Query\",\n        description:\n          \"zal antwoorden <b>alleen</b> geven, indien de context van het document wordt gevonden.<br />U moet het commando @agent gebruiken om tools te gebruiken.\",\n      },\n      automatic: {\n        title: \"Auto\",\n        description:\n          \"zal automatisch tools gebruiken als het model en de provider native tool-aanroepen ondersteunen.<br />Als native tooling niet wordt ondersteund, moet u het `@agent`-commando gebruiken om tools te gebruiken.\",\n      },\n    },\n    history: {\n      title: \"Chatgeschiedenis\",\n      \"desc-start\":\n        \"Het aantal vorige chats dat in het kortetermijngeheugen van de reactie wordt opgenomen.\",\n      recommend: \"Aanbevolen 20. \",\n      \"desc-end\":\n        \"Alles meer dan 45 leidt waarschijnlijk tot continue chatfouten, afhankelijk van de berichtgrootte.\",\n    },\n    prompt: {\n      title: \"Prompt\",\n      description:\n        \"De prompt die in deze werkruimte zal worden gebruikt. Definieer de context en instructies voor de AI om een reactie te genereren. Je moet een zorgvuldig samengestelde prompt geven zodat de AI een relevante en nauwkeurige reactie kan genereren.\",\n      history: {\n        title: \"Geschiedenis van systeemprompts\",\n        clearAll: \"Alles wissen\",\n        noHistory: \"Geen geschiedenis van systeemprompts beschikbaar\",\n        restore: \"Herstellen\",\n        delete: \"Verwijderen\",\n        deleteConfirm:\n          \"Weet u zeker dat u dit geschiedenisitem wilt verwijderen?\",\n        clearAllConfirm:\n          \"Weet u zeker dat u alle geschiedenis wilt wissen? Deze actie kan niet ongedaan worden gemaakt.\",\n        expand: \"Uitbreiden\",\n        publish: \"Publiceren naar Community Hub\",\n      },\n    },\n    refusal: {\n      title: \"Afwijzingsreactie in Querymodus\",\n      \"desc-start\": \"Wanneer in\",\n      query: \"query\",\n      \"desc-end\":\n        \"modus, wil je wellicht een aangepaste afwijzingsreactie geven wanneer er geen context wordt gevonden.\",\n      \"tooltip-title\": \"Waarom zie ik dit?\",\n      \"tooltip-description\":\n        \"U bevindt zich in de querymodus, die alleen informatie uit uw documenten gebruikt. Schakel over naar de chatmodus voor flexibelere gesprekken, of klik hier om onze documentatie te raadplegen voor meer informatie over chatmodi.\",\n    },\n    temperature: {\n      title: \"LLM Temperatuur\",\n      \"desc-start\":\n        'Deze instelling bepaalt hoe \"creatief\" je LLM-antwoorden zullen zijn.',\n      \"desc-end\":\n        \"Hoe hoger het getal, hoe creatiever. Voor sommige modellen kan dit leiden tot onsamenhangende antwoorden als het te hoog wordt ingesteld.\",\n      hint: \"De meeste LLM's hebben verschillende acceptabele reeksen van geldige waarden. Raadpleeg je LLM-provider voor die informatie.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Vector database-identificator\",\n    snippets: {\n      title: \"Maximale Contextfragmenten\",\n      description:\n        \"Deze instelling bepaalt het maximale aantal contextfragmenten dat per chat of query naar de LLM wordt verzonden.\",\n      recommend: \"Aanbevolen: 4\",\n    },\n    doc: {\n      title: \"Document gelijkenisdrempel\",\n      description:\n        \"De minimale gelijkenisscore die vereist is voor een bron om als gerelateerd aan de chat te worden beschouwd. Hoe hoger het getal, hoe meer vergelijkbaar de bron moet zijn met de chat.\",\n      zero: \"Geen beperking\",\n      low: \"Laag (gelijkenisscore ≥ .25)\",\n      medium: \"Middel (gelijkenisscore ≥ .50)\",\n      high: \"Hoog (gelijkenisscore ≥ .75)\",\n    },\n    reset: {\n      reset: \"Vector Database Resetten\",\n      resetting: \"Vectoren wissen...\",\n      confirm:\n        \"Je staat op het punt de vector database van deze werkruimte te resetten. Dit zal alle momenteel ingebedde vectoren verwijderen.\\n\\nDe originele bronbestanden blijven onaangetast. Deze actie is onomkeerbaar.\",\n      error: \"Werkruimte vector database kon niet worden gereset!\",\n      success: \"Werkruimte vector database is gereset!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"De prestaties van LLM's die geen tool-aanroep expliciet ondersteunen, zijn sterk afhankelijk van de capaciteiten en nauwkeurigheid van het model. Sommige vaardigheden kunnen beperkt of niet-functioneel zijn.\",\n    provider: {\n      title: \"Werkruimte Agent LLM Provider\",\n      description:\n        \"De specifieke LLM-provider en -model die voor het @agent-agent van deze werkruimte zal worden gebruikt.\",\n    },\n    mode: {\n      chat: {\n        title: \"Werkruimte Agent Chatmodel\",\n        description:\n          \"Het specifieke chatmodel dat zal worden gebruikt voor het @agent-agent van deze werkruimte.\",\n      },\n      title: \"Werkruimte Agentmodel\",\n      description:\n        \"Het specifieke LLM-model dat voor het @agent-agent van deze werkruimte zal worden gebruikt.\",\n      wait: \"-- wachten op modellen --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG & langetermijngeheugen\",\n        description:\n          'Sta de agent toe om je lokale documenten te gebruiken om een vraag te beantwoorden of vraag de agent om stukken inhoud \"te onthouden\" voor langetermijngeheugenopslag.',\n      },\n      view: {\n        title: \"Documenten bekijken & samenvatten\",\n        description:\n          \"Sta de agent toe om de inhoud van momenteel ingebedde werkruimtebestanden op te sommen en samen te vatten.\",\n      },\n      scrape: {\n        title: \"Websites schrapen\",\n        description:\n          \"Sta de agent toe om de inhoud van websites te bezoeken en te schrapen.\",\n      },\n      generate: {\n        title: \"Grafieken genereren\",\n        description:\n          \"Sta de standaardagent toe om verschillende soorten grafieken te genereren uit verstrekte of in de chat gegeven gegevens.\",\n      },\n      save: {\n        title: \"Genereren & opslaan van bestanden naar browser\",\n        description:\n          \"Sta de standaardagent toe om te genereren en te schrijven naar bestanden die worden opgeslagen en kunnen worden gedownload in je browser.\",\n      },\n      web: {\n        title: \"Live web zoeken en browsen\",\n        description:\n          \"Maak het mogelijk voor uw agent om het internet te doorzoeken om uw vragen te beantwoorden, door een verbinding te maken met een webzoekprovider (SERP).\",\n      },\n      sql: {\n        title: \"SQL-connector\",\n        description:\n          \"Maak het mogelijk voor uw agent om SQL te gebruiken om uw vragen te beantwoorden, door verbinding te maken met verschillende SQL-databaseproviders.\",\n      },\n      default_skill:\n        \"Standaard is deze functie ingeschakeld, maar u kunt deze uitschakelen als u niet wilt dat de agent er gebruik van kan maken.\",\n    },\n    mcp: {\n      title: \"MCP-servers\",\n      \"loading-from-config\": \"MCP-servers laden vanuit een configuratiebestand\",\n      \"learn-more\": \"Meer informatie over MCP-servers.\",\n      \"no-servers-found\": \"Geen MCP-servers gevonden.\",\n      \"tool-warning\":\n        \"Om de beste prestaties te garanderen, overweeg dan om ongewenste tools uit te schakelen om de context te behouden.\",\n      \"stop-server\": \"Stoppen met de MCP-server\",\n      \"start-server\": \"Start de MCP-server\",\n      \"delete-server\": \"Verwijder de MCP-server\",\n      \"tool-count-warning\":\n        \"Deze MCP-server heeft <b> bepaalde tools ingeschakeld</b> die context gebruiken in elke chat. <br /> Overweeg om ongewenste tools uit te schakelen om context te besparen.\",\n      \"startup-command\": \"Startcommando\",\n      command: \"Instructie\",\n      arguments: \"Argumenten\",\n      \"not-running-warning\":\n        \"Deze MCP-server is niet actief – deze kan zijn uitgeschakeld of een fout ervaren tijdens het opstarten.\",\n      \"tool-call-arguments\": \"Argumenten voor het aanroepen van een tool\",\n      \"tools-enabled\": \"hulpmiddelen zijn geactiveerd\",\n    },\n    settings: {\n      title: \"Instellingen voor vaardigheden van agenten\",\n      \"max-tool-calls\": {\n        title: \"Maximaal aantal tool-aanroepen per antwoord\",\n        description:\n          \"Het maximale aantal tools dat een agent kan gebruiken om een enkele reactie te genereren. Dit voorkomt dat tools onbeperkt worden aangeroepen en dat er oneindige loops ontstaan.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Intelligente selectie van vaardigheden\",\n        \"beta-badge\": \"Betaling\",\n        description:\n          'Maak gebruik van een onbeperkt aantal tools en verminder het gebruik van \"cut tokens\" met tot wel 80% per query – AnythingLLM selecteert automatisch de juiste vaardigheden voor elke vraag.',\n        \"max-tools\": {\n          title: \"Max Tools\",\n          description:\n            \"Het maximale aantal tools dat kan worden geselecteerd voor elke query. Wij raden aan om deze waarde hoger in te stellen voor modellen met een grotere context.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Werkruimte Chats\",\n    description:\n      \"Dit zijn alle opgenomen chats en berichten die door gebruikers zijn verzonden, gerangschikt op hun aanmaakdatum.\",\n    export: \"Exporteren\",\n    table: {\n      id: \"Id\",\n      by: \"Verzonden Door\",\n      workspace: \"Werkruimte\",\n      prompt: \"Prompt\",\n      response: \"Response\",\n      at: \"Verzonden Om\",\n    },\n  },\n  api: {\n    title: \"API-sleutels\",\n    description:\n      \"API-sleutels stellen de houder in staat om deze AnythingLLM-instantie programmatisch te openen en beheren.\",\n    link: \"Lees de API-documentatie\",\n    generate: \"Genereer Nieuwe API-sleutel\",\n    table: {\n      key: \"API-sleutel\",\n      by: \"Aangemaakt Door\",\n      created: \"Aangemaakt\",\n    },\n  },\n  llm: {\n    title: \"LLM Voorkeur\",\n    description:\n      \"Dit zijn de inloggegevens en instellingen voor je voorkeurs LLM-chat & inbeddingprovider. Het is belangrijk dat deze sleutels actueel en correct zijn, anders zal AnythingLLM niet goed werken.\",\n    provider: \"LLM Provider\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Azure Service Endpoint\",\n        api_key: \"API Key\",\n        chat_deployment_name: \"Chat Deployment Naam\",\n        chat_model_token_limit: \"Chat Model Token Limiet\",\n        model_type: \"Model Type\",\n        default: \"Standaard\",\n        reasoning: \"Redeneren\",\n        model_type_tooltip:\n          \"Als uw implementatie een redeneermodel gebruikt (o1, o1-mini, o3-mini, enz.), stel dit dan in op 'Redeneren'. Anders kunnen uw chatverzoeken mislukken.\",\n      },\n    },\n  },\n  transcription: {\n    title: \"Transcriptiemodel Voorkeur\",\n    description:\n      \"Dit zijn de inloggegevens en instellingen voor je voorkeurs transcriptiemodelprovider. Het is belangrijk dat deze sleutels actueel en correct zijn, anders worden media en audio niet getranscribeerd.\",\n    provider: \"Transcriptieprovider\",\n    \"warn-start\":\n      \"Het gebruik van het lokale fluistermodel op machines met beperkte RAM of CPU kan AnythingLLM vertragen bij het verwerken van mediabestanden.\",\n    \"warn-recommend\":\n      \"We raden minstens 2GB RAM aan en upload bestanden <10Mb.\",\n    \"warn-end\":\n      \"Het ingebouwde model wordt automatisch gedownload bij het eerste gebruik.\",\n  },\n  embedding: {\n    title: \"Inbedding Voorkeur\",\n    \"desc-start\":\n      \"Bij het gebruik van een LLM die geen ingebouwde ondersteuning voor een inbeddingengine heeft, moet je mogelijk aanvullende inloggegevens opgeven voor het inbedden van tekst.\",\n    \"desc-end\":\n      \"Inbedding is het proces van het omzetten van tekst in vectoren. Deze inloggegevens zijn vereist om je bestanden en prompts om te zetten naar een formaat dat AnythingLLM kan gebruiken om te verwerken.\",\n    provider: {\n      title: \"Inbedding Provider\",\n    },\n  },\n  text: {\n    title: \"Tekst Splitsen & Chunking Voorkeuren\",\n    \"desc-start\":\n      \"Soms wil je misschien de standaard manier wijzigen waarop nieuwe documenten worden gesplitst en gechunkt voordat ze in je vector database worden ingevoerd.\",\n    \"desc-end\":\n      \"Je moet deze instelling alleen wijzigen als je begrijpt hoe tekstsplitsing werkt en de bijbehorende effecten.\",\n    size: {\n      title: \"Tekst Chunk Grootte\",\n      description:\n        \"Dit is de maximale lengte van tekens die aanwezig kan zijn in een enkele vector.\",\n      recommend: \"Inbed model maximale lengte is\",\n    },\n    overlap: {\n      title: \"Tekst Chunk Overlap\",\n      description:\n        \"Dit is de maximale overlap van tekens die optreedt tijdens het chunking tussen twee aangrenzende tekstchunks.\",\n    },\n  },\n  vector: {\n    title: \"Vector Database\",\n    description:\n      \"Dit zijn de inloggegevens en instellingen voor hoe je AnythingLLM-instantie zal functioneren. Het is belangrijk dat deze sleutels actueel en correct zijn.\",\n    provider: {\n      title: \"Vector Database Provider\",\n      description: \"Er is geen configuratie nodig voor LanceDB.\",\n    },\n  },\n  embeddable: {\n    title: \"Inbedbare Chat Widgets\",\n    description:\n      \"Inbedbare chatwidgets zijn openbare chatinterfaces die zijn gekoppeld aan een enkele werkruimte. Hiermee kun je werkruimten bouwen die je vervolgens kunt publiceren naar de wereld.\",\n    create: \"Maak inbedding\",\n    table: {\n      workspace: \"Werkruimte\",\n      chats: \"Verzonden Chats\",\n      active: \"Actieve Domeinen\",\n      created: \"Aangemaakt\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Inbedding Chats\",\n    export: \"Exporteren\",\n    description:\n      \"Dit zijn alle opgenomen chats en berichten van elke inbedding die je hebt gepubliceerd.\",\n    table: {\n      embed: \"Inbedding\",\n      sender: \"Afzender\",\n      message: \"Bericht\",\n      response: \"Reactie\",\n      at: \"Verzonden Om\",\n    },\n  },\n  event: {\n    title: \"Gebeurtenislogboeken\",\n    description:\n      \"Bekijk alle acties en gebeurtenissen die op deze instantie plaatsvinden voor monitoring.\",\n    clear: \"Gebeurtenislogboeken Wissen\",\n    table: {\n      type: \"Gebeurtenistype\",\n      user: \"Gebruiker\",\n      occurred: \"Opgetreden Op\",\n    },\n  },\n  privacy: {\n    title: \"Privacy & Gegevensverwerking\",\n    description:\n      \"Dit is je configuratie voor hoe verbonden derden en AnythingLLM je gegevens verwerken.\",\n    anonymous: \"Anonieme Telemetrie Ingeschakeld\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Zoek naar data-connectoren\",\n    \"no-connectors\": \"Geen data-connectoren gevonden.\",\n    github: {\n      name: \"GitHub-repository\",\n      description:\n        \"Importeer een volledige openbare of privé GitHub-repository met één klik.\",\n      URL: \"URL van de GitHub-repository\",\n      URL_explained: \"URL van de GitHub-repository die u wilt verzamelen.\",\n      token: \"GitHub-toegangstoken\",\n      optional: \"optioneel\",\n      token_explained: \"Toegangstoken om rate limiting te voorkomen.\",\n      token_explained_start: \"Zonder een \",\n      token_explained_link1: \"Persoonlijk toegangstoken\",\n      token_explained_middle:\n        \", kan de GitHub API het aantal bestanden dat kan worden verzameld beperken vanwege rate limiting. U kunt \",\n      token_explained_link2: \"een tijdelijk toegangstoken aanmaken\",\n      token_explained_end: \" om dit probleem te voorkomen.\",\n      ignores: \"Bestanden die genegeerd worden\",\n      git_ignore:\n        \"Lijst in .gitignore-indeling om specifieke bestanden te negeren tijdens het verzamelen. Druk op Enter na elke vermelding die u wilt opslaan.\",\n      task_explained:\n        \"Zodra de taak is voltooid, zijn alle bestanden beschikbaar om in te sluiten in werkruimtes in de documentkiezer.\",\n      branch: \"De branch waarvan u bestanden wilt verzamelen.\",\n      branch_loading: \"-- beschikbare branches laden --\",\n      branch_explained: \"De branch waarvan u bestanden wilt verzamelen.\",\n      token_information:\n        \"Zonder het invullen van het <b>GitHub-toegangstoken</b> kan deze dataconnector alleen de <b>top-level</b> bestanden van de repository verzamelen vanwege de limieten voor het aantal aanvragen via de openbare API van GitHub.\",\n      token_personal:\n        \"Vraag hier een gratis persoonlijk toegangstoken aan met een GitHub-account.\",\n    },\n    gitlab: {\n      name: \"GitLab-repository\",\n      description:\n        \"Importeer een volledige openbare of privé GitLab-repository met één klik.\",\n      URL: \"URL van de GitLab-repository\",\n      URL_explained: \"URL van de GitLab-repository die u wilt verzamelen.\",\n      token: \"GitLab-toegangstoken\",\n      optional: \"optioneel\",\n      token_description:\n        \"Selecteer extra entiteiten om op te halen via de GitLab API.\",\n      token_explained_start: \"Zonder een \",\n      token_explained_link1: \"Persoonlijk toegangstoken\",\n      token_explained_middle:\n        \", kan de GitLab API het aantal bestanden dat kan worden verzameld beperken vanwege rate limiting. U kunt \",\n      token_explained_link2: \"een tijdelijk toegangstoken aanmaken\",\n      token_explained_end: \" om dit probleem te voorkomen.\",\n      fetch_issues: \"Problemen ophalen als documenten\",\n      ignores: \"Bestanden negeren\",\n      git_ignore:\n        \"Lijst in  .gitignore-formaat om specifieke bestanden te negeren tijdens het verzamelen. Druk op Enter na elke vermelding die u wilt opslaan.\",\n      task_explained:\n        \"Zodra de taak is voltooid, zijn alle bestanden beschikbaar om in te sluiten in werkruimtes in de documentkiezer.\",\n      branch: \"Branch waarvan u bestanden wilt verzamelen\",\n      branch_loading: \"-- beschikbare branches laden --\",\n      branch_explained: \"Branch waarvan u bestanden wilt verzamelen.\",\n      token_information:\n        \"Zonder het invullen van het <b>GitLab-toegangstoken</b> kan deze dataconnector alleen de <b>top-level</b> bestanden van de repository verzamelen vanwege de limieten voor het aantal aanvragen via de openbare GitLab API.\",\n      token_personal:\n        \"Vraag hier een gratis persoonlijk toegangstoken aan met een GitLab-account.\",\n    },\n    youtube: {\n      name: \"YouTube-transcriptie\",\n      description:\n        \"Importeer de transcriptie van een volledige YouTube-video via een link.\",\n      URL: \"URL van de YouTube-video\",\n      URL_explained_start:\n        \"Voer de URL van een YouTube-video in om de transcriptie ervan op te halen. De video moet \",\n      URL_explained_link: \"ondertiteling hebben en\",\n      URL_explained_end: \"beschikbaar zijn.\",\n      task_explained:\n        \"Zodra de transcriptie is voltooid, kan deze worden ingesloten in werkruimtes in de documentkiezer.\",\n    },\n    \"website-depth\": {\n      name: \"Bulk Link Scraper\",\n      description:\n        \"Schraap een website en de bijbehorende sublinks tot een bepaalde diepte.\",\n      URL: \"URL van de website\",\n      URL_explained: \"URL van de website die u wilt schrapen.\",\n      depth: \"Crawldiepte\",\n      depth_explained:\n        \"Dit is het aantal sublinks dat de tool vanaf de oorspronkelijke URL moet volgen.\",\n      max_pages: \"Maximum aantal pagina's\",\n      max_pages_explained: \"Maximum aantal links om te schrapen.\",\n      task_explained:\n        \"Zodra de taak is voltooid, is alle geschraapte inhoud beschikbaar om in te sluiten in werkruimtes in de documentkiezer.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Importeer een volledige Confluence-pagina met één klik.\",\n      deployment_type: \"Confluence-implementatietype\",\n      deployment_type_explained:\n        \"Bepaal of uw Confluence-instantie wordt gehost in de Atlassian-cloud of zelf gehost.\",\n      base_url: \"Confluence-basis-URL\",\n      base_url_explained: \"Dit is de basis-URL van uw Confluence-ruimte.\",\n      space_key: \"Confluence-spacesleutel\",\n      space_key_explained:\n        \"Dit is de spacesleutel van uw Confluence-instantie die zal worden gebruikt. Begint meestal met ~\",\n      username: \"Confluence-gebruikersnaam\",\n      username_explained: \"Uw Confluence-gebruikersnaam\",\n      auth_type: \"Confluence-authenticatietype\",\n      auth_type_explained:\n        \"Selecteer het authenticatietype dat u wilt gebruiken om toegang te krijgen tot uw Confluence-pagina's.\",\n      auth_type_username: \"Gebruikersnaam en toegangstoken\",\n      auth_type_personal: \"Persoonlijk toegangstoken\",\n      token: \"Confluence-toegangstoken\",\n      token_explained_start:\n        \"U moet een toegangstoken opgeven voor authenticatie. U kunt \",\n      token_explained_link: \"hier\",\n      token_desc: \" een toegangstoken genereren voor authenticatie\",\n      pat_token: \"Persoonlijk Confluence-toegangstoken\",\n      pat_token_explained: \"Uw persoonlijke Confluence-toegangstoken.\",\n      task_explained:\n        \"Zodra de taak is voltooid, is de pagina-inhoud beschikbaar om in te sluiten in werkruimtes in de documentkiezer.\",\n      bypass_ssl: \"SSL-certificaatvalidatie overslaan\",\n      bypass_ssl_explained:\n        \"Schakel deze optie in om SSL-certificaatvalidatie te omzeilen voor zelfgehoste Confluence-instanties met een zelfondertekend certificaat\",\n    },\n    manage: {\n      documents: \"Documenten\",\n      \"data-connectors\": \"Gegevensconnectoren\",\n      \"desktop-only\":\n        \"Het bewerken van deze instellingen is alleen mogelijk op een desktopapparaat. Ga naar deze pagina op uw desktop om verder te gaan.\",\n      dismiss: \"Afwijzen\",\n      editing: \"Bewerken\",\n    },\n    directory: {\n      \"my-documents\": \"Mijn documenten\",\n      \"new-folder\": \"Nieuwe map\",\n      \"search-document\": \"Zoek naar een document\",\n      \"no-documents\": \"Geen documenten\",\n      \"move-workspace\": \"Verplaatsen naar werkruimte\",\n      \"delete-confirmation\":\n        \"Weet u zeker dat u deze bestanden en mappen wilt verwijderen?\\nHiermee worden de bestanden automatisch uit het systeem en alle bestaande werkruimten verwijderd.\\nDeze actie is niet onomkeerbaar.\",\n      \"removing-message\":\n        \"{{count}} documenten en {{folderCount}} mappen worden verwijderd. Even geduld alstublieft.\",\n      \"move-success\": \"{{count}} documenten succesvol verplaatst.\",\n      no_docs: \"Geen documenten\",\n      select_all: \"Alles selecteren\",\n      deselect_all: \"Alles deselecteren\",\n      remove_selected: \"Verwijderen Geselecteerd\",\n      costs: \"*Eenmalige kosten voor embedden\",\n      save_embed: \"Opslaan en embedden\",\n      \"total-documents_one\": \"{{count}} document\",\n      \"total-documents_other\": \"{{count}} documenten\",\n    },\n    upload: {\n      \"processor-offline\": \"Documentverwerker niet beschikbaar\",\n      \"processor-offline-desc\":\n        \"We kunnen uw bestanden momenteel niet uploaden omdat de documentverwerker offline is. Probeer het later opnieuw.\",\n      \"click-upload\": \"Klik om te uploaden of sleep en laat vallen\",\n      \"file-types\":\n        \"Ondersteunt tekstbestanden, csv's, spreadsheets, audiobestanden en meer!\",\n      \"or-submit-link\": \"Of dien een link in\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"Bezig met ophalen...\",\n      \"fetch-website\": \"Website ophalen\",\n      \"privacy-notice\":\n        \"Deze bestanden worden geüpload naar de documentverwerker die op deze AnythingLLM-instantie draait. Deze bestanden worden niet verzonden naar of gedeeld met derden.\",\n    },\n    pinning: {\n      what_pinning: \"Wat is het vastzetten van documenten?\",\n      pin_explained_block1:\n        \"Wanneer u een document vastzet in AnythingLLM, injecteren we de volledige inhoud van het document in uw promptvenster, zodat uw LLM het volledig kan begrijpen.\",\n      pin_explained_block2:\n        \"Dit werkt het beste met modellen met een grote context of kleine bestanden die essentieel zijn voor de kennisbasis.\",\n      pin_explained_block3:\n        \"Als u standaard niet de gewenste antwoorden krijgt van AnythingLLM, is vastzetten een uitstekende manier om met één klik antwoorden van hogere kwaliteit te krijgen.\",\n      accept: \"Oké, begrepen.\",\n    },\n    watching: {\n      what_watching: \"Wat doet het volgen van een document?\",\n      watch_explained_block1:\n        \"Wanneer u een document in AnythingLLM volgt, synchroniseren we de inhoud van uw document automatisch met regelmatige tussenpozen vanuit de originele bron. Hierdoor wordt de inhoud in elke werkruimte waar dit bestand wordt beheerd automatisch bijgewerkt.\",\n      watch_explained_block2:\n        \"Deze functie ondersteunt momenteel online content en is niet beschikbaar voor handmatig geüploade documenten.\",\n      watch_explained_block3_start:\n        \"U kunt beheren welke documenten worden gevolgd vanuit de \",\n      watch_explained_block3_link: \"Bestandsbeheer\",\n      watch_explained_block3_end: \" beheerdersweergave.\",\n      accept: \"Oké, begrepen\",\n    },\n    obsidian: {\n      vault_location: \"Locatie van de kluis\",\n      vault_description:\n        \"Selecteer uw Obsidian-kluismap om alle notities en hun koppelingen te importeren.\",\n      selected_files: \"{{count}} markdown-bestanden gevonden\",\n      importing: \"Kluis importeren...\",\n      import_vault: \"Kluis importeren\",\n      processing_time:\n        \"Dit kan even duren, afhankelijk van de grootte van uw kluis.\",\n      vault_warning:\n        \"Zorg ervoor dat uw Obsidian-kluis niet geopend is om conflicten te voorkomen.\",\n    },\n  },\n  chat_window: {\n    send_message: \"Een bericht verzenden\",\n    attach_file: \"Een bestand aan deze chat toevoegen\",\n    text_size: \"Tekstgrootte wijzigen.\",\n    microphone: \"Spreek je prompt uit.\",\n    send: \"Promptbericht naar werkruimte verzenden\",\n    attachments_processing:\n      \"Bijlagen worden verwerkt. Even geduld alstublieft...\",\n    tts_speak_message: \"TTS-spreekbericht\",\n    copy: \"Kopiëren\",\n    regenerate: \"Opnieuw genereren\",\n    regenerate_response: \"Reactie opnieuw genereren\",\n    good_response: \"Goede reactie\",\n    more_actions: \"Meer acties\",\n    fork: \"Fork\",\n    delete: \"Verwijderen\",\n    cancel: \"Annuleren\",\n    edit_prompt: \"Prompt bewerken\",\n    edit_response: \"Reactie bewerken\",\n    preset_reset_description:\n      \"Wis je chatgeschiedenis en begin een nieuwe chat\",\n    add_new_preset: \"Nieuwe preset toevoegen\",\n    command: \"Commando\",\n    your_command: \"jouw-commando\",\n    placeholder_prompt: \"Dit is de inhoud die wordt ingevoegd voor je prompt.\",\n    description: \"Beschrijving\",\n    placeholder_description: \"Reageert met een gedicht over LLM's.\",\n    save: \"Opslaan\",\n    small: \"Klein\",\n    normal: \"Normaal\",\n    large: \"Groot\",\n    workspace_llm_manager: {\n      search: \"Zoek naar LLM-aanbieders\",\n      loading_workspace_settings: \"Werkruimte-instellingen laden...\",\n      available_models: \"Beschikbare modellen voor {{provider}}\",\n      available_models_description: \"Selecteer een model voor deze werkruimte.\",\n      save: \"Gebruik dit model\",\n      saving: \"Model instellen als standaard voor de werkruimte...\",\n      missing_credentials: \"Deze aanbieder mist logingegevens!\",\n      missing_credentials_description: \"Klik om logingegevens in te stellen\",\n    },\n    submit: \"Indienen\",\n    edit_info_user:\n      '\"Verzenden\" herstelt het antwoord van de AI. \"Opslaan\" wijzigt alleen uw bericht.',\n    edit_info_assistant:\n      \"Uw wijzigingen worden direct op deze reactie opgeslagen.\",\n    see_less: \"Minder zien\",\n    see_more: \"Meer zien\",\n    tools: \"Gereedschap\",\n    browse: \"Bladeren\",\n    text_size_label: \"Lettergrootte\",\n    select_model: \"Kies het model\",\n    sources: \"Bronnen\",\n    document: \"Document\",\n    similarity_match: \"wedstrijd\",\n    source_count_one: \"{{count}} verwijzing\",\n    source_count_other: \"{{count}} referenties\",\n    preset_exit_description: \"Beëindig de huidige agent-sessie\",\n    add_new: \"Voeg toe\",\n    edit: \"Bewerk\",\n    publish: \"Publiceren\",\n    stop_generating: \"Stoppen met het genereren van antwoorden\",\n    pause_tts_speech_message: \"Pauzeer de spraak van de tekstberichten.\",\n    slash_commands: \"Korte commando's\",\n    agent_skills: \"Vaardigheden van agenten\",\n    manage_agent_skills: \"Beheer van de vaardigheden van de agent\",\n    agent_skills_disabled_in_session:\n      \"Het is niet mogelijk om vaardigheden aan te passen tijdens een actieve sessie. Gebruik eerst de commando `/exit` om de sessie te beëindigen.\",\n    start_agent_session: \"Start Agent Sessie\",\n    use_agent_session_to_use_tools:\n      'U kunt tools in de chat gebruiken door een sessie met een agent te starten, beginnend met \"@agent\" aan het begin van uw bericht.',\n  },\n  profile_settings: {\n    edit_account: \"Account bewerken\",\n    profile_picture: \"Profielafbeelding\",\n    remove_profile_picture: \"Profielafbeelding verwijderen\",\n    username: \"Gebruikersnaam\",\n    new_password: \"Nieuw wachtwoord\",\n    password_description: \"Wachtwoord moet minimaal 8 tekens lang zijn\",\n    cancel: \"Annuleren\",\n    update_account: \"Account bijwerken\",\n    theme: \"Themavoorkeur\",\n    language: \"Voorkeurstaal\",\n    failed_upload: \"Uploaden van profielafbeelding mislukt: {{error}}\",\n    upload_success: \"Profielafbeelding geüpload.\",\n    failed_remove: \"Verwijderen van profielafbeelding mislukt: {{error}}\",\n    profile_updated: \"Profiel bijgewerkt.\",\n    failed_update_user: \"Gebruiker bijwerken mislukt: {{error}}\",\n    account: \"Account\",\n    support: \"Ondersteuning\",\n    signout: \"Afmelden\",\n  },\n  customization: {\n    interface: {\n      title: \"UI-voorkeuren\",\n      description: \"Stel uw UI-voorkeuren in voor AnythingLLM.\",\n    },\n    branding: {\n      title: \"Branding & Whitelabeling\",\n      description:\n        \"Geef uw AnythingLLM-instantie een whitelabel met uw eigen branding.\",\n    },\n    chat: {\n      title: \"Chat\",\n      description: \"Stel uw chatvoorkeuren in voor AnythingLLM.\",\n      auto_submit: {\n        title: \"Spraakinvoer automatisch verzenden\",\n        description:\n          \"Verzend spraakinvoer automatisch na een periode van stilte\",\n      },\n      auto_speak: {\n        title: \"Antwoorden automatisch uitspreken\",\n        description: \"Spreek antwoorden van de AI automatisch uit\",\n      },\n      spellcheck: {\n        title: \"Spellingscontrole inschakelen\",\n        description:\n          \"Schakel de spellingscontrole in of uit in het chatinvoerveld\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Thema\",\n        description: \"Selecteer uw favoriete kleurenthema voor de applicatie.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Scrollbalk weergeven\",\n        description: \"Schakel de scrollbalk in of uit in het chatvenster.\",\n      },\n      \"support-email\": {\n        title: \"E-mailadres voor ondersteuning\",\n        description:\n          \"Stel het e-mailadres voor ondersteuning in dat toegankelijk moet zijn voor gebruikers wanneer ze hulp nodig hebben.\",\n      },\n      \"app-name\": {\n        title: \"Naam\",\n        description:\n          \"Stel een naam in die op de inlogpagina voor alle gebruikers wordt weergegeven.\",\n      },\n      \"display-language\": {\n        title: \"Weergavetaal\",\n        description:\n          \"Selecteer de gewenste taal waarin de gebruikersinterface van AnythingLLM moet worden weergegeven - wanneer vertalingen beschikbaar zijn.\",\n      },\n      logo: {\n        title: \"Merklogo\",\n        description: \"Upload uw eigen logo om op alle pagina's te tonen.\",\n        add: \"Voeg een eigen logo toe\",\n        recommended: \"Aanbevolen formaat: 800 x 200\",\n        remove: \"Verwijderen\",\n        replace: \"Vervangen\",\n      },\n      \"browser-appearance\": {\n        title: \"Browserweergave\",\n        description:\n          \"Pas de weergave van het browsertabblad en de titel aan wanneer de app is geopend.\",\n        tab: {\n          title: \"Titel\",\n          description:\n            \"Stel een aangepaste tabtitel in wanneer de app in een browser wordt geopend.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description:\n            \"Gebruik een aangepaste favicon voor het browsertabblad.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Voettekst items in de zijbalk\",\n        description:\n          \"Pas de voettekst items aan die onderaan de zijbalk worden weergegeven.\",\n        icon: \"Pictogram\",\n        link: \"Link\",\n      },\n      \"render-html\": {\n        title: \"HTML weergeven in chat\",\n        description:\n          \"HTML-reacties weergeven in assistentreacties.\\nLet op: Dit kan resulteren in een veel hogere kwaliteit van de reacties, maar kan ook leiden tot potentiële beveiligingsrisico's.\",\n      },\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Maak een agent\",\n      editWorkspace: \"Werkruimte bewerken\",\n      uploadDocument: \"Upload een document\",\n    },\n    greeting: \"Hoe kan ik u vandaag helpen?\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Sneltoetsen\",\n    shortcuts: {\n      settings: \"Instellingen openen\",\n      workspaceSettings: \"Huidige werkruimte-instellingen openen\",\n      home: \"Naar de startpagina gaan\",\n      workspaces: \"Werkruimtes beheren\",\n      apiKeys: \"Instellingen voor API-sleutels\",\n      llmPreferences: \"LLM-voorkeuren\",\n      chatSettings: \"Chat-instellingen\",\n      help: \"Help voor toetsenbordsneltoetsen weergeven\",\n      showLLMSelector: \"LLM-selector voor werkruimtes weergeven\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Geslaagd!\",\n        success_description:\n          \"Uw systeemprompt is gepubliceerd op de Community Hub!\",\n        success_thank_you: \"Bedankt voor het delen met de community!\",\n        view_on_hub: \"Bekijken op Community Hub\",\n        modal_title: \"Systeemprompt publiceren\",\n        name_label: \"Naam\",\n        name_description: \"Dit is de weergavenaam van je systeemprompt.\",\n        name_placeholder: \"Mijn systeemprompt\",\n        description_label: \"Beschrijving\",\n        description_description:\n          \"Dit is de beschrijving van je systeemprompt. Gebruik dit om het doel van je systeemprompt te beschrijven.\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Tags worden gebruikt om je systeemprompt te labelen voor gemakkelijker zoeken. Je kunt meerdere tags toevoegen. Maximaal 5 tags. Maximaal 20 tekens per tag.\",\n        tags_placeholder: \"Typ en druk op Enter om tags toe te voegen\",\n        visibility_label: \"Zichtbaarheid\",\n        public_description:\n          \"Openbare systeemprompts zijn voor iedereen zichtbaar.\",\n        private_description:\n          \"Privé systeemprompts zijn alleen voor jou zichtbaar.\",\n        publish_button: \"Publiceren naar Community Hub\",\n        submitting: \"Publiceren...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Dit is de daadwerkelijke systeemprompt die gebruikt zal worden om de LLM te begeleiden.\",\n        prompt_placeholder: \"Voer hier uw systeemprompt in...\",\n      },\n      agent_flow: {\n        success_title: \"Succes!\",\n        success_description:\n          \"Je agentflow is gepubliceerd op de Community Hub!\",\n        success_thank_you: \"Bedankt voor het delen met de community!\",\n        view_on_hub: \"Bekijk op de Community Hub\",\n        modal_title: \"Agentflow publiceren\",\n        name_label: \"Naam\",\n        name_description: \"Dit is de weergavenaam van je agentflow.\",\n        name_placeholder: \"Mijn agentflow\",\n        description_label: \"Beschrijving\",\n        description_description:\n          \"Dit is de beschrijving van je agentflow. Gebruik dit om het doel van je agentflow te beschrijven.\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Tags worden gebruikt om je agentflow te labelen voor eenvoudiger zoeken. Je kunt meerdere tags toevoegen. Maximaal 5 tags. Maximaal 20 tekens per tag.\",\n        tags_placeholder: \"Typ en druk op Enter om tags toe te voegen\",\n        visibility_label: \"Zichtbaarheid\",\n        submitting: \"Publiceren...\",\n        submit: \"Publiceren naar Community Hub\",\n        privacy_note:\n          \"Agentflows worden altijd als privé geüpload om gevoelige gegevens te beschermen. U kunt de zichtbaarheid in de Community Hub wijzigen na publicatie. Controleer of uw flow geen gevoelige of privé-informatie bevat voordat u publiceert.\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Authenticatie vereist\",\n          description:\n            \"U moet zich authenticeren bij de AnythingLLM Community Hub voordat u items kunt publiceren.\",\n          button: \"Verbinden met Community Hub\",\n        },\n      },\n      slash_command: {\n        success_title: \"Succes!\",\n        success_description:\n          \"Je slash-commando is gepubliceerd op de Community Hub!\",\n        success_thank_you: \"Bedankt voor het delen met de community!\",\n        view_on_hub: \"Bekijk op de Community Hub\",\n        modal_title: \"Slash-commando publiceren\",\n        name_label: \"Naam\",\n        name_description: \"Dit is de weergavenaam van je slash-commando.\",\n        name_placeholder: \"Mijn slash-commando\",\n        description_label: \"Beschrijving\",\n        description_description:\n          \"Dit is de beschrijving van je slash-commando. Gebruik dit om het doel van je slash-commando te beschrijven.\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Tags worden gebruikt om je slash-commando te labelen voor eenvoudiger zoeken. Je kunt meerdere tags toevoegen. Max 5 tags. Maximaal 20 tekens per tag.\",\n        tags_placeholder: \"Typ en druk op Enter om tags toe te voegen\",\n        visibility_label: \"Zichtbaarheid\",\n        public_description:\n          \"Openbare slash-opdrachten zijn voor iedereen zichtbaar.\",\n        private_description:\n          \"Privé slash-opdrachten zijn alleen voor jou zichtbaar.\",\n        publish_button: \"Publiceren naar Community Hub\",\n        submitting: \"Publiceren...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Dit is de prompt die wordt gebruikt wanneer de slash-opdracht wordt geactiveerd.\",\n        prompt_placeholder: \"Voer hier je prompt in...\",\n      },\n    },\n  },\n  security: {\n    title: \"Veiligheid\",\n    multiuser: {\n      title: \"Multi-Gebruikersmodus\",\n      description:\n        \"Stel je instantie in om je team te ondersteunen door Multi-Gebruikersmodus in te schakelen.\",\n      enable: {\n        \"is-enable\": \"Multi-Gebruikersmodus is Ingeschakeld\",\n        enable: \"Schakel Multi-Gebruikersmodus In\",\n        description:\n          \"Standaard ben je de enige beheerder. Als beheerder moet je accounts aanmaken voor alle nieuwe gebruikers of beheerders. Verlies je wachtwoord niet, want alleen een beheerdersgebruiker kan wachtwoorden resetten.\",\n        username: \"Beheerdersaccount gebruikersnaam\",\n        password: \"Beheerdersaccount wachtwoord\",\n      },\n    },\n    password: {\n      title: \"Wachtwoordbeveiliging\",\n      description:\n        \"Bescherm je AnythingLLM-instantie met een wachtwoord. Als je dit vergeet, is er geen herstelmethode, dus zorg ervoor dat je dit wachtwoord opslaat.\",\n      \"password-label\": \"Instances wachtwoord\",\n    },\n  },\n  home: {\n    welcome: \"Welkom\",\n    chooseWorkspace: \"Kies een werkruimte om te beginnen!\",\n    notAssigned:\n      \"Je bent nog niet toegewezen aan een werkruimte.\\nNeem contact op met je beheerder om toegang te vragen tot een werkruimte.\",\n    goToWorkspace: 'Ga naar de werkruimte \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/normalizeEn.mjs",
    "content": "/* global process */\n// This script is used to normalize the translations files to ensure they are all the same.\n// This will take the en file and compare it to all other files and ensure they are all the same.\n// If a non-en file is missing a key, it will be added to the file and set to null\nimport { resources } from \"./resources.js\";\nimport fs from \"fs\";\nconst languageNames = new Intl.DisplayNames(Object.keys(resources), {\n  type: \"language\",\n});\n\nfunction langDisplayName(lang) {\n  return languageNames.of(lang);\n}\n\nfunction compareStructures(lang, a, b, subdir = null) {\n  //if a and b aren't the same type, they can't be equal\n  if (typeof a !== typeof b && a !== null && b !== null) {\n    console.log(\"Invalid type comparison\", [\n      {\n        lang,\n        a: typeof a,\n        b: typeof b,\n        values: {\n          a,\n          b,\n        },\n        ...(!!subdir ? { subdir } : {}),\n      },\n    ]);\n    return false;\n  }\n\n  // Need the truthy guard because\n  // typeof null === 'object'\n  if (a && typeof a === \"object\") {\n    var keysA = Object.keys(a).sort(),\n      keysB = Object.keys(b).sort();\n\n    //if a and b are objects with different no of keys, unequal\n    if (keysA.length !== keysB.length) {\n      console.log(\"Keys are missing!\", {\n        [lang]: keysA,\n        en: keysB,\n        ...(!!subdir ? { subdir } : {}),\n        diff: {\n          added: keysB.filter((key) => !keysA.includes(key)),\n          removed: keysA.filter((key) => !keysB.includes(key)),\n        },\n      });\n      return false;\n    }\n\n    //if keys aren't all the same, unequal\n    if (\n      !keysA.every(function (k, i) {\n        return k === keysB[i];\n      })\n    ) {\n      console.log(\"Keys are not equal!\", {\n        [lang]: keysA,\n        en: keysB,\n        ...(!!subdir ? { subdir } : {}),\n      });\n      return false;\n    }\n\n    //recurse on the values for each key\n    return keysA.every(function (key) {\n      //if we made it here, they have identical keys\n      return compareStructures(lang, a[key], b[key], key);\n    });\n\n    //for primitives just ignore since we don't check values.\n  } else {\n    return true;\n  }\n}\n\nfunction normalizeTranslations(lang, source, target, _subdir = null) {\n  // Handle primitives - if target exists, keep it, otherwise set null\n  if (!source || typeof source !== \"object\") {\n    return target ?? null;\n  }\n\n  // Handle objects\n  const normalized = target && typeof target === \"object\" ? { ...target } : {};\n\n  // Add all keys from source (English), setting to null if missing\n  for (const key of Object.keys(source)) {\n    normalized[key] = normalizeTranslations(\n      lang,\n      source[key],\n      normalized[key],\n      key\n    );\n  }\n\n  // If a non-en file has a key that is NOT in the en file, it will be removed\n  for (const key of Object.keys(normalized)) {\n    if (!source[key]) delete normalized[key];\n  }\n\n  return normalized;\n}\n\nfunction ISOToFilename(lang) {\n  const ISO_TO_FILENAME = {\n    \"zh-tw\": \"zh_TW\",\n    pt: \"pt_BR\",\n    vi: \"vn\",\n  };\n  return ISO_TO_FILENAME[lang] || lang.replace(\"-\", \"_\");\n}\n\nconst failed = [];\nconst TRANSLATIONS = {};\nfor (const [lang, { common }] of Object.entries(resources)) {\n  TRANSLATIONS[lang] = common;\n}\n\nconst PRIMARY = { ...TRANSLATIONS[\"en\"] };\ndelete TRANSLATIONS[\"en\"];\n\nconsole.log(\n  `The following translation files will be normalized against the English file: [${Object.keys(\n    TRANSLATIONS\n  ).join(\",\")}]`\n);\n\n// Normalize each non-English translation\nfor (const [lang, translations] of Object.entries(TRANSLATIONS)) {\n  const normalized = normalizeTranslations(lang, PRIMARY, translations);\n\n  // Update the translations in resources\n  resources[lang].common = normalized;\n\n  // Verify the structure matches\n  const passed = compareStructures(lang, normalized, PRIMARY);\n  console.log(`${langDisplayName(lang)} (${lang}): ${passed ? \"✅\" : \"❌\"}`);\n  !passed && failed.push(lang);\n\n  const langFilename = ISOToFilename(lang);\n  fs.writeFileSync(\n    `./${langFilename}/common.js`,\n    `// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = ${JSON.stringify(normalized, null, 2)}\n\nexport default TRANSLATIONS;`\n  );\n}\n\nif (failed.length !== 0) {\n  throw new Error(\n    `Error verifying normalized translations. Please check the logs.`,\n    failed\n  );\n}\n\nconsole.log(\n  `👍 All translation files have been normalized to match the English schema!`\n);\n\nprocess.exit(0);\n"
  },
  {
    "path": "frontend/src/locales/pl/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"Witamy w\",\n      getStarted: \"Rozpocznij\",\n    },\n    llm: {\n      title: \"Preferencje modeli językowych\",\n      description:\n        \"AnythingLLM może współpracować z wieloma dostawcami modeli językowych\",\n    },\n    userSetup: {\n      title: \"Konfiguracja użytkownika\",\n      description: \"Skonfiguruj ustawienia użytkownika.\",\n      howManyUsers: \"Ilu użytkowników będzie korzystać z tej instancji?\",\n      justMe: \"Tylko ja\",\n      myTeam: \"Mój zespół\",\n      instancePassword: \"Hasło instancji\",\n      setPassword: \"Czy chcesz ustawić hasło?\",\n      passwordReq: \"Hasła muszą składać się z co najmniej 8 znaków.\",\n      passwordWarn:\n        \"Ważne jest, aby zapisać to hasło, ponieważ nie ma metody jego odzyskania.\",\n      adminUsername: \"Nazwa użytkownika konta administratora\",\n      adminPassword: \"Hasło konta administratora\",\n      adminPasswordReq: \"Hasła muszą składać się z co najmniej 8 znaków.\",\n      teamHint:\n        \"Domyślnie będziesz jedynym administratorem. Po zakończeniu wdrażania możesz tworzyć i zapraszać innych użytkowników lub administratorów. Nie zgub hasła, ponieważ tylko administratorzy mogą je resetować.\",\n    },\n    data: {\n      title: \"Obsługa danych i prywatność\",\n      description:\n        \"Dbamy o przejrzystość i kontrolę danych osobowych użytkowników.\",\n      settingsHint:\n        \"Ustawienia te można zmienić w dowolnym momencie w ustawieniach.\",\n    },\n    survey: {\n      title: \"Witamy w AnythingLLM\",\n      description:\n        \"Pomóż nam stworzyć AnythingLLM dostosowany do Twoich potrzeb. Opcjonalnie.\",\n      email: \"Jaki jest Twój adres e-mail?\",\n      useCase: \"Do czego będziesz używać AnythingLLM?\",\n      useCaseWork: \"Do pracy\",\n      useCasePersonal: \"Do użytku osobistego\",\n      useCaseOther: \"Inne\",\n      comment: \"Skąd dowiedziałeś się o AnythingLLM?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube itp. - Daj nam znać, jak nas znalazłeś!\",\n      skip: \"Pomiń ankietę\",\n      thankYou: \"Dziękujemy za opinię!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Nazwa obszaru roboczego\",\n    user: \"Użytkownik\",\n    selection: \"Wybór modelu\",\n    saving: \"Zapisywanie...\",\n    save: \"Zapisz zmiany\",\n    previous: \"Poprzednia strona\",\n    next: \"Następna strona\",\n    optional: \"Opcjonalnie\",\n    yes: \"Tak\",\n    no: \"Nie\",\n    search: \"Wyszukaj\",\n    username_requirements:\n      \"Nazwa użytkownika musi mieć od 2 do 32 znaków, zaczynać się małą literą i zawierać tylko małe litery, cyfry, podkreślenia, myślniki i kropki.\",\n    on: \"Na\",\n    none: \"Brak\",\n    stopped: \"Zatrzymano\",\n    loading: \"Ładowanie\",\n    refresh: \"Odświeżyć\",\n  },\n  settings: {\n    title: \"Ustawienia instancji\",\n    invites: \"Zaproszenia\",\n    users: \"Użytkownicy\",\n    workspaces: \"Obszary robocze\",\n    \"workspace-chats\": \"Czaty w obszarach roboczych\",\n    customization: \"Personalizacja\",\n    interface: \"Preferencje interfejsu użytkownika\",\n    branding: \"Branding i white-labeling\",\n    chat: \"Czat\",\n    \"api-keys\": \"Interfejs API dla programistów\",\n    llm: \"LLM\",\n    transcription: \"Transkrypcja\",\n    embedder: \"Embeddings\",\n    \"text-splitting\": \"Dzielenie tekstu\",\n    \"voice-speech\": \"Głos i mowa\",\n    \"vector-database\": \"Wektorowa baza danych\",\n    embeds: \"Osadzone czaty\",\n    security: \"Bezpieczeństwo\",\n    \"event-logs\": \"Dzienniki zdarzeń\",\n    privacy: \"Prywatność i dane\",\n    \"ai-providers\": \"Dostawcy AI\",\n    \"agent-skills\": \"Umiejętności agenta\",\n    admin: \"Administrator\",\n    tools: \"Narzędzia\",\n    \"system-prompt-variables\": \"Zmienne instrukcji systemowej\",\n    \"experimental-features\": \"Funkcje eksperymentalne\",\n    contact: \"Kontakt z pomocą techniczną\",\n    \"browser-extension\": \"Rozszerzenie przeglądarki\",\n    \"mobile-app\": \"AnythingLLM Mobile\",\n    \"community-hub\": {\n      title: \"Centrum Społeczności\",\n      trending: \"Odkryj popularne\",\n      \"your-account\": \"Twój profil\",\n      \"import-item\": \"Importuj element\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Witamy w\",\n      \"placeholder-username\": \"Nazwa użytkownika\",\n      \"placeholder-password\": \"Hasło\",\n      login: \"Logowanie\",\n      validating: \"Weryfikacja...\",\n      \"forgot-pass\": \"Nie pamiętam hasła\",\n      reset: \"Reset\",\n    },\n    \"sign-in\": \"Zaloguj się do {{appName}}.\",\n    \"password-reset\": {\n      title: \"Resetowanie hasła\",\n      description: \"Podaj poniżej niezbędne informacje, aby zresetować hasło.\",\n      \"recovery-codes\": \"Kody odzyskiwania\",\n      \"back-to-login\": \"Powrót do logowania\",\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Utwórz agenta\",\n      editWorkspace: \"Edytuj przestrzeń roboczą\",\n      uploadDocument: \"Załaduj dokument\",\n    },\n    greeting: \"W czym mogę Ci dzisiaj pomóc?\",\n  },\n  \"new-workspace\": {\n    title: \"Nowy obszar roboczy\",\n    placeholder: \"Mój obszar roboczy\",\n  },\n  \"workspaces—settings\": {\n    general: \"Ustawienia ogólne\",\n    chat: \"Ustawienia czatu\",\n    vector: \"Wektorowa baza danych\",\n    members: \"Członkowie\",\n    agent: \"Konfiguracja agenta\",\n  },\n  general: {\n    vector: {\n      title: \"Liczba wektorów\",\n      description: \"Całkowita liczba wektorów w bazie danych wektorów.\",\n    },\n    names: {\n      description:\n        \"Spowoduje to jedynie zmianę wyświetlanej nazwy obszaru roboczego.\",\n    },\n    message: {\n      title: \"Sugerowane wiadomości na czacie\",\n      description: \"Dostosuj wiadomości, które będą sugerowane użytkownikom.\",\n      add: \"Dodaj nową wiadomość\",\n      save: \"Zapisz wiadomości\",\n      heading: \"Wyjaśnij mi\",\n      body: \"Korzyści z AnythingLLM\",\n    },\n    delete: {\n      title: \"Usuń obszar roboczy\",\n      description:\n        \"Usuń ten obszar roboczy i wszystkie jego dane. Spowoduje to usunięcie obszaru roboczego dla wszystkich użytkowników.\",\n      delete: \"Usuń obszar roboczy\",\n      deleting: \"Usuwanie obszaru roboczego...\",\n      \"confirm-start\": \"Zamierzasz usunąć cały swój\",\n      \"confirm-end\":\n        \"obszar roboczy. Spowoduje to usunięcie wszystkich danych z wektorowej bazy danych. Oryginalne pliki źródłowe pozostaną nietknięte. Działanie to jest nieodwracalne.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Dostawca modeli językowych dla obszaru roboczego\",\n      description:\n        \"Konkretny dostawca i model LLM, który będzie używany dla tego obszaru roboczego. Domyślnie używany jest dostawca i model z preferencji systemowych.\",\n      search: \"Wyszukaj wszystkich dostawców LLM\",\n    },\n    model: {\n      title: \"Model językowy dla obszaru roboczego\",\n      description:\n        \"Określony model, który będzie używany w tym obszarze roboczym. Jeśli pole jest puste, użyty zostanie model z preferencji systemowych.\",\n    },\n    mode: {\n      title: \"Tryb czatu\",\n      chat: {\n        title: \"Czat\",\n        description:\n          \"zapewni odpowiedzi, wykorzystując ogólną wiedzę LLM oraz kontekst dokumentu, w którym ta wiedza znajduje się.<br />Będziesz musiał użyć komendy `@agent` w celu korzystania z narzędzi.\",\n      },\n      query: {\n        title: \"Zapytanie (wyszukiwanie)\",\n        description:\n          \"będzie dostarczać odpowiedzi <b>tylko</b>, jeśli zostanie zidentyfikowany kontekst dokumentu.<br />Będziesz musiał użyć komendy `@agent` w celu korzystania z narzędzi.\",\n      },\n      automatic: {\n        title: \"Samochód\",\n        description:\n          \"automatycznie będzie wykorzystywał narzędzia, jeśli model i dostawca obsługują natywne wywoływanie narzędzi. Jeśli natywne narzędzia nie są obsługiwane, konieczne będzie użycie polecenia `@agent` w celu wykorzystania narzędzi.\",\n      },\n    },\n    history: {\n      title: \"Historia czatu\",\n      \"desc-start\":\n        \"Liczba poprzednich wiadomości, które zostaną uwzględnione w pamięci krótkotrwałej\",\n      recommend: \"Zalecane: 20.\",\n      \"desc-end\":\n        \"Więcej niż 45 może prowadzić do problemów z działaniem czatu.\",\n    },\n    prompt: {\n      title: \"Instrukcja systemowa\",\n      description:\n        \"Instrukcja, która będzie używana w tym obszarze roboczym. Zdefiniuj kontekst i instrukcje dla AI. Powinieneś dostarczyć starannie opracowaną instrukcję, aby AI mogło wygenerować odpowiednią i dokładną odpowiedź.\",\n      history: {\n        title: \"Historia instrukcji systemowych\",\n        clearAll: \"Wyczyść wszystko\",\n        noHistory: \"Historia instrukcji systemowych nie jest dostępna\",\n        restore: \"Przywróć\",\n        delete: \"Usuń\",\n        publish: \"Opublikuj w Community Hub\",\n        deleteConfirm: \"Czy na pewno chcesz usunąć ten element historii?\",\n        clearAllConfirm:\n          \"Czy na pewno chcesz wyczyścić całą historię? Tej czynności nie można cofnąć.\",\n        expand: \"Rozwiń\",\n      },\n    },\n    refusal: {\n      title: \"Tryb zapytania - odpowiedź odmowna\",\n      \"desc-start\": \"W trybie\",\n      query: \"zapytania (wyszukiwanie)\",\n      \"desc-end\":\n        \"istnieje możliwość zwrócenia niestandardowej odpowiedzi odmownej, w sytuacji gdy nie znaleziono odpowiedniego kontekstu.\",\n      \"tooltip-title\": \"Dlaczego to widzę?\",\n      \"tooltip-description\":\n        \"Jesteś w trybie zapytań, który wykorzystuje tylko informacje z Twoich dokumentów. Przełącz się do trybu czatu, aby uzyskać bardziej elastyczne rozmowy, lub kliknij tutaj, aby odwiedzić naszą dokumentację i dowiedzieć się więcej o trybach czatu.\",\n    },\n    temperature: {\n      title: \"Temperatura modelu\",\n      \"desc-start\":\n        'To ustawienie kontroluje, jak \"kreatywne\" będą odpowiedzi modelu językowego.',\n      \"desc-end\":\n        \"Im wyższa liczba, tym większa kreatywność. W przypadku niektórych modeli może to prowadzić do niespójnych odpowiedzi przy zbyt wysokich ustawieniach.\",\n      hint: \"Większość modeli językowych ma różne dopuszczalne zakresy wartości. Informacje na ten temat można uzyskać u dostawcy modelu językowego.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Identyfikator wektorowej bazy danych\",\n    snippets: {\n      title: \"Maksymalna liczba fragmentów\",\n      description:\n        \"To ustawienie kontroluje maksymalną ilość fragmentów kontekstu, które zostaną wysłane do modelu językowego.\",\n      recommend: \"Zalecane: 4\",\n    },\n    doc: {\n      title: \"Próg podobieństwa dokumentów\",\n      description:\n        \"Minimalny wynik podobieństwa wymagany do uznania źródła za powiązane z czatem. Im wyższa liczba, tym bardziej źródło musi być powiązane z czatem.\",\n      zero: \"Brak ograniczeń\",\n      low: \"Niski (wynik podobieństwa ≥ .25)\",\n      medium: \"Średni (wynik podobieństwa ≥ .50)\",\n      high: \"Wysoki (wynik podobieństwa ≥ .75)\",\n    },\n    reset: {\n      reset: \"Resetuj bazę wektorową\",\n      resetting: \"Czyszczenie wektorów...\",\n      confirm:\n        \"Baza danych wektorów tego obszaru roboczego zostanie zresetowana. Spowoduje to usunięcie wszystkich aktualnie osadzonych wektorów. Oryginalne pliki źródłowe pozostaną nietknięte. Ta czynność jest nieodwracalna.\",\n      error: \"Nie można zresetować bazy danych wektorów obszaru roboczego!\",\n      success: \"Baza danych wektorów obszaru roboczego została zresetowana!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"Wydajność modeli LLM, które nie obsługują bezpośrednio wywoływania narzędzi, zależy w dużym stopniu od możliwości i dokładności modelu. Niektóre możliwości mogą być ograniczone lub niefunkcjonalne.\",\n    provider: {\n      title: \"Dostawca LLM dla agenta\",\n      description:\n        \"Konkretny dostawca i model LLM, który będzie używany dla agenta @agent, w tym obszarze roboczym.\",\n    },\n    mode: {\n      chat: {\n        title: \"Model czatu agenta\",\n        description:\n          \"Konkretny model czatu, który będzie używany dla agenta @agent tego obszaru roboczego.\",\n      },\n      title: \"Model agenta\",\n      description:\n        \"Konkretny model LLM, który będzie używany dla agenta @agent tego obszaru roboczego.\",\n      wait: \"-- oczekiwanie na modele\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG i pamięć długotrwała\",\n        description:\n          'Pozwól agentowi wykorzystać twoje lokalne dokumenty, aby odpowiedzieć na zapytanie lub poproś agenta o \"zapamiętanie\" fragmentów treści w celu odzyskania pamięci długoterminowej.',\n      },\n      view: {\n        title: \"Wyświetlanie i podsumowywanie dokumentów\",\n        description:\n          \"Umożliwienie agentowi wyświetlenia listy i podsumowania zawartości aktualnie osadzonych plików obszaru roboczego.\",\n      },\n      scrape: {\n        title: \"Pobieranie treści stron internetowych\",\n        description:\n          \"Zezwalaj agentowi na odwiedzanie i pobieranie zawartości stron internetowych.\",\n      },\n      generate: {\n        title: \"Generowanie wykresów\",\n        description:\n          \"Pozwól domyślnemu agentowi generować różne typy wykresów na podstawie danych dostarczonych lub podanych na czacie.\",\n      },\n      save: {\n        title: \"Generowanie i zapisywanie plików w przeglądarce\",\n        description:\n          \"Pozwól domyślnemu agentowi generować i zapisywać pliki, które można zapisać i pobrać w przeglądarce.\",\n      },\n      web: {\n        title: \"Wyszukiwanie i przeglądanie stron internetowych na żywo\",\n        description:\n          \"Pozwól swojemu agentowi na wyszukiwanie informacji w Internecie, aby odpowiadał na Twoje pytania, poprzez połączenie z dostawcą usług wyszukiwania (SERP).\",\n      },\n      sql: {\n        title: \"Połączenie z bazą danych SQL\",\n        description:\n          \"Umożliw agentowi korzystanie z języka SQL, aby odpowiadał na Twoje pytania, poprzez połączenie z różnymi dostawcami baz danych SQL.\",\n      },\n      default_skill:\n        \"Domyślnie, ta umiejętność jest włączona, ale można ją wyłączyć, jeśli nie chcemy, aby była dostępna dla agenta.\",\n    },\n    mcp: {\n      title: \"Serwery MCP\",\n      \"loading-from-config\": \"Ładowanie serwerów MCP z pliku konfiguracyjnego\",\n      \"learn-more\": \"Dowiedz się więcej o serwerach MCP.\",\n      \"no-servers-found\": \"Nie znaleziono serwerów MCP.\",\n      \"tool-warning\":\n        \"Aby uzyskać najlepsze wyniki, rozważ wyłączenie niepotrzebnych narzędzi, aby zminimalizować zakłócenia.\",\n      \"stop-server\": \"Zatrzymaj serwer MCP\",\n      \"start-server\": \"Uruchom serwer MCP\",\n      \"delete-server\": \"Usuń serwer MCP\",\n      \"tool-count-warning\":\n        \"Ten serwer MCP ma włączone <b> narzędzia, które będą zużywać kontekst w każdej rozmowie.</b> Rozważ wyłączenie niepotrzebnych narzędzi, aby oszczędzać kontekst.\",\n      \"startup-command\": \"Polecenie uruchamiające\",\n      command: \"Rozkaz\",\n      arguments: \"Argumenty\",\n      \"not-running-warning\":\n        \"Ten serwer MCP nie działa – może być zatrzymany lub może występować w nim błąd podczas uruchamiania.\",\n      \"tool-call-arguments\": \"Argumenty wywoływania funkcji\",\n      \"tools-enabled\": \"narzędzia są aktywne\",\n    },\n    settings: {\n      title: \"Ustawienia umiejętności agenta\",\n      \"max-tool-calls\": {\n        title: \"Maksymalna liczba żądań narzędzi na odpowiedź\",\n        description:\n          \"Maksymalna liczba narzędzi, które agent może łączyć, aby wygenerować pojedynczą odpowiedź. Zapobiega to niekontrolowanemu wywoływaniu narzędzi i tworzeniu nieskończonych pętli.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Inteligentny wybór umiejętności\",\n        \"beta-badge\": \"Wersja beta\",\n        description:\n          \"Umożliwia korzystanie z nieograniczonej liczby narzędzi oraz redukcję zużycia tokenów o do 80% na każde zapytanie – EverythingLLM automatycznie wybiera odpowiednie umiejętności dla każdego zapytania.\",\n        \"max-tools\": {\n          title: \"Narzędzia Max\",\n          description:\n            \"Maksymalna liczba narzędzi, które można wybrać dla każdego zapytania. Zalecamy ustawienie tej wartości na wyższe poziomy dla modeli o większym kontekście.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Czaty w obszarach roboczych\",\n    description:\n      \"Są to wszystkie czaty i wiadomości wysłane przez użytkowników uporządkowane według daty utworzenia.\",\n    export: \"Eksport\",\n    table: {\n      id: \"ID\",\n      by: \"Wysłane przez\",\n      workspace: \"Obszar roboczy\",\n      prompt: \"Prompt\",\n      response: \"Odpowiedź\",\n      at: \"Wysłane o\",\n    },\n  },\n  customization: {\n    interface: {\n      title: \"Preferencje interfejsu użytkownika\",\n      description: \"Ustaw preferencje interfejsu użytkownika dla AnythingLLM.\",\n    },\n    branding: {\n      title: \"Branding i white-labeling\",\n      description:\n        \"Oznakuj swoją instancję AnythingLLM niestandardowym brandingiem.\",\n    },\n    chat: {\n      title: \"Czat\",\n      description: \"Ustaw preferencje czatu dla AnythingLLM.\",\n      auto_submit: {\n        title: \"Automatyczne przesyłanie mowy\",\n        description: \"Automatyczne przesyłanie mowy po wykryciu ciszy.\",\n      },\n      auto_speak: {\n        title: \"Automatyczne wypowiadanie odpowiedzi\",\n        description: \"Automatycznie wypowiadaj odpowiedzi AI.\",\n      },\n      spellcheck: {\n        title: \"Włącz sprawdzanie pisowni\",\n        description:\n          \"Włącz lub wyłącz sprawdzanie pisowni w polu wprowadzania tekstu.\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Motyw\",\n        description: \"Wybierz preferowany motyw kolorystyczny dla aplikacji.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Pokaż pasek przewijania\",\n        description: \"Włącz lub wyłącz pasek przewijania w oknie czatu.\",\n      },\n      \"support-email\": {\n        title: \"E-mail wsparcia\",\n        description:\n          \"Ustaw adres e-mail, który będzie dostępny dla użytkowników, gdy potrzebują pomocy.\",\n      },\n      \"app-name\": {\n        title: \"Nazwa\",\n        description:\n          \"Ustawienie nazwy wyświetlanej na stronie logowania dla wszystkich użytkowników.\",\n      },\n      \"display-language\": {\n        title: \"Język\",\n        description:\n          \"Wybierz preferowany język interfejsu użytkownika AnythingLLM - jeśli dostępne są tłumaczenia.\",\n      },\n      logo: {\n        title: \"Logo\",\n        description:\n          \"Prześlij swoje niestandardowe logo, aby wyświetlić je na wszystkich stronach.\",\n        add: \"Dodaj niestandardowe logo\",\n        recommended: \"Zalecany rozmiar: 800 x 200\",\n        remove: \"Usuń\",\n        replace: \"Zmień\",\n      },\n      \"browser-appearance\": {\n        title: \"Wygląd przeglądarki\",\n        description:\n          \"Dostosuj wygląd karty przeglądarki, gdy aplikacja jest otwarta.\",\n        tab: {\n          title: \"Tytuł\",\n          description:\n            \"Ustawienie niestandardowego tytułu karty, gdy aplikacja jest otwarta w przeglądarce.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description:\n            \"Użyj niestandardowej ikony favicon dla karty przeglądarki.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Linki w stopce\",\n        description: \"Dostosuj linki wyświetlane w stopce paska bocznego.\",\n        icon: \"Ikona\",\n        link: \"Link\",\n      },\n      \"render-html\": {\n        title: \"Renderowanie HTML w czacie\",\n        description:\n          \"Wyświetlanie odpowiedzi w formacie HTML w odpowiedziach asystenta.\\nMoże to prowadzić do znacznie wyższej jakości odpowiedzi, ale również wiąże się z potencjalnymi zagrożeniami bezpieczeństwa.\",\n      },\n    },\n  },\n  api: {\n    title: \"Klucze API\",\n    description:\n      \"Klucze API umożliwiają dostęp do instancji AnythingLLM i zarządzanie nią.\",\n    link: \"Przeczytaj dokumentację API\",\n    generate: \"Generuj nowy klucz API\",\n    table: {\n      key: \"Klucz API\",\n      by: \"Utworzony przez\",\n      created: \"Utworzony o\",\n    },\n  },\n  llm: {\n    title: \"Preferencje LLM\",\n    description:\n      \"Tutaj skonfigurujesz dostawcę modeli językowych używanych do czatów i embeddingów. Upewnij się, że wszystkie klucze są aktualne i poprawne - bez tego AnythingLLM nie będzie działać.\",\n    provider: \"Dostawca LLM\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Punkt końcowy usługi Azure\",\n        api_key: \"Klucz API\",\n        chat_deployment_name: \"Nazwa wdrożenia czatu\",\n        chat_model_token_limit: \"Limit tokenów modelu czatu\",\n        model_type: \"Typ modelu\",\n        default: \"Domyślne\",\n        reasoning: \"Uzasadnienie\",\n        model_type_tooltip:\n          \"Jeśli w Państwa systemie używany jest model rozumowania (np. o1, o1-mini, o3-mini), ustaw tę opcję na „Rozumowanie”. W przeciwnym razie, Państwa zapytania w czacie mogą nie działać.\",\n      },\n    },\n  },\n  transcription: {\n    title: \"Preferencje modelu transkrypcji\",\n    description:\n      \"Tutaj skonfigurujesz dostawcę modeli używanych do transkrypcji plików audio i wideo. Upewnij się, że klucze są poprawne - bez tego pliki audio nie będą transkrybowane.\",\n    provider: \"Dostawca usług transkrypcji\",\n    \"warn-start\":\n      \"Korzystanie z lokalnego modelu Whisper na komputerach z ograniczoną pamięcią RAM lub procesorem może spowodować przerwanie pracy AnythingLLM podczas przetwarzania plików multimedialnych.\",\n    \"warn-recommend\":\n      \"Zalecana konfiguracja to co najmniej 2 GB pamięci RAM, przesyłaj pliki <10 MB.\",\n    \"warn-end\":\n      \"Wbudowany model zostanie automatycznie pobrany przy pierwszym użyciu.\",\n  },\n  embedding: {\n    title: \"Preferencje dot. embeddingów\",\n    \"desc-start\":\n      \"W przypadku korzystania z LLM, który nie obsługuje natywnie silnika embeddingów - może być konieczna dodatkowa konfiguracja poświadczeń.\",\n    \"desc-end\":\n      \"Embedding to proces przekształcania tekstu na wektory. Poświadczenia są wymagane do przekształcenia plików i tekstu za pomocą wybranego modelu.\",\n    provider: {\n      title: \"Model używany do tworzenia embeddingów\",\n    },\n  },\n  text: {\n    title: \"Preferencje dot. podziału tekstu i dzielenia na fragmenty\",\n    \"desc-start\":\n      \"Czasami może zaistnieć potrzeba zmiany domyślnego sposobu, w jaki nowe dokumenty są dzielone i fragmentowane przed wstawieniem ich do wektorowej bazy danych.\",\n    \"desc-end\":\n      \"Powinieneś modyfikować to ustawienie tylko wtedy, gdy rozumiesz, jak działa dzielenie tekstu i jakie są jego skutki uboczne.\",\n    size: {\n      title: \"Rozmiar fragmentu tekstu\",\n      description:\n        \"Jest to maksymalna długość znaków, które mogą występować w pojedynczym wektorze.\",\n      recommend: \"Maksymalna długość modelu osadzonego wynosi\",\n    },\n    overlap: {\n      title: \"Nakładanie się fragmentów tekstu\",\n      description:\n        \"Jest to maksymalna liczba nakładających się znaków, które występuje podczas fragmentacji między dwoma sąsiednimi fragmentami tekstu.\",\n    },\n  },\n  vector: {\n    title: \"Wektorowa baza danych\",\n    description:\n      \"Tutaj skonfigurujesz wektorową bazę danych dla AnythingLLM. Upewnij się, że wszystkie ustawienia są poprawne.\",\n    provider: {\n      title: \"Wektorowa baza danych\",\n      description: \"LanceDB nie wymaga żadnej konfiguracji.\",\n    },\n  },\n  embeddable: {\n    title: \"Osadzone widżety czatu\",\n    description:\n      \"Osadzane widżety czatu to publiczne interfejsy czatu, które są powiązane z pojedynczym obszarem roboczym. Umożliwiają one tworzenie przestrzeni roboczych, które następnie można publikować na całym świecie.\",\n    create: \"Utwórz osadzenie\",\n    table: {\n      workspace: \"Obszar roboczy\",\n      chats: \"Wysłane wiadomości\",\n      active: \"Aktywne domeny\",\n      created: \"Utworzony\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Historia czatu\",\n    export: \"Eksport\",\n    description:\n      \"Są to wszystkie czaty i wiadomości z dowolnego opublikowanego widżetu czatu.\",\n    table: {\n      embed: \"Obszar roboczy\",\n      sender: \"Nadawca\",\n      message: \"Wiadomość\",\n      response: \"Odpowiedź\",\n      at: \"Wysłane o\",\n    },\n  },\n  event: {\n    title: \"Dzienniki zdarzeń\",\n    description: \"Wyświetl wszystkie akcje i zdarzenia.\",\n    clear: \"Wyczyść dzienniki zdarzeń\",\n    table: {\n      type: \"Typ zdarzenia\",\n      user: \"Użytkownik\",\n      occurred: \"Wystąpiło o\",\n    },\n  },\n  privacy: {\n    title: \"Prywatność i obsługa danych\",\n    description:\n      \"Jest to konfiguracja sposobu, w jaki połączeni dostawcy zewnętrzni i AnythingLLM przetwarzają dane użytkownika.\",\n    anonymous: \"Włączona anonimowa telemetria\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Wyszukaj źródła danych\",\n    \"no-connectors\": \"Nie znaleziono źródeł danych.\",\n    obsidian: {\n      vault_location: \"Lokalizacja folderu Obsidian\",\n      vault_description:\n        \"Wybierz folder Obsidian, aby zaimportować wszystkie notatki i ich połączenia.\",\n      selected_files: \"Znaleziono {{count}} plików markdown\",\n      importing: \"Importowanie folderu Obsidian...\",\n      import_vault: \"Importuj folder\",\n      processing_time:\n        \"Może to trochę potrwać w zależności od wielkości folderu.\",\n      vault_warning:\n        \"Aby uniknąć konfliktów, upewnij się, że folder Obsidian nie jest aktualnie otwarty.\",\n    },\n    github: {\n      name: \"GitHub Repo\",\n      description:\n        \"Zaimportuj całe publiczne lub prywatne repozytorium GitHub jednym kliknięciem.\",\n      URL: \"Adres URL repozytorium GitHub\",\n      URL_explained: \"Adres URL repozytorium GitHub, które chcesz pobrać.\",\n      token: \"Token dostępu GitHub\",\n      optional: \"opcjonalny\",\n      token_explained: \"Token dostępu, zapobiegający ograniczeniu szybkości.\",\n      token_explained_start: \"Bez \",\n      token_explained_link1: \"Osobistego tokenu dostępu \",\n      token_explained_middle:\n        \"API GitHub może ograniczać liczbę plików, które mogą zostać pobrane ze względu na limity szybkości. Utwórz\",\n      token_explained_link2: \" tymczasowy token dostępu\",\n      token_explained_end: \" aby uniknąć tego problemu.\",\n      ignores: \"Ignorowane pliki\",\n      git_ignore:\n        \"Lista w formacie .gitignore. Naciśnij enter po każdym wpisie, aby go zapisać.\",\n      task_explained:\n        \"Po zakończeniu wszystkie pliki będą dostępne do osadzenia w obszarach roboczych w selektorze dokumentów.\",\n      branch: \"Gałąź, z której mają być pobierane pliki.\",\n      branch_loading: \"-- ładowanie dostępnych gałęzi\",\n      branch_explained: \"Gałąź, z której mają być pobierane pliki.\",\n      token_information:\n        \"Bez wypełnienia <b>GitHub Access Token</b> ten konektor danych będzie mógł pobierać tylko pliki <b>z głównego katalogu</b> repozytorium ze względu na ograniczenia szybkości publicznego API GitHub.\",\n      token_personal:\n        \"Uzyskaj bezpłatny osobisty token dostępu do konta GitHub tutaj.\",\n    },\n    gitlab: {\n      name: \"GitLab Repo\",\n      description:\n        \"Zaimportuj całe publiczne lub prywatne repozytorium GitLab jednym kliknięciem.\",\n      URL: \"Adres URL repozytorium GitLab\",\n      URL_explained: \"Adres URL repozytorium GitLab, które chcesz pobrać.\",\n      token: \"Token dostępu GitLab\",\n      optional: \"opcjonalny\",\n      token_description:\n        \"Wybierz dodatkowe elementy do pobrania z interfejsu API GitLab.\",\n      token_explained_start: \"Bez \",\n      token_explained_link1: \"Osobistego tokenu dostępu \",\n      token_explained_middle:\n        \"API GitLab może ograniczyć liczbę plików, które mogą zostać pobrane ze względu na limity szybkości. Utwórz\",\n      token_explained_link2: \" tymczasowy token dostępu\",\n      token_explained_end: \" aby uniknąć tego problemu.\",\n      fetch_issues: \"Pobierz Issues jako Dokumenty\",\n      ignores: \"Ignorowane pliki\",\n      git_ignore:\n        \"Lista w formacie .gitignore. Naciśnij enter po każdym wpisie, aby go zapisać.\",\n      task_explained:\n        \"Po zakończeniu wszystkie pliki będą dostępne do osadzenia w obszarach roboczych w selektorze dokumentów.\",\n      branch: \"Gałąź, z której chcesz pobierać pliki\",\n      branch_loading: \"-- ładowanie dostępnych gałęzi\",\n      branch_explained: \"Gałąź, z której mają być pobierane pliki.\",\n      token_information:\n        \"Bez wypełnienia <b>GitLab Access Token</b> ten konektor danych będzie mógł pobierać tylko pliki <b>z głównego katalogu</b> repozytorium ze względu na ograniczenia szybkości publicznego API GitLab.\",\n      token_personal:\n        \"Uzyskaj bezpłatny osobisty token dostępu do konta GitLab tutaj.\",\n    },\n    youtube: {\n      name: \"Transkrypcja YouTube\",\n      description: \"Zaimportuj transkrypcję całego filmu YouTube z łącza.\",\n      URL: \"Adres URL filmu YouTube\",\n      URL_explained_start:\n        \"Wprowadź adres URL dowolnego filmu z YouTube, aby pobrać jego transkrypcję. Film musi zawierać\",\n      URL_explained_link: \" napisy\",\n      URL_explained_end: \".\",\n      task_explained:\n        \"Po zakończeniu transkrypcja będzie dostępna do osadzenia w obszarach roboczych w selektorze dokumentów.\",\n    },\n    \"website-depth\": {\n      name: \"Masowe pobieranie zawartości web\",\n      description:\n        \"Pobiera treści ze strony internetowej wraz z jej podstronami do określonej głębokości (liczby podstron).\",\n      URL: \"Adres URL witryny\",\n      URL_explained:\n        \"Adres URL strony internetowej, z której chcesz pobrać treści.\",\n      depth: \"Głębokość przeszukiwania\",\n      depth_explained:\n        \"Określa ile poziomów podstron zostanie przeszukanych począwszy od głównego adresu URL.\",\n      max_pages: \"Maksymalna liczba stron\",\n      max_pages_explained: \"Maksymalna liczba stron do pobrania.\",\n      task_explained:\n        \"Po zakończeniu cała pobrana zawartość będzie dostępna do dodania w obszarach roboczych w oknie dodawania danych.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Zaimportuj całą stronę Confluence jednym kliknięciem.\",\n      deployment_type: \"Rodzaj wdrożenia Confluence\",\n      deployment_type_explained:\n        \"Określ, czy instancja Confluence jest hostowana w chmurze Atlassian, czy samodzielnie.\",\n      base_url: \"Bazowy adres URL Confluence\",\n      base_url_explained: \"Jest to podstawowy adres URL Confluence.\",\n      space_key: \"Klucz przestrzeni Confluence\",\n      space_key_explained:\n        \"Jest to klucz instancji Confluence. Zwykle zaczyna się od ~\",\n      username: \"Nazwa użytkownika Confluence\",\n      username_explained: \"Nazwa użytkownika Confluence\",\n      auth_type: \"Typ autoryzacji Confluence\",\n      auth_type_explained:\n        \"Wybierz typ uwierzytelniania, którego chcesz użyć do uzyskania dostępu do Confluence.\",\n      auth_type_username: \"Nazwa użytkownika i token dostępu\",\n      auth_type_personal: \"Osobisty token dostępu\",\n      token: \"Token dostępu do Confluence\",\n      token_explained_start:\n        \"W celu uwierzytelnienia należy podać token dostępu. Token dostępu można wygenerować \",\n      token_explained_link: \"tutaj\",\n      token_desc: \"Token dostępu\",\n      pat_token: \"Osobisty token dostępu do Confluence\",\n      pat_token_explained: \"Osobisty token dostępu do Confluence.\",\n      task_explained:\n        \"Po zakończeniu zawartość strony będzie dostępna do osadzenia w obszarach roboczych w selektorze dokumentów.\",\n      bypass_ssl: \"Omijanie weryfikacji certyfikatu SSL\",\n      bypass_ssl_explained:\n        \"Włącz tę opcję, aby ominąć weryfikację certyfikatu SSL dla instancji Confluence, które są samodzielnie hostowane i posiadają certyfikat samodzielnie podpisany.\",\n    },\n    manage: {\n      documents: \"Dokumenty\",\n      \"data-connectors\": \"Źródła danych\",\n      \"desktop-only\":\n        \"Edycja tych ustawień jest dostępna tylko w wersji desktopowej. Aby kontynuować, przejdź do tej strony na komputerze.\",\n      dismiss: \"Odrzuć\",\n      editing: \"Edycja\",\n    },\n    directory: {\n      \"my-documents\": \"Moje dokumenty\",\n      \"new-folder\": \"Nowy folder\",\n      \"search-document\": \"Wyszukiwanie dokumentu\",\n      \"no-documents\": \"Brak dokumentów\",\n      \"move-workspace\": \"Przenieś do obszaru roboczego\",\n      \"delete-confirmation\":\n        \"Czy na pewno chcesz usunąć te pliki i foldery? Spowoduje to usunięcie plików z systemu i automatyczne usunięcie ich z istniejących obszarów roboczych. Działanie to nie jest odwracalne.\",\n      \"removing-message\":\n        \"Usuwanie dokumentów {{count}} i folderów {{folderCount}}. Proszę czekać.\",\n      \"move-success\": \"Pomyślnie przeniesiono {{count}} dokumentów.\",\n      no_docs: \"Brak dokumentów\",\n      select_all: \"Wybierz wszystko\",\n      deselect_all: \"Odznacz wszystko\",\n      remove_selected: \"Usuń wybrane\",\n      costs: \"*Jednorazowy koszt dodania danych\",\n      save_embed: \"Zapisz\",\n      \"total-documents_one\": \"{{count}} dokument\",\n      \"total-documents_other\": \"{{count}} dokumenty\",\n    },\n    upload: {\n      \"processor-offline\": \"Procesor dokumentów niedostępny\",\n      \"processor-offline-desc\":\n        \"Nie możemy teraz przesłać plików, ponieważ procesor dokumentów jest w trybie offline. Spróbuj ponownie później.\",\n      \"click-upload\": \"Kliknij, aby przesłać lub przeciągnij i upuść\",\n      \"file-types\":\n        \"obsługuje pliki tekstowe, csv, arkusze kalkulacyjne, pliki audio i wiele więcej!\",\n      \"or-submit-link\": \"lub prześlij link\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"Pobieranie...\",\n      \"fetch-website\": \"Pobierz zawartość strony\",\n      \"privacy-notice\":\n        \"Pliki zostaną przetworzone w obrębie danej instancji AnythingLLM. Pliki te nie będą udostępniane innym podmiotom.\",\n    },\n    pinning: {\n      what_pinning: \"Czym jest przypinanie dokumentów?\",\n      pin_explained_block1:\n        \"Kiedy <b>przypinasz</b> dokument w AnythingLLM, dodamy całą zawartość dokumentu do okna promptu, aby LLM mógł w pełni zrozumieć jego treść.\",\n      pin_explained_block2:\n        \"Działa to najlepiej w przypadku <b>dużych modeli kontekstowych</b> lub małych plików, które są krytyczne dla bazy wiedzy.\",\n      pin_explained_block3:\n        \"Jeśli domyślnie nie otrzymujesz pożądanych odpowiedzi z AnythingLLM, przypinanie jest świetnym sposobem na uzyskanie wyższej jakości odpowiedzi za jednym kliknięciem.\",\n      accept: \"Ok, rozumiem\",\n    },\n    watching: {\n      what_watching: \"Do czego służy oglądanie dokumentu?\",\n      watch_explained_block1:\n        \"Podczas <b>obserwowania</b> dokumentu w AnythingLLM będziemy <i>automatycznie</i> synchronizować zawartość dokumentu z jego oryginalnym źródłem w regularnych odstępach czasu. Spowoduje to automatyczną aktualizację zawartości w każdym obszarze roboczym, w którym ten plik jest zarządzany.\",\n      watch_explained_block2:\n        \"Ta funkcja obsługuje obecnie treści online i nie będzie dostępna dla dokumentów przesyłanych ręcznie.\",\n      watch_explained_block3_start:\n        \"Możesz zarządzać obserwowanymi dokumentami z poziomu\",\n      watch_explained_block3_link: \"Menedżer plików\",\n      watch_explained_block3_end: \" widok administratora.\",\n      accept: \"Ok, rozumiem\",\n    },\n  },\n  chat_window: {\n    attachments_processing: \"Załączniki są przetwarzane. Proszę czekać...\",\n    send_message: \"Wyślij wiadomość\",\n    attach_file: \"Dołącz plik do tego czatu\",\n    text_size: \"Zmiana rozmiaru tekstu.\",\n    microphone: \"Wypowiedz swoją prośbę.\",\n    send: \"Wyślij wiadomość do obszaru roboczego\",\n    tts_speak_message: \"Wypowiedz komunikat głosowo\",\n    copy: \"Kopiuj\",\n    regenerate: \"Generuj ponownie\",\n    regenerate_response: \"Wygeneruj ponownie odpowiedź\",\n    good_response: \"Dobra odpowiedź\",\n    more_actions: \"Więcej działań\",\n    fork: \"Utwórz rozgałęzienie\",\n    delete: \"Usuń\",\n    cancel: \"Anuluj\",\n    edit_prompt: \"Edytuj prompt\",\n    edit_response: \"Edytuj odpowiedź\",\n    preset_reset_description: \"Wyczyść historię czatu i rozpocznij nowy czat\",\n    add_new_preset: \" Dodaj nowe polecenie slash\",\n    command: \"Polecenie\",\n    your_command: \"twoje-polecenie\",\n    placeholder_prompt: \"Ta treść zostanie dodana przed Twoim pytaniem.\",\n    description: \"Opis\",\n    placeholder_description: \"Stwórz opis swojego polecenia slash.\",\n    save: \"Zapisz\",\n    small: \"Mały\",\n    normal: \"Normalny\",\n    large: \"Duży\",\n    workspace_llm_manager: {\n      search: \"Wyszukaj dostawców LLM\",\n      loading_workspace_settings: \"Ładowanie ustawień obszaru roboczego...\",\n      available_models: \"Dostępne modele dla {{provider}}\",\n      available_models_description:\n        \"Wybierz model, który będzie używany w tym obszarze roboczym.\",\n      save: \"Użyj tego modelu\",\n      saving: \"Ustawienie modelu jako domyślnego dla obszaru roboczego...\",\n      missing_credentials: \"Temu dostawcy brakuje poświadczeń!\",\n      missing_credentials_description:\n        \"Kliknij, aby skonfigurować poświadczenia\",\n    },\n    submit: \"Prześlij\",\n    edit_info_user:\n      '\"Wyślij\" powoduje ponowne wygenerowanie odpowiedzi przez sztuczną inteligencję. \"Zapisz\" aktualizuje tylko Twoje wiadomości.',\n    edit_info_assistant:\n      \"Twoje zmiany zostaną zapisane bezpośrednio w tej odpowiedzi.\",\n    see_less: \"Zobacz mniej\",\n    see_more: \"Zobacz więcej\",\n    tools: \"Narzędzia\",\n    browse: \"Przeglądaj\",\n    text_size_label: \"Rozmiar czcionki\",\n    select_model: \"Wybierz model\",\n    sources: \"Źródła\",\n    document: \"Dokument\",\n    similarity_match: \"mecz\",\n    source_count_one: \"{{count}} – odniesienie\",\n    source_count_other: \"{{count}} – odnośniki\",\n    preset_exit_description: \"Zakończ bieżącą sesję z przedstawicielem\",\n    add_new: \"Dodaj nowe\",\n    edit: \"Edytuj\",\n    publish: \"Opublikować\",\n    stop_generating: \"Przestań generować odpowiedź\",\n    pause_tts_speech_message: \"Wstrzymać odtwarzanie mowy z wiadomości\",\n    slash_commands: \"Polecenia skrótowe\",\n    agent_skills: \"Umiejętności agenta\",\n    manage_agent_skills: \"Zarządzanie umiejętnościami agentów\",\n    agent_skills_disabled_in_session:\n      \"Nie można modyfikować umiejętności podczas trwającej sesji. Aby zakończyć sesję, należy najpierw użyć komendy /exit.\",\n    start_agent_session: \"Rozpocznij sesję dla agenta\",\n    use_agent_session_to_use_tools:\n      \"Możesz korzystać z narzędzi w czacie, inicjując sesję z agentem, wpisując '@agent' na początku swojego zapytania.\",\n  },\n  profile_settings: {\n    edit_account: \"Edytuj konto\",\n    profile_picture: \"Zdjęcie profilowe\",\n    remove_profile_picture: \"Usuń zdjęcie profilowe\",\n    username: \"Nazwa użytkownika\",\n    new_password: \"Nowe hasło\",\n    password_description: \"Hasz do 8 znaków.\",\n    cancel: \"Anuluj\",\n    update_account: \"Zaktualizuj konto\",\n    theme: \"Preferencje dotyczące motywu\",\n    language: \"Preferowany język\",\n    failed_upload: \"Nie udało się przesłać zdjęcia profilowego: {{error}}.\",\n    upload_success: \"Dodano zdjęcie profilowe.\",\n    failed_remove: \"Nie udało się usunąć zdjęcia profilowego: {{error}}.\",\n    profile_updated: \"Profil został zaktualizowany.\",\n    failed_update_user: \"Nie udało się zaktualizować użytkownika: {{error}}.\",\n    account: \"Konto\",\n    support: \"Wsparcie\",\n    signout: \"Wyloguj się\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Skróty klawiaturowe\",\n    shortcuts: {\n      settings: \"Otwórz ustawienia\",\n      workspaceSettings: \"Otwórz ustawienia bieżącego obszaru roboczego\",\n      home: \"Przejdź do strony głównej\",\n      workspaces: \"Zarządzanie obszarami roboczymi\",\n      apiKeys: \"Ustawienia kluczy API\",\n      llmPreferences: \"Preferencje LLM\",\n      chatSettings: \"Ustawienia czatu\",\n      help: \"Pokaż pomoc dotyczącą skrótów klawiaturowych\",\n      showLLMSelector: \"Pokaż selektor LLM obszaru roboczego\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Sukces!\",\n        success_description:\n          \"Twoja instrukcja systemowa została opublikowana w centrum społeczności!\",\n        success_thank_you: \"Dziękujemy za udostępnienie społeczności!\",\n        view_on_hub: \"Zobacz w Community Hub\",\n        modal_title: \"Opublikuj instrukcję systemową\",\n        name_label: \"Nazwa\",\n        name_description: \"Jest to wyświetlana nazwa instrukcji systemowej.\",\n        name_placeholder: \"Moja instrukcja systemowa\",\n        description_label: \"Opis\",\n        description_description:\n          \"To jest opis instrukcji systemowej. Użyj tego, aby opisać cel instrukcji systemowej.\",\n        tags_label: \"Tagi\",\n        tags_description:\n          \"Tagi służą do oznaczania instrukcji systemowych w celu łatwiejszego wyszukiwania. Można dodać wiele tagów. Maksymalnie 5 tagów. Maksymalnie 20 znaków na tag.\",\n        tags_placeholder: \"Wpisz i naciśnij Enter, aby dodać tagi\",\n        visibility_label: \"Widoczność\",\n        public_description:\n          \"Publiczne instrukcje systemowe są widoczne dla wszystkich.\",\n        private_description:\n          \"Prywatne instrukcje systemowe są widoczne tylko dla użytkownika.\",\n        publish_button: \"Opublikuj w Community Hub\",\n        submitting: \"Publikacja...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Jest to rzeczywista instrukcja systemowa, która będzie używana do kierowania LLM.\",\n        prompt_placeholder: \"Wprowadź tutaj instrukcję systemową...\",\n      },\n      agent_flow: {\n        success_title: \"Sukces!\",\n        success_description:\n          \"Twój Agent Flow został opublikowany w Community Hub!\",\n        success_thank_you: \"Dziękujemy za udostępnienie społeczności!\",\n        view_on_hub: \"Zobacz w Community Hub\",\n        modal_title: \"Publikowanie przepływu agenta\",\n        name_label: \"Nazwa\",\n        name_description: \"Jest to wyświetlana nazwa przepływu agenta.\",\n        name_placeholder: \"Mój przepływ agenta\",\n        description_label: \"Opis\",\n        description_description:\n          \"To jest opis przepływu agenta. Użyj tego, aby opisać cel przepływu agenta.\",\n        tags_label: \"Tagi\",\n        tags_description:\n          \"Tagi służą do oznaczania przepływów agentów w celu łatwiejszego wyszukiwania. Można dodać wiele tagów. Maksymalnie 5 tagów. Maksymalnie 20 znaków na tag.\",\n        tags_placeholder: \"Wpisz i naciśnij Enter, aby dodać tagi\",\n        visibility_label: \"Widoczność\",\n        submitting: \"Publikacja...\",\n        submit: \"Opublikuj w Community Hub\",\n        privacy_note:\n          \"Przepływy agenta są zawsze przesyłane jako prywatne, aby chronić wszelkie poufne dane. Widoczność można zmienić w Community Hub po opublikowaniu. Przed opublikowaniem upewnij się, że przepływ nie zawiera żadnych poufnych lub prywatnych informacji.\",\n      },\n      slash_command: {\n        success_title: \"Sukces!\",\n        success_description:\n          \"Twoje polecenie slash zostało opublikowane w centrum społeczności!\",\n        success_thank_you: \"Dziękujemy za udostępnienie społeczności!\",\n        view_on_hub: \"Zobacz w Community Hub\",\n        modal_title: \"Publikuj polecenie slash\",\n        name_label: \"Nazwa\",\n        name_description: \"Jest to wyświetlana nazwa polecenia slash.\",\n        name_placeholder: \"Moje polecenie slash\",\n        description_label: \"Opis\",\n        description_description:\n          \"To jest opis polecenia slash. Użyj tego, aby opisać cel polecenia slash.\",\n        tags_label: \"Tagi\",\n        tags_description:\n          \"Tagi są używane do oznaczania poleceń slash w celu łatwiejszego wyszukiwania. Można dodać wiele tagów. Maksymalnie 5 tagów. Maksymalnie 20 znaków na tag.\",\n        tags_placeholder: \"Wpisz i naciśnij Enter, aby dodać tagi\",\n        visibility_label: \"Widoczność\",\n        public_description:\n          \"Publiczne polecenia slash są widoczne dla wszystkich.\",\n        private_description:\n          \"Prywatne polecenia slash są widoczne tylko dla użytkownika.\",\n        publish_button: \"Opublikuj w Community Hub\",\n        submitting: \"Publikacja...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Jest to tekst zachęty, który zostanie użyty po uruchomieniu polecenia slash.\",\n        prompt_placeholder: \"Wprowadź tutaj swój prompt...\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Wymagane uwierzytelnienie\",\n          description:\n            \"Przed opublikowaniem elementów należy uwierzytelnić się w centrum społeczności AnythingLLM.\",\n          button: \"Połączenie z centrum społeczności\",\n        },\n      },\n    },\n  },\n  security: {\n    title: \"Bezpieczeństwo\",\n    multiuser: {\n      title: \"Tryb wielu użytkowników\",\n      description:\n        \"Skonfiguruj swoją instancję do obsługi zespołu, aktywując tryb wielu użytkowników.\",\n      enable: {\n        \"is-enable\": \"Tryb wielu użytkowników jest włączony\",\n        enable: \"Włącz tryb wielu użytkowników\",\n        description:\n          \"Domyślnie będziesz jedynym administratorem. Jako administrator będziesz musiał utworzyć konta dla wszystkich nowych użytkowników lub administratorów. Nie zgub hasła, ponieważ tylko administrator może je zresetować.\",\n        username: \"Nazwa użytkownika konta administratora\",\n        password: \"Hasło konta administratora\",\n      },\n    },\n    password: {\n      title: \"Ochrona hasłem\",\n      description:\n        \"Chroń swoją instancję AnythingLLM hasłem. Jeśli go zapomnisz, nie ma metody odzyskiwania, więc upewnij się, że zapisałeś to hasło.\",\n      \"password-label\": \"Hasło instancji\",\n    },\n  },\n  home: {\n    welcome: \"Witamy\",\n    chooseWorkspace: \"Wybierz obszar roboczy, aby rozpocząć czat!\",\n    notAssigned:\n      \"Nie jesteś przypisany do żadnego obszaru roboczego.\\nSkontaktuj się z administratorem, aby poprosić o dostęp do obszaru roboczego.\",\n    goToWorkspace: 'Przejdź do obszaru roboczego \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/pt_BR/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"Bem-vindo ao\",\n      getStarted: \"Começar\",\n    },\n    llm: {\n      title: \"Preferência de LLM\",\n      description:\n        \"AnythingLLM funciona com vários provedores de LLM. Este será o serviço que lidará com os chats.\",\n    },\n    userSetup: {\n      title: \"Configuração do Usuário\",\n      description: \"Configure suas preferências de usuário.\",\n      howManyUsers: \"Quantos usuários usarão esta instância?\",\n      justMe: \"Apenas eu\",\n      myTeam: \"Minha equipe\",\n      instancePassword: \"Senha da Instância\",\n      setPassword: \"Deseja configurar uma senha?\",\n      passwordReq: \"Senhas devem ter pelo menos 8 caracteres.\",\n      passwordWarn:\n        \"É importante salvar esta senha pois não há método de recuperação.\",\n      adminUsername: \"Nome de usuário admin\",\n      adminPassword: \"Senha de admin\",\n      adminPasswordReq: \"Senhas devem ter pelo menos 8 caracteres.\",\n      teamHint:\n        \"Por padrão, você será o único admin. Após a configuração, você poderá convidar outros usuários ou admins. Não perca sua senha, pois apenas admins podem redefini-la.\",\n    },\n    data: {\n      title: \"Privacidade de Dados\",\n      description:\n        \"Estamos comprometidos com transparência e controle sobre seus dados pessoais.\",\n      settingsHint:\n        \"Estas configurações podem ser alteradas a qualquer momento.\",\n    },\n    survey: {\n      title: \"Bem-vindo ao AnythingLLM\",\n      description: \"Ajude-nos a melhorar o AnythingLLM. Opcional.\",\n      email: \"Qual seu email?\",\n      useCase: \"Como você usará o AnythingLLM?\",\n      useCaseWork: \"Para trabalho\",\n      useCasePersonal: \"Uso pessoal\",\n      useCaseOther: \"Outro\",\n      comment: \"Como você conheceu o AnythingLLM?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube, etc. - Conte como nos encontrou!\",\n      skip: \"Pular Pesquisa\",\n      thankYou: \"Obrigado pelo seu feedback!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Nome do Workspace\",\n    user: \"Usuário\",\n    selection: \"Seleção de Modelo\",\n    saving: \"Salvando...\",\n    save: \"Salvar alterações\",\n    previous: \"Página Anterior\",\n    next: \"Próxima Página\",\n    optional: \"Opcional\",\n    yes: \"Sim\",\n    no: \"Não\",\n    search: \"Pesquisar\",\n    username_requirements:\n      \"O nome de usuário deve ter de 2 a 32 caracteres, começar com uma letra minúscula e conter apenas letras minúsculas, números, sublinhados, hífens e pontos.\",\n    on: \"Sobre\",\n    none: \"Nenhum\",\n    stopped: \"Parado\",\n    loading: \"Carregando\",\n    refresh: \"Atualizar\",\n  },\n  settings: {\n    title: \"Configurações da Instância\",\n    invites: \"Convites\",\n    users: \"Usuários\",\n    workspaces: \"Workspaces\",\n    \"workspace-chats\": \"Chats do Workspace\",\n    customization: \"Personalização\",\n    interface: \"Preferências de UI\",\n    branding: \"Marca e Etiqueta Branca\",\n    chat: \"Chat\",\n    \"api-keys\": \"API de Desenvolvedor\",\n    llm: \"LLM\",\n    transcription: \"Transcrição\",\n    embedder: \"Vinculador\",\n    \"text-splitting\": \"Divisor de Texto\",\n    \"voice-speech\": \"Voz e Fala\",\n    \"vector-database\": \"Banco de Dados Vetorial\",\n    embeds: \"Vinculador de Chat\",\n    security: \"Segurança\",\n    \"event-logs\": \"Logs de Eventos\",\n    privacy: \"Privacidade e Dados\",\n    \"ai-providers\": \"Provedores de IA\",\n    \"agent-skills\": \"Habilidades de Agente\",\n    admin: \"Admin\",\n    tools: \"Ferramentas\",\n    \"system-prompt-variables\": \"Variáveis de Prompt\",\n    \"experimental-features\": \"Recursos Experimentais\",\n    contact: \"Suporte\",\n    \"browser-extension\": \"Extensão de Navegador\",\n    \"mobile-app\": \"AnythingLLM Mobile\",\n    \"community-hub\": {\n      title: \"Centro Comunitário\",\n      trending: \"Explore as tendências\",\n      \"your-account\": \"Sua Conta\",\n      \"import-item\": \"Importar Item\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Bem-vindo ao\",\n      \"placeholder-username\": \"Nome de usuário\",\n      \"placeholder-password\": \"Senha\",\n      login: \"Login\",\n      validating: \"Validando...\",\n      \"forgot-pass\": \"Esqueci a senha\",\n      reset: \"Redefinir\",\n    },\n    \"sign-in\": \"Acesse sua {{appName}} conta.\",\n    \"password-reset\": {\n      title: \"Redefinição de Senha\",\n      description:\n        \"Forneça as informações necessárias para redefinir sua senha.\",\n      \"recovery-codes\": \"Códigos de Recuperação\",\n      \"back-to-login\": \"Voltar ao Login\",\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Criar um Agente\",\n      editWorkspace: \"Editar o Espaço de Trabalho\",\n      uploadDocument: \"Enviar um documento\",\n    },\n    greeting: \"Como posso ajudá-lo hoje?\",\n  },\n  \"new-workspace\": {\n    title: \"Novo Workspace\",\n    placeholder: \"Meu Workspace\",\n  },\n  \"workspaces—settings\": {\n    general: \"Configurações Gerais\",\n    chat: \"Configurações de Chat\",\n    vector: \"Banco de Dados Vetorial\",\n    members: \"Membros\",\n    agent: \"Configuração de Agente\",\n  },\n  general: {\n    vector: {\n      title: \"Contagem de Vetores\",\n      description: \"Número total de vetores no seu banco de dados.\",\n    },\n    names: {\n      description: \"Isso altera apenas o nome exibido do seu workspace.\",\n    },\n    message: {\n      title: \"Sugestões de Chat\",\n      description:\n        \"Personalize as mensagens sugeridas aos usuários do workspace.\",\n      add: \"Adicionar mensagem\",\n      save: \"Salvar Mensagens\",\n      heading: \"Explique para mim\",\n      body: \"os benefícios do AnythingLLM\",\n    },\n    delete: {\n      title: \"Excluir Workspace\",\n      description:\n        \"Exclua este workspace e todos seus dados. Isso afetará todos os usuários.\",\n      delete: \"Excluir Workspace\",\n      deleting: \"Excluindo Workspace...\",\n      \"confirm-start\": \"Você está prestes a excluir todo o\",\n      \"confirm-end\":\n        \"workspace. Isso removerá todos os vetores do banco de dados.\\n\\nOs arquivos originais permanecerão intactos. Esta ação é irreversível.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Provedor de LLM\",\n      description:\n        \"O provedor e modelo específico que será usado neste workspace. Por padrão, usa as configurações do sistema.\",\n      search: \"Buscar todos provedores\",\n    },\n    model: {\n      title: \"Modelo de Chat\",\n      description:\n        \"O modelo específico para este workspace. Se vazio, usará a preferência do sistema.\",\n    },\n    mode: {\n      title: \"Modo de Chat\",\n      chat: {\n        title: \"Chat\",\n        description:\n          'fornecerá respostas com base no conhecimento geral do LLM e no contexto do documento encontrado.<br />Você precisará usar o comando \"@agent\" para utilizar as ferramentas.',\n      },\n      query: {\n        title: \"Consulta\",\n        description:\n          'fornecerá respostas <b>apenas</b> caso o contexto do documento seja encontrado.<br />Você precisará usar o comando \"@agent\" para utilizar as ferramentas.',\n      },\n      automatic: {\n        title: \"Automóvel\",\n        description:\n          'utilizará automaticamente as ferramentas, se o modelo e o provedor suportarem a chamada nativa de ferramentas. Se a chamada nativa de ferramentas não for suportada, você precisará usar o comando \"@agent\" para utilizar as ferramentas.',\n      },\n    },\n    history: {\n      title: \"Histórico de Chat\",\n      \"desc-start\":\n        \"Número de chats anteriores que serão incluídos na memória de curto prazo.\",\n      recommend: \"Recomendado: 20. \",\n      \"desc-end\":\n        \"Valores acima de 45 podem causar falhas dependendo do tamanho das mensagens.\",\n    },\n    prompt: {\n      title: \"Prompt de Sistema\",\n      description:\n        \"O prompt usado neste workspace. Defina o contexto e instruções para a IA gerar respostas relevantes e precisas.\",\n      history: {\n        title: \"Histórico de Prompts\",\n        clearAll: \"Limpar Tudo\",\n        noHistory: \"Nenhum histórico disponível\",\n        restore: \"Restaurar\",\n        delete: \"Excluir\",\n        deleteConfirm: \"Tem certeza que deseja excluir este item?\",\n        clearAllConfirm:\n          \"Tem certeza que deseja limpar todo o histórico? Esta ação é irreversível.\",\n        expand: \"Expandir\",\n        publish: \"Publicar no Hub\",\n      },\n    },\n    refusal: {\n      title: \"Modo Resposta de recusa\",\n      \"desc-start\": \"Quando\",\n      query: \"consulta\",\n      \"desc-end\":\n        \"modo, você pode definir uma resposta personalizada quando nenhum contexto for encontrado.\",\n      \"tooltip-title\": \"Resposta de Recusa\",\n      \"tooltip-description\":\n        \"Configure uma mensagem personalizada quando o sistema não conseguir responder baseado no contexto disponível.\",\n    },\n    temperature: {\n      title: \"Temperatura do LLM\",\n      \"desc-start\": 'Controla o nível de \"criatividade\" das respostas.',\n      \"desc-end\":\n        \"Valores mais altos geram respostas mais criativas, mas para alguns modelos podem se tornar incoerentes.\",\n      hint: \"Cada modelo LLM tem faixas de valores válidos. Consulte seu provedor.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Identificador do banco de dados\",\n    snippets: {\n      title: \"Máximo de Trechos\",\n      description:\n        \"Controla a quantidade máxima de trechos de contexto enviados ao LLM por chat.\",\n      recommend: \"Recomendado: 4\",\n    },\n    doc: {\n      title: \"Limiar de similaridade\",\n      description:\n        \"Pontuação mínima para uma fonte ser considerada relevante para o chat. Valores mais altos exigem maior similaridade.\",\n      zero: \"Sem restrição\",\n      low: \"Baixo (≥ .25)\",\n      medium: \"Médio (≥ .50)\",\n      high: \"Alto (≥ .75)\",\n    },\n    reset: {\n      reset: \"Resetar Banco de Dados\",\n      resetting: \"Limpando vetores...\",\n      confirm:\n        \"Você está prestes a resetar o banco de dados deste workspace. Isso removerá todos os vetores atuais.\\n\\nOs arquivos originais permanecerão intactos. Esta ação é irreversível.\",\n      error: \"Falha ao resetar o banco de dados!\",\n      success: \"Banco de dados resetado com sucesso!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"O desempenho de LLMs sem suporte a tool-calling varia conforme as capacidades do modelo. Algumas funcionalidades podem ser limitadas.\",\n    provider: {\n      title: \"Provedor LLM de Agente de Workspace\",\n      description:\n        \"O provedor LLM e modelo específico que será usado por este agente @agent deste workspace.\",\n    },\n    mode: {\n      chat: {\n        title: \"Modelo de Chat para Agente de workspace\",\n        description:\n          \"O modelo de chat específico para o agente @agent deste workspace.\",\n      },\n      title: \"Modelo para Agente de workspace\",\n      description:\n        \"O modelo LLM específico que será usado pelo agente @agent deste workspace.\",\n      wait: \"-- aguardando modelos --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG & memória longa duração\",\n        description:\n          'Permite ao agente usar documentos locais para responder suas perguntas ou perguntar ao agente \"lembrar\" conteúdos de sua memória de longa duração.',\n      },\n      view: {\n        title: \"Visualizar & resumir\",\n        description:\n          \"Permite ao agente listar e resumir conteúdos guardados dos arquivos do workspace.\",\n      },\n      scrape: {\n        title: \"Extrair sites\",\n        description:\n          \"Permite ao agente visitar e extrair conteúdo de websites.\",\n      },\n      generate: {\n        title: \"Gerar gráficos\",\n        description:\n          \"Permite ao agente padrão gerar diversos tipos de gráficos a partir de dados armazenados ou informados no chat.\",\n      },\n      save: {\n        title: \"Gerar & salvar arquivos\",\n        description: \"Permite ao agente gerar e salvar arquivos no navegador.\",\n      },\n      web: {\n        title: \"Busca na web\",\n        description:\n          \"Permita que seu agente acesse a web para responder às suas perguntas, conectando-se a um provedor de pesquisa na web (SERP).\",\n      },\n      sql: {\n        title: \"Conector SQL\",\n        description:\n          \"Permita que seu agente utilize o SQL para responder às suas perguntas, conectando-se a diversos provedores de bancos de dados SQL.\",\n      },\n      default_skill:\n        \"Por padrão, essa habilidade está ativada, mas você pode desativá-la se não quiser que ela esteja disponível para o agente.\",\n    },\n    mcp: {\n      title: \"Servidores MCP\",\n      \"loading-from-config\":\n        \"Carregar servidores MCP a partir do arquivo de configuração\",\n      \"learn-more\": \"Saiba mais sobre os servidores MCP.\",\n      \"no-servers-found\": \"Nenhum servidor MCP encontrado.\",\n      \"tool-warning\":\n        \"Para obter o melhor desempenho, considere desativar as ferramentas desnecessárias para preservar o contexto.\",\n      \"stop-server\": \"Pare o servidor MCP\",\n      \"start-server\": \"Iniciar o servidor MCP\",\n      \"delete-server\": \"Excluir o servidor MCP\",\n      \"tool-count-warning\":\n        \"Este servidor MCP tem as seguintes ferramentas habilitadas: {{count}}, que consumirão contexto em cada conversa.</b> Considere desativar as ferramentas indesejadas para economizar contexto.\",\n      \"startup-command\": \"Comando de inicialização\",\n      command: \"Ordem\",\n      arguments: \"Argumentos\",\n      \"not-running-warning\":\n        \"Este servidor MCP não está em funcionamento – pode estar parado ou estar apresentando um erro durante a inicialização.\",\n      \"tool-call-arguments\": \"Argumentos de chamada de ferramenta\",\n      \"tools-enabled\": \"ferramentas ativadas\",\n    },\n    settings: {\n      title: \"Configurações de Habilidades do Agente\",\n      \"max-tool-calls\": {\n        title: \"Número máximo de chamadas de ferramenta por resposta\",\n        description:\n          \"O número máximo de ferramentas que um agente pode encadear para gerar uma única resposta. Isso evita chamadas excessivas de ferramentas e loops infinitos.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Seleção Inteligente de Habilidades\",\n        \"beta-badge\": \"Beta\",\n        description:\n          \"Permita o uso ilimitado de ferramentas e reduza o consumo de tokens em até 80% por consulta — O AnythingLLM seleciona automaticamente as habilidades mais adequadas para cada solicitação.\",\n        \"max-tools\": {\n          title: \"Ferramentas Max\",\n          description:\n            \"O número máximo de ferramentas que podem ser selecionadas para cada consulta. Recomendamos definir este valor para modelos com contextos maiores.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Chats do Workspace\",\n    description:\n      \"Todos os chats registrados enviados por usuários, ordenados por data de criação.\",\n    export: \"Exportar\",\n    table: {\n      id: \"ID\",\n      by: \"Enviado Por\",\n      workspace: \"Workspace\",\n      prompt: \"Prompt\",\n      response: \"Resposta\",\n      at: \"Enviado Em\",\n    },\n  },\n  customization: {\n    interface: {\n      title: \"Preferências de UI\",\n      description: \"Defina suas preferências de interface.\",\n    },\n    branding: {\n      title: \"Marca & Etiqueta Branca\",\n      description: \"Personalize sua instância do AnythingLLM com sua marca.\",\n    },\n    chat: {\n      title: \"Chat\",\n      description: \"Defina preferências de chat.\",\n      auto_submit: {\n        title: \"Envio Automático\",\n        description: \"Envia automaticamente entrada de voz após silêncio.\",\n      },\n      auto_speak: {\n        title: \"Falar Respostas\",\n        description: \"Fala automaticamente as respostas da IA.\",\n      },\n      spellcheck: {\n        title: \"Verificação Ortográfica\",\n        description: \"Ativa/desativa verificação ortográfica no chat.\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Tema\",\n        description: \"Selecione seu tema de cores preferido.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Mostrar Barra\",\n        description: \"Ativa/desativa barra de rolagem no chat.\",\n      },\n      \"support-email\": {\n        title: \"Email de Suporte\",\n        description: \"Defina o email de suporte acessível aos usuários.\",\n      },\n      \"app-name\": {\n        title: \"Nome\",\n        description:\n          \"Defina um nome exibido na página de login para todos os usuários.\",\n      },\n      \"display-language\": {\n        title: \"Idioma\",\n        description:\n          \"Selecione o idioma preferido para a interface - quando houver traduções.\",\n      },\n      logo: {\n        title: \"Logo\",\n        description: \"Envie seu logo personalizado.\",\n        add: \"Adicionar logo\",\n        recommended: \"Tamanho recomendado: 800 x 200\",\n        remove: \"Remover\",\n        replace: \"Substituir\",\n      },\n      \"browser-appearance\": {\n        title: \"Aparência no Navegador\",\n        description: \"Personalize a aparência da aba e título no navegador.\",\n        tab: {\n          title: \"Título\",\n          description: \"Defina um título personalizado para a aba.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description: \"Use um favicon personalizado.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Itens do Rodapé\",\n        description:\n          \"Personalize os itens exibidos no rodapé da barra lateral.\",\n        icon: \"Ícone\",\n        link: \"Link\",\n      },\n      \"render-html\": {\n        title: \"Renderizar HTML no chat\",\n        description:\n          \"Renderizar respostas HTML nas respostas do assistente.\\nIsso pode resultar em uma qualidade de resposta muito maior, mas também pode levar a riscos potenciais de segurança.\",\n      },\n    },\n  },\n  api: {\n    title: \"Chaves API\",\n    description: \"Chaves API permitem acesso programático a esta instância.\",\n    link: \"Leia a documentação da API\",\n    generate: \"Gerar Nova Chave\",\n    table: {\n      key: \"Chave API\",\n      by: \"Criado Por\",\n      created: \"Criado Em\",\n    },\n  },\n  llm: {\n    title: \"Preferência de LLM\",\n    description:\n      \"Credenciais e configurações do seu provedor de LLM. Essas chaves devem estar corretas para o funcionamento adequado.\",\n    provider: \"Provedor de LLM\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Endpoint do Serviço Azure\",\n        api_key: \"Chave da API\",\n        chat_deployment_name: \"Nome do Deployment de Chat\",\n        chat_model_token_limit: \"Limite de Tokens do Modelo de Chat\",\n        model_type: \"Tipo do Modelo\",\n        default: \"Padrão\",\n        reasoning: \"Raciocínio\",\n        model_type_tooltip:\n          'Se o seu ambiente de uso utiliza um modelo de raciocínio (o1, o1-mini, o3-mini, etc.), defina esta opção como \"Raciocínio\". Caso contrário, suas solicitações de chat podem falhar.',\n      },\n    },\n  },\n  transcription: {\n    title: \"Preferência de Transcrição\",\n    description:\n      \"Credenciais e configurações do seu provedor de transcrição. Essas chaves devem estar corretas para processar arquivos de mídia.\",\n    provider: \"Provedor de Transcrição\",\n    \"warn-start\":\n      \"Usar o modelo local whisper em máquinas com RAM ou CPU limitada pode travar o AnythingLLM.\",\n    \"warn-recommend\": \"Recomendamos pelo menos 2GB de RAM e arquivos <10Mb.\",\n    \"warn-end\":\n      \"O modelo interno será baixado automaticamente no primeiro uso.\",\n  },\n  embedding: {\n    title: \"Preferência de Vínculo\",\n    \"desc-start\":\n      \"Ao usar um LLM sem suporte nativo a vínculo, você pode precisar especificar credenciais adicionais.\",\n    \"desc-end\":\n      \"Vínculo é o processo de transformar texto em vetores. Essas credenciais são necessárias para processar arquivos e prompts.\",\n    provider: {\n      title: \"Provedor de Vínculo\",\n    },\n  },\n  text: {\n    title: \"Preferências de Divisão de Texto\",\n    \"desc-start\":\n      \"Você pode alterar a forma como novos documentos são divididos antes de serem inseridos no banco de dados vetorial.\",\n    \"desc-end\": \"Modifique apenas se entender os efeitos da divisão de texto.\",\n    size: {\n      title: \"Tamanho dos Trechos\",\n      description: \"Comprimento máximo de caracteres em um único vetor.\",\n      recommend: \"Tamanho máximo do modelo de vínculo é\",\n    },\n    overlap: {\n      title: \"Sobreposição de Trechos\",\n      description:\n        \"Sobreposição máxima de caracteres entre dois trechos adjacentes.\",\n    },\n  },\n  vector: {\n    title: \"Banco de Dados Vetorial\",\n    description:\n      \"Credenciais e configurações do seu banco de dados vetorial. Essas chaves devem estar corretas para o funcionamento adequado.\",\n    provider: {\n      title: \"Provedor do Banco\",\n      description: \"Nenhuma configuração necessária para LanceDB.\",\n    },\n  },\n  embeddable: {\n    title: \"Widgets de Chat vinculado\",\n    description:\n      \"Widgets de chat vinculadas são interfaces de chats públicos ligadas a um único workspace. Isto permite construir workspaces e publicá-los na web.\",\n    create: \"Criar vínculo\",\n    table: {\n      workspace: \"Workspace\",\n      chats: \"Chats Enviados\",\n      active: \"Domínios Ativos\",\n      created: \"Criado Em\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Chats Vinculados\",\n    export: \"Exportar\",\n    description: \"Todos os chats registrados de qualquer vínculo publicado.\",\n    table: {\n      embed: \"Vínculo\",\n      sender: \"Remetente\",\n      message: \"Mensagem\",\n      response: \"Resposta\",\n      at: \"Enviado Em\",\n    },\n  },\n  event: {\n    title: \"Logs de Eventos\",\n    description:\n      \"Visualize todas as ações e eventos nesta instância para monitoramento.\",\n    clear: \"Limpar Logs de eventos\",\n    table: {\n      type: \"Tipo de Evento\",\n      user: \"Usuário\",\n      occurred: \"Ocorrido Em\",\n    },\n  },\n  privacy: {\n    title: \"Privacidade & Dados\",\n    description:\n      \"Configurações de como provedores terceiros e o AnythingLLM lidam com seus dados.\",\n    anonymous: \"Telemetria Anônima Ativa\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Buscar conectores\",\n    \"no-connectors\": \"Nenhum conector encontrado.\",\n    obsidian: {\n      vault_location: \"Local do Cofre\",\n      vault_description:\n        \"Selecione sua pasta do Obsidian para importar todas as notas.\",\n      selected_files: \"Encontrados {{count}} arquivos markdown\",\n      importing: \"Importando cofre...\",\n      import_vault: \"Importar Cofre\",\n      processing_time: \"Pode levar algum tempo dependendo do tamanho do cofre.\",\n      vault_warning:\n        \"Para evitar conflitos, certifique-se que seu cofre Obsidian não está aberto.\",\n    },\n    github: {\n      name: \"Repositório GitHub\",\n      description:\n        \"Importe um repositório GitHub público ou privado com um clique.\",\n      URL: \"URL do Repositório\",\n      URL_explained: \"URL do repositório que deseja coletar.\",\n      token: \"Token de Acesso\",\n      optional: \"opcional\",\n      token_explained: \"Token para evitar limitação de taxa.\",\n      token_explained_start: \"Sem um \",\n      token_explained_link1: \"Token de Acesso Pessoal\",\n      token_explained_middle:\n        \", a API do GitHub pode limitar o número de arquivos coletados. Você pode \",\n      token_explained_link2: \"criar um Token Temporário\",\n      token_explained_end: \" para evitar isso.\",\n      ignores: \"Arquivos Ignorados\",\n      git_ignore:\n        \"Liste no formato .gitignore para ignorar arquivos específicos. Pressione enter após cada entrada.\",\n      task_explained:\n        \"Após conclusão, todos os arquivos estarão disponíveis para vínculo.\",\n      branch: \"Branch\",\n      branch_loading: \"-- carregando branches --\",\n      branch_explained: \"Branch para coletar arquivos.\",\n      token_information:\n        \"Sem preencher o <b>Token de Acesso</b>, este conector só poderá coletar arquivos <b>do nível superior</b> devido a limitações da API pública.\",\n      token_personal: \"Obtenha um Token de Acesso Pessoal gratuito aqui.\",\n    },\n    gitlab: {\n      name: \"Repositório GitLab\",\n      description:\n        \"Importe um repositório GitLab público ou privado com um clique.\",\n      URL: \"URL do Repositório\",\n      URL_explained: \"URL do repositório que deseja coletar.\",\n      token: \"Token de Acesso\",\n      optional: \"opcional\",\n      token_description: \"Selecione entidades adicionais para buscar na API.\",\n      token_explained_start: \"Sem um \",\n      token_explained_link1: \"Token de Acesso Pessoal\",\n      token_explained_middle:\n        \", a API do GitLab pode limitar o número de arquivos coletados. Você pode \",\n      token_explained_link2: \"criar um Token Temporário\",\n      token_explained_end: \" para evitar isso.\",\n      fetch_issues: \"Buscar Issues como Documentos\",\n      ignores: \"Arquivos Ignorados\",\n      git_ignore:\n        \"Liste no formato .gitignore para ignorar arquivos específicos. Pressione enter após cada entrada.\",\n      task_explained:\n        \"Após conclusão, todos os arquivos estarão disponíveis para vínculo.\",\n      branch: \"Branch\",\n      branch_loading: \"-- carregando branches --\",\n      branch_explained: \"Branch para coletar arquivos.\",\n      token_information:\n        \"Sem preencher o <b>Token de Acesso</b>, este conector só poderá coletar arquivos <b>do nível superior</b> devido a limitações da API pública.\",\n      token_personal: \"Obtenha um Token de Acesso Pessoal gratuito aqui.\",\n    },\n    youtube: {\n      name: \"Transcrição do YouTube\",\n      description:\n        \"Importe a transcrição de um vídeo do YouTube a partir de um link.\",\n      URL: \"URL do Vídeo\",\n      URL_explained_start:\n        \"Insira a URL de qualquer vídeo do YouTube para buscar sua transcrição. O vídeo deve ter \",\n      URL_explained_link: \"legendas\",\n      URL_explained_end: \" disponíveis.\",\n      task_explained:\n        \"Após conclusão, a transcrição estará disponível para vínculo.\",\n    },\n    \"website-depth\": {\n      name: \"Coletor de Links\",\n      description:\n        \"Extraia um site e seus sublinks até uma certa profundidade.\",\n      URL: \"URL do Site\",\n      URL_explained: \"URL do site que deseja extrair.\",\n      depth: \"Profundidade\",\n      depth_explained:\n        \"Número de links filhos que o coletor deve seguir a partir da URL original.\",\n      max_pages: \"Máximo de Páginas\",\n      max_pages_explained: \"Número máximo de links para extrair.\",\n      task_explained:\n        \"Após conclusão, todo o conteúdo estará disponível para vínculo.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Importe uma página do Confluence com um clique.\",\n      deployment_type: \"Tipo de instalação\",\n      deployment_type_explained:\n        \"Determine se sua instância é hospedada na nuvem ou auto-hospedada.\",\n      base_url: \"URL Base\",\n      base_url_explained: \"URL base do seu espaço no Confluence.\",\n      space_key: \"Chave do Espaço\",\n      space_key_explained:\n        \"Chave do espaço no Confluence que será usada. Geralmente começa com ~\",\n      username: \"Nome de Usuário\",\n      username_explained: \"Seu nome de usuário no Confluence\",\n      auth_type: \"Tipo de Autenticação\",\n      auth_type_explained:\n        \"Selecione o tipo de autenticação para acessar suas páginas.\",\n      auth_type_username: \"Usuário e Token\",\n      auth_type_personal: \"Token Pessoal\",\n      token: \"Token de Acesso\",\n      token_explained_start:\n        \"Forneça um token de acesso para autenticação. Você pode gerar um token\",\n      token_explained_link: \"aqui\",\n      token_desc: \"Token para autenticação\",\n      pat_token: \"Token Pessoal\",\n      pat_token_explained: \"Seu token pessoal de acesso.\",\n      task_explained:\n        \"Após conclusão, o conteúdo da página estará disponível para vínculo.\",\n      bypass_ssl: \"Desviar a validação do certificado SSL\",\n      bypass_ssl_explained:\n        \"Habilite esta opção para contornar a validação do certificado SSL para instâncias do Confluence hospedadas por si mesmo, com certificado autoassinado.\",\n    },\n    manage: {\n      documents: \"Documentos\",\n      \"data-connectors\": \"Conectores de Dados\",\n      \"desktop-only\":\n        \"Editar estas configurações só está disponível em dispositivos desktop. Acesse esta página em seu desktop para continuar.\",\n      dismiss: \"Ignorar\",\n      editing: \"Editando\",\n    },\n    directory: {\n      \"my-documents\": \"Meus Documentos\",\n      \"new-folder\": \"Nova Pasta\",\n      \"search-document\": \"Buscar documento\",\n      \"no-documents\": \"Nenhum Documento\",\n      \"move-workspace\": \"Mover para Workspace\",\n      \"delete-confirmation\":\n        \"Tem certeza que deseja excluir estes arquivos e pastas?\\nIsso removerá os arquivos do sistema e de todos os workspaces automaticamente.\\nEsta ação é irreversível.\",\n      \"removing-message\":\n        \"Removendo {{count}} documentos e {{folderCount}} pastas. Aguarde.\",\n      \"move-success\": \"{{count}} documentos movidos com sucesso.\",\n      no_docs: \"Nenhum Documento\",\n      select_all: \"Selecionar Tudo\",\n      deselect_all: \"Desmarcar Tudo\",\n      remove_selected: \"Remover Selecionados\",\n      costs: \"*Custo único para vínculos\",\n      save_embed: \"Salvar e Inserir\",\n      \"total-documents_one\": \"{{count}} documento\",\n      \"total-documents_other\": \"{{count}} documentos\",\n    },\n    upload: {\n      \"processor-offline\": \"Processador de documentos Indisponível\",\n      \"processor-offline-desc\":\n        \"Não é possível enviar arquivos agora. O processador de documentos está offline. Tente mais tarde.\",\n      \"click-upload\": \"Clique para enviar ou arraste e solte\",\n      \"file-types\": \"suporta textos, csv, planilhas, áudios e mais!\",\n      \"or-submit-link\": \"ou envie um link\",\n      \"placeholder-link\": \"https://exemplo.com\",\n      fetching: \"Buscando...\",\n      \"fetch-website\": \"Buscar site\",\n      \"privacy-notice\":\n        \"Esses arquivos são enviados ao processador local do AnythingLLM. Não são compartilhados com terceiros.\",\n    },\n    pinning: {\n      what_pinning: \"O que é fixar documento?\",\n      pin_explained_block1:\n        \"Ao <b>fixar</b> um documento, o conteúdo será injetado na janela do prompt para o LLM entender.\",\n      pin_explained_block2:\n        \"Funciona melhor com <b>modelos de contexto grande</b> ou arquivos pequenos e importantes.\",\n      pin_explained_block3:\n        \"Se não tiver boas respostas, fixar pode melhorar a qualidade com um clique.\",\n      accept: \"Ok, entendi\",\n    },\n    watching: {\n      what_watching: \"O que é monitorar um documento?\",\n      watch_explained_block1:\n        \"Ao <b>monitorar</b>, o conteúdo será <i>sincronizado</i> com a fonte em intervalos regulares.\",\n      watch_explained_block2:\n        \"Funciona apenas com conteúdo online, não com uploads manuais.\",\n      watch_explained_block3_start:\n        \"Você pode gerenciar documentos monitorados no \",\n      watch_explained_block3_link: \"Gerenciador de arquivos\",\n      watch_explained_block3_end: \" na visão de admin.\",\n      accept: \"Ok, entendi\",\n    },\n  },\n  chat_window: {\n    attachments_processing: \"Anexos em processamento. Aguarde...\",\n    send_message: \"Enviar mensagem\",\n    attach_file: \"Anexar arquivo ao chat\",\n    text_size: \"Alterar tamanho do texto.\",\n    microphone: \"Fale seu prompt.\",\n    send: \"Enviar prompt para o workspace\",\n    tts_speak_message: \"Leitura em voz alta da mensagem\",\n    copy: \"Copiar\",\n    regenerate: \"Regerar\",\n    regenerate_response: \"Regerar resposta\",\n    good_response: \"Resposta satisfatória\",\n    more_actions: \"Mais ações\",\n    fork: \"Fork\",\n    delete: \"Excluir\",\n    cancel: \"Cancelar\",\n    edit_prompt: \"Editar prompt\",\n    edit_response: \"Editar resposta\",\n    preset_reset_description: \"Limpa o histórico do seu chat e inicia um novo\",\n    add_new_preset: \" Insere um novo Preset\",\n    command: \"Comando\",\n    your_command: \"seu-comando\",\n    placeholder_prompt:\n      \"Este é o conteúdo que será injetado a frente do seu prompt.\",\n    description: \"Descrição\",\n    placeholder_description: \"Responde como um poema sobre LLMs.\",\n    save: \"Salvar\",\n    small: \"Pequeno\",\n    normal: \"Normal\",\n    large: \"Grande\",\n    workspace_llm_manager: {\n      search: \"Buscar provedores de LLM\",\n      loading_workspace_settings: \"Carregando configurações do workspace...\",\n      available_models: \"Modelos Disponíveis\",\n      available_models_description: \"Selecione um modelo para este workspace\",\n      save: \"Salvar modelo do workspace\",\n      saving: \"Salvando...\",\n      missing_credentials: \"Credenciais em falta\",\n      missing_credentials_description:\n        \"Configure as credenciais do LLM primeiro\",\n    },\n    submit: \"Enviar\",\n    edit_info_user:\n      '\"Enviar\" recria a resposta da IA. \"Salvar\" atualiza apenas sua mensagem.',\n    edit_info_assistant:\n      \"Suas alterações serão salvas diretamente nesta resposta.\",\n    see_less: \"Ver menos\",\n    see_more: \"Ver mais\",\n    tools: \"Ferramentas\",\n    browse: \"Navegar\",\n    text_size_label: \"Tamanho do texto\",\n    select_model: \"Selecione o modelo\",\n    sources: \"Fontes\",\n    document: \"Documento\",\n    similarity_match: \"jogo\",\n    source_count_one: \"Referência a {{count}}\",\n    source_count_other: \"Referências a {{count}}\",\n    preset_exit_description: \"Interrompa a sessão atual do agente\",\n    add_new: \"Adicionar novo\",\n    edit: \"Editar\",\n    publish: \"Publicar\",\n    stop_generating: \"Pare de gerar respostas\",\n    pause_tts_speech_message: \"Pausar a leitura de voz da mensagem\",\n    slash_commands: \"Comandos Rápidos\",\n    agent_skills: \"Habilidades do Agente\",\n    manage_agent_skills: \"Gerenciar as habilidades dos agentes\",\n    agent_skills_disabled_in_session:\n      \"Não é possível modificar as habilidades durante uma sessão de agente ativa. Utilize o comando `/exit` para encerrar a sessão primeiro.\",\n    start_agent_session: \"Iniciar Sessão de Agente\",\n    use_agent_session_to_use_tools:\n      'Você pode utilizar as ferramentas disponíveis no chat iniciando uma sessão com um agente, adicionando \"@agent\" no início da sua mensagem.',\n  },\n  profile_settings: {\n    edit_account: \"Editar conta\",\n    profile_picture: \"Foto de perfil\",\n    remove_profile_picture: \"Remover foto de perfil\",\n    username: \"Nome de usuário\",\n    new_password: \"Nova senha\",\n    password_description: \"A senha deve ter no mínimo 8 caracteres\",\n    cancel: \"Cancelar\",\n    update_account: \"Atualizar conta\",\n    theme: \"Preferência de tema\",\n    language: \"Idioma preferido\",\n    failed_upload: \"Falha no upload da foto de perfil\",\n    upload_success: \"Foto de perfil atualizada com sucesso\",\n    failed_remove: \"Falha ao remover foto de perfil\",\n    profile_updated: \"Perfil atualizado com sucesso\",\n    failed_update_user: \"Falha ao atualizar perfil do usuário\",\n    account: \"Conta\",\n    support: \"Suporte\",\n    signout: \"Sair\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Atalhos de Teclado\",\n    shortcuts: {\n      settings: \"Ajustes\",\n      workspaceSettings: \"Abrir os ajustes do workspace\",\n      home: \"Ir para a página inicial\",\n      workspaces: \"Gerenciar workspaces\",\n      apiKeys: \"Ajustes das chaves da API\",\n      llmPreferences: \"Preferências do LLM\",\n      chatSettings: \"Ajustes do chat\",\n      help: \"Exibe ajuda e atalhos\",\n      showLLMSelector: \"Exibir seletor de LLM\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Prompt de sistema publicado!\",\n        success_description:\n          \"Seu prompt de sistema foi publicado com sucesso no Hub da Comunidade.\",\n        success_thank_you: \"Obrigado por contribuir!\",\n        view_on_hub: \"Ver no Hub\",\n        modal_title: \"Publicar prompt de sistema\",\n        name_label: \"Nome\",\n        name_description: \"Nome único para seu prompt de sistema\",\n        name_placeholder: \"Meu prompt de sistema incrível\",\n        description_label: \"Descrição\",\n        description_description: \"Descreva o que seu prompt de sistema faz\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Adicione tags para ajudar outros a encontrar seu prompt\",\n        tags_placeholder: \"prompt, assistente, produtividade\",\n        visibility_label: \"Visibilidade\",\n        public_description: \"Qualquer pessoa pode ver e usar este prompt\",\n        private_description: \"Apenas você pode ver e usar este prompt\",\n        publish_button: \"Publicar prompt de sistema\",\n        submitting: \"Publicando...\",\n        prompt_label: \"Prompt de sistema\",\n        prompt_description: \"O conteúdo do seu prompt de sistema\",\n        prompt_placeholder: \"Você é um assistente útil que...\",\n      },\n      agent_flow: {\n        success_title: \"Fluxo de agente publicado!\",\n        success_description:\n          \"Seu fluxo de agente foi publicado com sucesso no Hub da Comunidade.\",\n        success_thank_you: \"Obrigado por contribuir!\",\n        view_on_hub: \"Ver no Hub\",\n        modal_title: \"Publicar fluxo de agente\",\n        name_label: \"Nome\",\n        name_description: \"Nome único para seu fluxo de agente\",\n        name_placeholder: \"Meu fluxo de agente incrível\",\n        description_label: \"Descrição\",\n        description_description: \"Descreva o que seu fluxo de agente faz\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Adicione tags para ajudar outros a encontrar seu fluxo\",\n        tags_placeholder: \"agente, automação, fluxo de trabalho\",\n        visibility_label: \"Visibilidade\",\n        submitting: \"Publicando...\",\n        submit: \"Publicar\",\n        privacy_note:\n          \"Nota: dados sensíveis serão removidos antes da publicação\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Faça login para publicar\",\n          description:\n            \"Você precisa estar logado para publicar no Hub da Comunidade\",\n          button: \"Fazer login\",\n        },\n      },\n      slash_command: {\n        success_title: \"Comando de barra publicado!\",\n        success_description:\n          \"Seu comando de barra foi publicado com sucesso no Hub da Comunidade.\",\n        success_thank_you: \"Obrigado por contribuir!\",\n        view_on_hub: \"Ver no Hub\",\n        modal_title: \"Publicar comando de barra\",\n        name_label: \"Nome\",\n        name_description: \"Nome único para seu comando de barra\",\n        name_placeholder: \"Meu comando incrível\",\n        description_label: \"Descrição\",\n        description_description: \"Descreva o que seu comando faz\",\n        tags_label: \"Tags\",\n        tags_description:\n          \"Adicione tags para ajudar outros a encontrar seu comando\",\n        tags_placeholder: \"comando, produtividade, útil\",\n        visibility_label: \"Visibilidade\",\n        public_description: \"Qualquer pessoa pode ver e usar este comando\",\n        private_description: \"Apenas você pode ver e usar este comando\",\n        publish_button: \"Publicar comando de barra\",\n        submitting: \"Publicando...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"O prompt que será executado quando o comando for usado\",\n        prompt_placeholder: \"Responda como um especialista em...\",\n      },\n    },\n  },\n  security: {\n    title: \"Segurança\",\n    multiuser: {\n      title: \"Modo Multi-Usuário\",\n      description:\n        \"Configure sua instância para suportar sua equipe ativando o modo multi-usuário.\",\n      enable: {\n        \"is-enable\": \"Modo Multi-Usuário Ativo\",\n        enable: \"Ativar Modo Multi-Usuário\",\n        description:\n          \"Por padrão, você será o único administrador. Como administrador, você precisará criar contas para novos usuários. Não perca sua senha, pois apenas administradores podem redefini-la.\",\n        username: \"Nome de usuário admin\",\n        password: \"Senha de admin\",\n      },\n    },\n    password: {\n      title: \"Proteção por Senha\",\n      description:\n        \"Proteja sua instância com uma senha. Não há recuperação, então salve esta senha.\",\n      \"password-label\": \"Senha da instância\",\n    },\n  },\n  home: {\n    welcome: \"Bem-vindo\",\n    chooseWorkspace: \"Escolha um espaço de trabalho para começar a conversar!\",\n    notAssigned:\n      \"Você ainda não está atribuído a nenhum espaço de trabalho.\\nEntre em contato com seu administrador para solicitar acesso a um espaço de trabalho.\",\n    goToWorkspace: 'Ir para o espaço de trabalho \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/resources.js",
    "content": "// Looking for a language to translate AnythingLLM to?\n// Create a `common.js` file in the language's ISO code https://www.w3.org/International/O-charset-lang.html\n// eg: Spanish => es/common.js\n// eg: French => fr/common.js\n// You should copy the en/common.js file as your template and just translate every string in there.\n// By default, we try to see what the browsers native language is set to and use that. If a string\n// is not defined or is null in the translation file, it will fallback to the value in the en/common.js file\n// RULES:\n// The EN translation file is the ground-truth for what keys and options are available. DO NOT add a special key\n// to a specific language file as this will break the other languages. Any new keys should be added to english\n// and the language file you are working on.\n\n// Contributor Notice: If you are adding a translation you MUST locally run `yarn verify:translations` from the root prior to PR.\n// please do not submit PR's without first verifying this test passes as it will tell you about missing keys or values\n// from the primary dictionary.\n\nimport English from \"./en/common.js\";\nimport Korean from \"./ko/common.js\";\nimport Spanish from \"./es/common.js\";\nimport French from \"./fr/common.js\";\nimport Mandarin from \"./zh/common.js\";\nimport German from \"./de/common.js\";\nimport Estonian from \"./et/common.js\";\nimport Russian from \"./ru/common.js\";\nimport Italian from \"./it/common.js\";\nimport Portuguese from \"./pt_BR/common.js\";\nimport Hebrew from \"./he/common.js\";\nimport Dutch from \"./nl/common.js\";\nimport Vietnamese from \"./vn/common.js\";\nimport TraditionalChinese from \"./zh_TW/common.js\";\nimport Farsi from \"./fa/common.js\";\nimport Turkish from \"./tr/common.js\";\nimport Arabic from \"./ar/common.js\";\nimport Danish from \"./da/common.js\";\nimport Japanese from \"./ja/common.js\";\nimport Lativian from \"./lv/common.js\";\nimport Polish from \"./pl/common.js\";\nimport Romanian from \"./ro/common.js\";\nimport Czech from \"./cs/common.js\";\n\nexport const defaultNS = \"common\";\nexport const resources = {\n  en: {\n    common: English,\n  },\n  zh: {\n    common: Mandarin,\n  },\n  \"zh-tw\": {\n    common: TraditionalChinese,\n  },\n  es: {\n    common: Spanish,\n  },\n  de: {\n    common: German,\n  },\n  fr: {\n    common: French,\n  },\n  ko: {\n    common: Korean,\n  },\n  et: {\n    common: Estonian,\n  },\n  ru: {\n    common: Russian,\n  },\n  it: {\n    common: Italian,\n  },\n  pt: {\n    common: Portuguese,\n  },\n  he: {\n    common: Hebrew,\n  },\n  nl: {\n    common: Dutch,\n  },\n  vi: {\n    common: Vietnamese,\n  },\n  fa: {\n    common: Farsi,\n  },\n  tr: {\n    common: Turkish,\n  },\n  ar: {\n    common: Arabic,\n  },\n  da: {\n    common: Danish,\n  },\n  ja: {\n    common: Japanese,\n  },\n  lv: {\n    common: Lativian,\n  },\n  pl: {\n    common: Polish,\n  },\n  ro: {\n    common: Romanian,\n  },\n  cs: {\n    common: Czech,\n  },\n};\n"
  },
  {
    "path": "frontend/src/locales/ro/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"Bine ai venit la\",\n      getStarted: \"Începe\",\n    },\n    llm: {\n      title: \"Preferința LLM\",\n      description:\n        \"AnythingLLM poate funcționa cu mai mulți furnizori LLM. Acesta va fi serviciul care gestionează conversațiile.\",\n    },\n    userSetup: {\n      title: \"Configurare Utilizator\",\n      description: \"Configurează setările utilizatorului tău.\",\n      howManyUsers: \"Câți utilizatori vor folosi această resursă?\",\n      justMe: \"Doar eu\",\n      myTeam: \"Echipa mea\",\n      instancePassword: \"Parola Resursei\",\n      setPassword: \"Dorești să setezi o parolă?\",\n      passwordReq: \"Parolele trebuie să aibă cel puțin 8 caractere.\",\n      passwordWarn:\n        \"Este important să salvezi această parolă deoarece nu există metodă de recuperare.\",\n      adminUsername: \"Numele contului de administrator\",\n      adminPassword: \"Parola contului de administrator\",\n      adminPasswordReq: \"Parolele trebuie să aibă cel puțin 8 caractere.\",\n      teamHint:\n        \"Implicit, vei fi singurul administrator. După finalizarea configurării inițiale, poți crea și invita alți utilizatori sau administratori. Nu pierde parola, deoarece doar administratorii pot reseta parolele.\",\n    },\n    data: {\n      title: \"Gestionarea datelor & Confidențialitate\",\n      description:\n        \"Suntem dedicați transparenței și controlului asupra datelor tale personale.\",\n      settingsHint:\n        \"Aceste setări pot fi reconfigurate oricând în setările aplicației.\",\n    },\n    survey: {\n      title: \"Bun venit la AnythingLLM\",\n      description:\n        \"Ajută-ne să facem AnythingLLM potrivit pentru nevoile tale. Opțional.\",\n      email: \"Care este adresa ta de email?\",\n      useCase: \"Pentru ce vei folosi AnythingLLM?\",\n      useCaseWork: \"Pentru muncă\",\n      useCasePersonal: \"Pentru uz personal\",\n      useCaseOther: \"Altele\",\n      comment: \"De unde ai aflat despre AnythingLLM?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube, etc. - Spune-ne cum ne-ai găsit!\",\n      skip: \"Sari peste sondaj\",\n      thankYou: \"Îți mulțumim pentru feedback!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Numele spațiilor de lucru\",\n    user: \"Utilizator\",\n    selection: \"Selecția modelului\",\n    saving: \"Se salvează...\",\n    save: \"Salvează modificările\",\n    previous: \"Pagina anterioară\",\n    next: \"Pagina următoare\",\n    optional: \"Opțional\",\n    yes: \"Da\",\n    no: \"Nu\",\n    search: \"Caută\",\n    username_requirements:\n      \"Numele de utilizator trebuie să aibă între 2 și 32 de caractere, să înceapă cu o literă mică și să conțină doar litere mici, cifre, liniuțe de subliniere, cratime și puncte.\",\n    on: \"În\",\n    none: \"Niciunul\",\n    stopped: \"Oprit\",\n    loading: \"Încărcare\",\n    refresh: \"Reîmprospătează\",\n  },\n  settings: {\n    title: \"Setările instanței\",\n    invites: \"Invitații\",\n    users: \"Utilizatori\",\n    workspaces: \"Spații de lucru\",\n    \"workspace-chats\": \"Conversațiile spațiului de lucru\",\n    customization: \"Personalizare\",\n    interface: \"Preferințe UI\",\n    branding: \"Branding & White-label\",\n    chat: \"Chat\",\n    \"api-keys\": \"API pentru dezvoltatori\",\n    llm: \"LLM\",\n    transcription: \"Transcriere\",\n    embedder: \"Embedder\",\n    \"text-splitting\": \"Împărțirea și segmentarea textului\",\n    \"voice-speech\": \"Voce & Vorbire\",\n    \"vector-database\": \"Baza de date vectorială\",\n    embeds: \"Chat Embed\",\n    security: \"Securitate\",\n    \"event-logs\": \"Jurnale de evenimente\",\n    privacy: \"Confidențialitate & Date\",\n    \"ai-providers\": \"Furnizori AI\",\n    \"agent-skills\": \"Abilități agent\",\n    admin: \"Administrator\",\n    tools: \"Instrumente\",\n    \"system-prompt-variables\": \"Variabile system prompt\",\n    \"experimental-features\": \"Funcții experimentale\",\n    contact: \"Contact suport\",\n    \"browser-extension\": \"Extensie browser\",\n    \"mobile-app\": \"AnythingLLM Mobile\",\n    \"community-hub\": {\n      title: \"Centru comunitar\",\n      trending: \"Descoperă tendințele\",\n      \"your-account\": \"Contul dumneavoastră\",\n      \"import-item\": \"Importați articolul\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Bine ai venit la\",\n      \"placeholder-username\": \"Nume utilizator\",\n      \"placeholder-password\": \"Parolă\",\n      login: \"Autentifică-te\",\n      validating: \"Se validează...\",\n      \"forgot-pass\": \"Ai uitat parola\",\n      reset: \"Resetează\",\n    },\n    \"sign-in\": \"Autentifică-te în {{appName}} cont.\",\n    \"password-reset\": {\n      title: \"Resetare parolă\",\n      description:\n        \"Introdu informațiile necesare mai jos pentru a reseta parola.\",\n      \"recovery-codes\": \"Coduri de recuperare\",\n      \"back-to-login\": \"Înapoi la autentificare\",\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Creați un agent\",\n      editWorkspace: \"Modifică spațiul de lucru\",\n      uploadDocument: \"Încărcați un document\",\n    },\n    greeting: \"Cu ce vă pot ajuta astăzi?\",\n  },\n  \"new-workspace\": {\n    title: \"Spațiu de lucru nou\",\n    placeholder: \"Spațiul meu de lucru\",\n  },\n  \"workspaces—settings\": {\n    general: \"Setări generale\",\n    chat: \"Setări chat\",\n    vector: \"Baza de date vectorială\",\n    members: \"Membri\",\n    agent: \"Configurare agent\",\n  },\n  general: {\n    vector: {\n      title: \"Număr vectori\",\n      description: \"Numărul total de vectori în baza ta de date vectorială.\",\n    },\n    names: {\n      description:\n        \"Aceasta va schimba doar numele afișat al spațiului de lucru.\",\n    },\n    message: {\n      title: \"Mesaje sugerate pentru chat\",\n      description:\n        \"Personalizează mesajele care vor fi sugerate utilizatorilor spațiului de lucru.\",\n      add: \"Adaugă mesaj nou\",\n      save: \"Salvează mesajele\",\n      heading: \"Explică-mi\",\n      body: \"beneficiile AnythingLLM\",\n    },\n    delete: {\n      title: \"Șterge spațiul de lucru\",\n      description:\n        \"Șterge acest spațiu de lucru și toate datele sale. Aceasta va șterge spațiul de lucru pentru toți utilizatorii.\",\n      delete: \"Șterge spațiul de lucru\",\n      deleting: \"Se șterge spațiul de lucru...\",\n      \"confirm-start\": \"Ești pe cale să ștergi întregul tău\",\n      \"confirm-end\":\n        \"spațiu de lucru. Această acțiune va elimina toate încorporările vectoriale (vector embeddings) din baza dumneavoastră de date vectorială.\\n\\nFișierele originale rămân neatinse. Această acțiune este ireversibilă.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Furnizor LLM pentru spațiu de lucru\",\n      description:\n        \"Furnizorul LLM și modelul specific folosit pentru acest spațiu de lucru. Implicit, folosește setările și furnizorul sistemului.\",\n      search: \"Caută toți furnizorii LLM\",\n    },\n    model: {\n      title: \"Modelul de chat al spațiului de lucru\",\n      description:\n        \"Modelul specific chat folosit de acest spațiu de lucru. Dacă e lăsat gol, folosește preferința LLM a sistemului.\",\n    },\n    mode: {\n      title: \"Mod chat\",\n      chat: {\n        title: \"Chat\",\n        description:\n          'va oferi răspunsuri folosind cunoștințele generale ale modelului LLM și contextul documentului respectiv.<br />Va trebui să utilizați comanda \"@agent\" pentru a utiliza instrumentele.',\n      },\n      query: {\n        title: \"Interogare\",\n        description:\n          'vor oferi răspunsuri **doar** dacă contextul documentului este identificat. Veți avea nevoie să utilizați comanda \"@agent\" pentru a utiliza instrumentele.',\n      },\n      automatic: {\n        title: \"Mașină\",\n        description:\n          'va utiliza automat instrumentele, dacă modelul și furnizorul suportă apelarea nativă a instrumentelor.<br />Dacă apelarea nativă a instrumentelor nu este suportată, veți avea nevoie să utilizați comanda \"@agent\" pentru a utiliza instrumentele.',\n      },\n    },\n    history: {\n      title: \"Istoric chat\",\n      \"desc-start\":\n        \"Numărul conversațiilor anterioare care vor fi incluse în memoria pe termen scurt a răspunsului.\",\n      recommend: \"Recomandat: 20.\",\n      \"desc-end\":\n        \"Mai mult de 45 poate duce la erori în conversații în funcție de mărimea mesajelor.\",\n    },\n    prompt: {\n      title: \"System Prompt\",\n      description:\n        \"Promptul folosit în acest spațiu de lucru. Definește contextul și instrucțiunile pentru AI să genereze răspunsuri relevante și precise.\",\n      history: {\n        title: \"Istoricul system prompt\",\n        clearAll: \"Șterge tot\",\n        noHistory: \"Nu există istoric disponibil\",\n        restore: \"Restaurează\",\n        delete: \"Șterge\",\n        publish: \"Publică în Comunitate\",\n        deleteConfirm: \"Sigur dorești să ștergi acest istoric?\",\n        clearAllConfirm:\n          \"Sigur dorești să ștergi tot istoricul? Această acțiune nu poate fi anulată.\",\n        expand: \"Extinde\",\n      },\n    },\n    refusal: {\n      title: \"Răspuns refuz în modul interogare\",\n      \"desc-start\": \"Atunci când ești în\",\n      query: \"modul interogare\",\n      \"desc-end\": \", poți personaliza răspunsul când nu se găsește context.\",\n      \"tooltip-title\": \"De ce văd asta?\",\n      \"tooltip-description\":\n        \"Ești în modul interogare (query), care folosește doar informațiile din documente. Treci pe modul chat pentru conversații mai flexibile sau vizitează documentația pentru mai multe detalii.\",\n    },\n    temperature: {\n      title: \"Temperatura LLM\",\n      \"desc-start\":\n        'Această setare controlează cât de \"creativ\" va fi răspunsul LLM-ului.',\n      \"desc-end\":\n        \"Cu cât numărul e mai mare, cu atât mai creativ. Pentru unele modele poate duce la răspunsuri incoerente la valori mari.\",\n      hint: \"Majoritatea LLM-urilor au un interval valid specific. Consultă furnizorul tău LLM pentru detalii.\",\n    },\n  },\n  vector: {\n    title: \"Baza de date vectorială\",\n    description:\n      \"Acestea sunt credențialele și setările pentru modul în care funcționează instanța ta AnythingLLM. Este important să fie corecte și actuale.\",\n    provider: {\n      title: \"Furnizor baza de date vectorială\",\n      description: \"Nu este necesară configurarea pentru LanceDB.\",\n    },\n  },\n  embeddable: {\n    title: \"Widget-uri chat integrabile (embeddable)\",\n    description:\n      \"Widgeturile de chat integrabile sunt interfețe de chat publice, asociate unui singur spațiu de lucru. Acestea vă permit să creați spații de lucru pe care le puteți apoi publica pentru întreaga lume.\",\n    create: \"Generează cod embed\",\n    table: {\n      workspace: \"Spațiu de lucru\",\n      chats: \"Chaturi trimise\",\n      active: \"Domenii active\",\n      created: \"Creat\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Istoric chat embed\",\n    export: \"Exportă\",\n    description:\n      \"Acestea sunt toate chat-urile și mesajele înregistrate din orice embed pe care l-ai publicat.\",\n    table: {\n      embed: \"Embed\",\n      sender: \"Expeditor\",\n      message: \"Mesaj\",\n      response: \"Răspuns\",\n      at: \"Trimis la\",\n    },\n  },\n  event: {\n    title: \"Jurnale de evenimente\",\n    description:\n      \"Vizualizează toate acțiunile și evenimentele care au loc pe această resursă pentru monitorizare.\",\n    clear: \"Șterge jurnalele\",\n    table: {\n      type: \"Tip eveniment\",\n      user: \"Utilizator\",\n      occurred: \"S-a întâmplat la\",\n    },\n  },\n  privacy: {\n    title: \"Confidențialitate & Gestionarea datelor\",\n    description:\n      \"Aceasta este configurația ta pentru modul în care furnizorii terți conectați și AnythingLLM gestionează datele tale.\",\n    anonymous: \"Telemetrie anonimă activată\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Caută conectori de date\",\n    \"no-connectors\": \"Nu au fost găsiți conectori de date.\",\n    obsidian: {\n      vault_location: \"Locația vault-ului\",\n      vault_description:\n        \"Selectează folderul vault-ului Obsidian pentru a importa toate notițele și conexiunile lor.\",\n      selected_files: \"Au fost găsite {{count}} fișiere markdown\",\n      importing: \"Import vault în curs...\",\n      import_vault: \"Importă Vault\",\n      processing_time:\n        \"Aceasta poate dura ceva timp în funcție de dimensiunea vault-ului.\",\n      vault_warning:\n        \"Pentru a evita conflictele, asigură-te că vault-ul Obsidian nu este deschis în acest moment.\",\n    },\n    github: {\n      name: \"Repo GitHub\",\n      description:\n        \"Importă un întreg repository public sau privat GitHub cu un singur click.\",\n      URL: \"URL repository GitHub\",\n      URL_explained:\n        \"URL-ul repository-ului GitHub pe care dorești să îl colectezi.\",\n      token: \"Token de acces GitHub\",\n      optional: \"opțional\",\n      token_explained: \"Token de acces pentru a preveni limitările de rată.\",\n      token_explained_start: \"Fără un \",\n      token_explained_link1: \"Token de acces personal\",\n      token_explained_middle:\n        \", API-ul GitHub poate limita numărul de fișiere ce pot fi colectate din cauza limitărilor. Poți \",\n      token_explained_link2: \"crea un token de acces temporar\",\n      token_explained_end: \" pentru a evita această problemă.\",\n      ignores: \"Fișiere ignorate\",\n      git_ignore:\n        \"Listează în format .gitignore fișierele de ignorat la colectare. Apasă enter după fiecare intrare pentru a salva.\",\n      task_explained:\n        \"Odată complet, toate fișierele vor fi disponibile pentru embedding în spații de lucru în selectorul de documente.\",\n      branch: \"Ramura din care dorești să colectezi fișiere.\",\n      branch_loading: \"-- încărcare ramuri disponibile --\",\n      branch_explained: \"Ramura din care dorești să colectezi fișiere.\",\n      token_information:\n        \"Fără token-ul de acces GitHub completat, acest conector va putea colecta doar fișierele de top datorită limitărilor API-ului public GitHub.\",\n      token_personal:\n        \"Obține un token de acces personal gratuit aici cu un cont GitHub.\",\n    },\n    gitlab: {\n      name: \"Repo GitLab\",\n      description:\n        \"Importă un întreg repository public sau privat GitLab cu un singur click.\",\n      URL: \"URL repository GitLab\",\n      URL_explained:\n        \"URL-ul repository-ului GitLab pe care dorești să îl colectezi.\",\n      token: \"Token de acces GitLab\",\n      optional: \"opțional\",\n      token_description:\n        \"Selectează entitățile suplimentare de preluat din API-ul GitLab.\",\n      token_explained_start: \"Fără un \",\n      token_explained_link1: \"Token de acces personal\",\n      token_explained_middle:\n        \", API-ul GitLab poate limita numărul de fișiere ce pot fi colectate din cauza limitărilor. Poți \",\n      token_explained_link2: \"crea un token de acces temporar\",\n      token_explained_end: \" pentru a evita această problemă.\",\n      fetch_issues: \"Preia issue-uri ca documente\",\n      ignores: \"Fișiere ignorate\",\n      git_ignore:\n        \"Listează în format .gitignore fișierele de ignorat la colectare. Apasă enter după fiecare intrare pentru a salva.\",\n      task_explained:\n        \"Odată complet, toate fișierele vor fi disponibile pentru embedding în spații de lucru în selectorul de documente.\",\n      branch: \"Ramura din care dorești să colectezi fișiere.\",\n      branch_loading: \"-- încărcare ramuri disponibile --\",\n      branch_explained: \"Ramura din care dorești să colectezi fișiere.\",\n      token_information:\n        \"Fără token-ul de acces GitLab completat, acest conector va putea colecta doar fișierele de top datorită limitărilor API-ului public GitLab.\",\n      token_personal:\n        \"Obține un token de acces personal gratuit aici cu un cont GitLab.\",\n    },\n    youtube: {\n      name: \"Transcriere YouTube\",\n      description: \"Importă transcrierea unui videoclip YouTube dintr-un link.\",\n      URL: \"URL videoclip YouTube\",\n      URL_explained_start:\n        \"Introdu URL-ul oricărui videoclip YouTube pentru a-i prelua textul. Videoclipul trebuie să aibă \",\n      URL_explained_link: \"subtitrări închise\",\n      URL_explained_end: \" disponibile.\",\n      task_explained:\n        \"Odată complet, transcrierea va fi disponibilă pentru embedding în spații de lucru în selectorul de documente.\",\n    },\n    \"website-depth\": {\n      name: \"Bulk Link Scraper\",\n      description:\n        \"Extrage o pagină web și link-urile sale din subpaginile până la o anumită adâncime.\",\n      URL: \"URL site web\",\n      URL_explained: \"URL-ul site-ului pe care dorești să îl culegi.\",\n      depth: \"Adâncime crawl\",\n      depth_explained:\n        \"Numărul de link-uri de copii pe care workerul trebuie să le urmărească din URL-ul originar.\",\n      max_pages: \"Număr maxim pagini\",\n      max_pages_explained: \"Numărul maxim de link-uri de colectat.\",\n      task_explained:\n        \"Odată complet, tot conținutul cules va fi disponibil pentru embedding în spații de lucru în selectorul de documente.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Importă o pagină Confluence cu un singur click.\",\n      deployment_type: \"Tip implementare Confluence\",\n      deployment_type_explained:\n        \"Determină dacă resursa ta Confluence este găzduită în cloud Atlassian sau self-hosted.\",\n      base_url: \"URL de bază Confluence\",\n      base_url_explained:\n        \"Acesta este URL-ul de bază al spațiului tău Confluence.\",\n      space_key: \"Cheie spațiu Confluence\",\n      space_key_explained:\n        \"Cheia spațiului din resursa ta Confluence care va fi folosită. De obicei începe cu ~\",\n      username: \"Nume utilizator Confluence\",\n      username_explained: \"Numele tău de utilizator Confluence\",\n      auth_type: \"Tip autentificare Confluence\",\n      auth_type_explained:\n        \"Selectează tipul de autentificare pentru accesarea paginilor Confluence.\",\n      auth_type_username: \"Nume utilizator și token de acces\",\n      auth_type_personal: \"Token de acces personal\",\n      token: \"Token de acces Confluence\",\n      token_explained_start:\n        \"Trebuie să furnizezi un token de acces pentru autentificare. Poți genera un token de acces \",\n      token_explained_link: \"aici\",\n      token_desc: \"Token de acces pentru autentificare\",\n      pat_token: \"Token de acces personal Confluence\",\n      pat_token_explained: \"Token-ul tău personal de acces Confluence.\",\n      task_explained:\n        \"Odată complet, conținutul paginii va fi disponibil pentru embedding în spații de lucru în selectorul de documente.\",\n      bypass_ssl: \"Ocolirea validării certificatului SSL\",\n      bypass_ssl_explained:\n        \"Activați această opțiune pentru a ocoli validarea certificatului SSL pentru instanțele Confluence găzduite de utilizator, cu un certificat semnat de utilizator.\",\n    },\n    manage: {\n      documents: \"Documente\",\n      \"data-connectors\": \"Conectori de date\",\n      \"desktop-only\":\n        \"Editarea acestor setări este disponibilă doar pe un dispozitiv desktop. Te rugăm să accesezi această pagină de pe desktop pentru a continua.\",\n      dismiss: \"Ignoră\",\n      editing: \"Se editează\",\n    },\n    directory: {\n      \"my-documents\": \"Documentele mele\",\n      \"new-folder\": \"Folder nou\",\n      \"search-document\": \"Căută document\",\n      \"no-documents\": \"Niciun document\",\n      \"move-workspace\": \"Mută în spațiul de lucru\",\n      \"delete-confirmation\":\n        \"Ești sigur că vrei să ștergi aceste fișiere și foldere?\\nAcest lucru va elimina fișierele din sistem și le va elimina automat din orice spațiu de lucru existent.\\nAceastă acțiune este ireversibilă.\",\n      \"removing-message\":\n        \"Se elimină {{count}} documente și {{folderCount}} foldere. Te rugăm să aștepți.\",\n      \"move-success\": \"S-au mutat cu succes {{count}} documente.\",\n      no_docs: \"Niciun document\",\n      select_all: \"Selectează tot\",\n      deselect_all: \"Deselectează tot\",\n      remove_selected: \"Elimină selectate\",\n      costs: \"*Cost unic pentru embeddings\",\n      save_embed: \"Salvează și încorporează\",\n      \"total-documents_one\": \"{{count}}\",\n      \"total-documents_other\": \"{{count}} documente\",\n    },\n    upload: {\n      \"processor-offline\": \"Procesorul de documente este offline\",\n      \"processor-offline-desc\":\n        \"Nu putem încărca fișierele tale acum deoarece procesorul de documente este offline. Te rugăm să încerci din nou mai târziu.\",\n      \"click-upload\": \"Clic pentru a încărca sau trage și plasa\",\n      \"file-types\":\n        \"suportă fișiere text, CSV-uri, foi de calcul, fișiere audio și multe altele!\",\n      \"or-submit-link\": \"sau trimite un link\",\n      \"placeholder-link\": \"https://exemplu.com\",\n      fetching: \"Se preia...\",\n      \"fetch-website\": \"Preluare site web\",\n      \"privacy-notice\":\n        \"Aceste fișiere vor fi încărcate în procesorul de documente care rulează pe această instanță AnythingLLM. Aceste fișiere nu sunt trimise sau partajate cu o terță parte.\",\n    },\n    pinning: {\n      what_pinning: \"Ce este fixarea documentelor?\",\n      pin_explained_block1:\n        \"Când **fixezi** un document în AnythingLLM, vom injecta întregul conținut al documentului în fereastra de prompt pentru ca LLM-ul tău să-l înțeleagă pe deplin.\",\n      pin_explained_block2:\n        \"Acest lucru funcționează cel mai bine cu **modele cu context mare** sau fișiere mici care sunt critice pentru baza sa de cunoștințe.\",\n      pin_explained_block3:\n        \"Dacă nu obții răspunsurile dorite de la AnythingLLM în mod implicit, atunci fixarea este o modalitate excelentă de a obține răspunsuri de calitate superioară dintr-un clic.\",\n      accept: \"Ok, am înțeles\",\n    },\n    watching: {\n      what_watching: \"Ce face vizualizarea unui document?\",\n      watch_explained_block1:\n        \"Când **urmărești** un document în AnythingLLM, vom sincroniza *automat* conținutul documentului tău din sursa originală la intervale regulate. Acest lucru va actualiza automat conținutul în fiecare spațiu de lucru unde acest fișier este gestionat.\",\n      watch_explained_block2:\n        \"Această funcție suportă în prezent conținutul online și nu va fi disponibilă pentru documentele încărcate manual.\",\n      watch_explained_block3_start:\n        \"Poți gestiona ce documente sunt urmărite din vizualizarea de administrator a \",\n      watch_explained_block3_link: \"Managerului de fișiere\",\n      watch_explained_block3_end: \".\",\n      accept: \"Ok, am înțeles\",\n    },\n  },\n  chat_window: {\n    attachments_processing:\n      \"Fișierele atașate se procesează. Te rugăm să aștepți...\",\n    send_message: \"Trimite mesaj\",\n    attach_file: \"Atașează un fișier la acest chat\",\n    text_size: \"Schimbă dimensiunea textului.\",\n    microphone: \"Vorbește promptul tău.\",\n    send: \"Trimite prompt către spațiul de lucru\",\n    tts_speak_message: \"Rostește mesajul TTS\",\n    copy: \"Copiază\",\n    regenerate: \"Regenerare\",\n    regenerate_response: \"Regenerare răspuns\",\n    good_response: \"Răspuns bun\",\n    more_actions: \"Mai multe acțiuni\",\n    fork: \"Fork\",\n    delete: \"Șterge\",\n    cancel: \"Anulează\",\n    edit_prompt: \"Editează prompt\",\n    edit_response: \"Editează răspuns\",\n    preset_reset_description:\n      \"Șterge istoricul chatului și începe o conversație nouă\",\n    add_new_preset: \" Adaugă preset nou\",\n    command: \"Comandă\",\n    your_command: \"comanda-ta\",\n    placeholder_prompt:\n      \"Acesta este conținutul care va fi injectat înaintea promptului tău.\",\n    description: \"Descriere\",\n    placeholder_description: \"Răspunde cu o poezie despre LLM-uri.\",\n    save: \"Salvează\",\n    small: \"Mic\",\n    normal: \"Normal\",\n    large: \"Mare\",\n    workspace_llm_manager: {\n      search: \"Caută furnizori LLM\",\n      loading_workspace_settings: \"Se încarcă setările spațiului de lucru...\",\n      available_models: \"Modele disponibile pentru {{provider}}\",\n      available_models_description:\n        \"Selectează un model pentru acest spațiu de lucru.\",\n      save: \"Folosește acest model\",\n      saving: \"Setez modelul ca implicit pentru spațiu de lucru...\",\n      missing_credentials: \"Acest furnizor lipsește credențiale!\",\n      missing_credentials_description: \"Click pentru a configura credențialele\",\n    },\n    submit: \"Trimite\",\n    edit_info_user:\n      \"„Trimite” recreează răspunsul generat de inteligența artificială. „Salvează” actualizează doar mesajul dumneavoastră.\",\n    edit_info_assistant:\n      \"Modificările pe care le faceți vor fi salvate direct în acest răspuns.\",\n    see_less: \"Vezi mai puțin\",\n    see_more: \"Vezi mai multe\",\n    tools: \"Unelte\",\n    browse: \"Navigați\",\n    text_size_label: \"Dimensiunea textului\",\n    select_model: \"Selectați modelul\",\n    sources: \"Surse\",\n    document: \"Document\",\n    similarity_match: \"meci\",\n    source_count_one: \"{{count}} – referință\",\n    source_count_other: \"Referințe către {{count}}\",\n    preset_exit_description: \"Întrerupeți sesiunea actuală a agentului\",\n    add_new: \"Adaugă\",\n    edit: \"Editează\",\n    publish: \"Publica\",\n    stop_generating: \"Opriți generarea răspunsului\",\n    pause_tts_speech_message:\n      \"Pauză în redarea vocii prin Text-to-Speech (TTS) a mesajului.\",\n    slash_commands: \"Comenzi scurte\",\n    agent_skills: \"Abilități ale agentului\",\n    manage_agent_skills: \"Gestionarea competențelor agenților\",\n    agent_skills_disabled_in_session:\n      \"Nu este posibil să modificați abilitățile în timpul unei sesiuni cu un agent activ. Pentru a încheia sesiunea, utilizați comanda /exit.\",\n    start_agent_session: \"Începe sesiunea de agent\",\n    use_agent_session_to_use_tools:\n      'Puteți utiliza instrumentele disponibile în chat, inițiind o sesiune cu un agent, începând mesajul cu \"@agent\".',\n  },\n  profile_settings: {\n    edit_account: \"Editează contul\",\n    profile_picture: \"Poză profil\",\n    remove_profile_picture: \"Șterge poza profil\",\n    username: \"Nume utilizator\",\n    new_password: \"Parolă nouă\",\n    password_description: \"Parola trebuie să aibă cel puțin 8 caractere\",\n    cancel: \"Anulează\",\n    update_account: \"Actualizează contul\",\n    theme: \"Preferință temă\",\n    language: \"Limba preferată\",\n    failed_upload: \"Încărcarea pozei de profil a eșuat: {{error}}\",\n    upload_success: \"Poză de profil încărcată.\",\n    failed_remove: \"Ștergerea pozei de profil a eșuat: {{error}}\",\n    profile_updated: \"Profil actualizat.\",\n    failed_update_user: \"Actualizarea utilizatorului a eșuat: {{error}}\",\n    account: \"Cont\",\n    support: \"Suport\",\n    signout: \"Deconectare\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Scurtături de tastatură\",\n    shortcuts: {\n      settings: \"Deschide setările\",\n      workspaceSettings: \"Deschide setările spațiului curent de lucru\",\n      home: \"Mergi la pagina principală\",\n      workspaces: \"Gestionează spațiile de lucru\",\n      apiKeys: \"Setările API Keys\",\n      llmPreferences: \"Preferințe LLM\",\n      chatSettings: \"Setări chat\",\n      help: \"Arată ajutor pentru scurtături de tastatură\",\n      showLLMSelector: \"Arată selectorul LLM pentru spațiu de lucru\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Succes!\",\n        success_description:\n          \"Promptul sistemului tău a fost publicat în Comunitate!\",\n        success_thank_you: \"Mulțumim pentru contribuția ta!\",\n        view_on_hub: \"Vezi pe Community Hub\",\n        modal_title: \"Publică System Prompt \",\n        name_label: \"Nume\",\n        name_description:\n          \"Acesta este numele afișat al System Prompt-ului tău.\",\n        name_placeholder: \"Asistentul meu\",\n        description_label: \"Descriere\",\n        description_description: \"Descrie scopul System Prompt-ului tău.\",\n        tags_label: \"Etichete\",\n        tags_description:\n          \"Etichetele ajută la căutarea Promptului. Max 5 etichete, max 20 caractere fiecare.\",\n        tags_placeholder: \"Tastează și apasă Enter pentru a adăuga etichete\",\n        visibility_label: \"Vizibilitate\",\n        public_description: \"Prompturile publice sunt vizibile tuturor.\",\n        private_description: \"Prompturile private sunt vizibile doar ție.\",\n        publish_button: \"Publică pe Community Hub\",\n        submitting: \"Se publică...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Acesta este promptul efectiv folosit pentru a ghida LLM-ul.\",\n        prompt_placeholder: \"Introdu System Prompt-ul aici...\",\n      },\n      agent_flow: {\n        success_title: \"Succes!\",\n        success_description:\n          \"Fluxul agentului tău a fost publicat în Comunitate!\",\n        success_thank_you: \"Mulțumim pentru contribuția ta!\",\n        view_on_hub: \"Vezi pe Community Hub\",\n        modal_title: \"Publică flux agent\",\n        name_label: \"Nume\",\n        name_description: \"Acesta este numele afișat al fluxului tău agent.\",\n        name_placeholder: \"Fluxul meu agent\",\n        description_label: \"Descriere\",\n        description_description: \"Descrie scopul fluxului tău agent.\",\n        tags_label: \"Etichete\",\n        tags_description:\n          \"Etichetele ajută la găsirea fluxului agent. Max 5 etichete, max 20 caractere fiecare.\",\n        tags_placeholder: \"Tastează și apasă Enter pentru a adăuga etichete\",\n        visibility_label: \"Vizibilitate\",\n        submitting: \"Se publică...\",\n        submit: \"Publică pe Community Hub\",\n        privacy_note:\n          \"Fluxurile agent sunt întotdeauna încărcate privat pentru a proteja datele sensibile. Poți schimba vizibilitatea după publicare. Verifică că nu conține informații sensibile înainte să publici.\",\n      },\n      slash_command: {\n        success_title: \"Succes!\",\n        success_description: \"Comanda slash ta a fost publicată în Comunitate!\",\n        success_thank_you: \"Mulțumim pentru contribuția ta!\",\n        view_on_hub: \"Vezi pe Community Hub\",\n        modal_title: \"Publică comandă slash\",\n        name_label: \"Nume\",\n        name_description: \"Acesta este numele afișat al comenzii tale slash.\",\n        name_placeholder: \"Comanda mea slash\",\n        description_label: \"Descriere\",\n        description_description: \"Descrie scopul comenzii tale slash.\",\n        tags_label: \"Etichete\",\n        tags_description:\n          \"Etichetele ajută la găsirea comenzii. Max 5 etichete, max 20 caractere fiecare.\",\n        tags_placeholder: \"Tastează și apasă Enter pentru a adăuga etichete\",\n        visibility_label: \"Vizibilitate\",\n        public_description: \"Comenzile slash publice sunt vizibile tuturor.\",\n        private_description: \"Comenzile slash private sunt vizibile doar ție.\",\n        publish_button: \"Publică pe Community Hub\",\n        submitting: \"Se publică...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Acesta este promptul folosit când se declanșează comanda slash.\",\n        prompt_placeholder: \"Introdu promptul aici...\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Autentificare necesară\",\n          description:\n            \"Trebuie să te autentifici cu AnythingLLM Community Hub înainte de a publica elemente.\",\n          button: \"Conectează-te la Community Hub\",\n        },\n      },\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Identificator bază de date vectorială\",\n    snippets: {\n      title: \"Număr maxim de fragmente de context\",\n      description:\n        \"Această setare controlează cantitatea maximă de fragmente de context care vor fi trimise către LLM per chat sau interogare (query).\",\n      recommend: \"Recomandat\",\n    },\n    doc: {\n      title: \"Prag de similaritate document\",\n      description:\n        \"Scorul minim de similaritate necesar pentru ca o sursă să fie considerată relevantă pentru conversație (chat). Cu cât numărul este mai mare, cu atât sursa trebuie să fie mai asemănătoare cu conversația (chat).\",\n      zero: \"Fără restricții\",\n      low: \"Scăzut (scor de similaritate ≥ .25)\",\n      medium: \"Mediu (scor de similaritate ≥ .50)\",\n      high: \"Înalt (scor de similaritate ≥ .75)\",\n    },\n    reset: {\n      reset: \"Resetează baza de date vectorială\",\n      resetting: \"Se șterg vectorii...\",\n      confirm:\n        \"Sunteți pe cale să resetați baza de date vectorială a acestui spațiu de lucru. Această acțiune va elimina toate încorporările vectoriale aflate în prezent în bază.\\n\\nFișierele sursă originale vor rămâne intacte. Această acțiune este ireversibilă.\",\n      error:\n        \"Baza de date vectorială a spațiului de lucru nu a putut fi resetată!\",\n      success: \"Baza de date vectorială a spațiului de lucru a fost resetată!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"Performanța LLM-urilor care nu suportă explicit apelarea de instrumente depinde în mare măsură de capabilitățile și acuratețea modelului. Unele abilități pot fi limitate sau nefuncționale.\",\n    provider: {\n      title: \"Furnizor LLM agent spațiu de lucru\",\n      description:\n        \"Furnizorul LLM și modelul specific care vor fi utilizate pentru agentul @agent al acestui spațiu de lucru.\",\n    },\n    mode: {\n      chat: {\n        title: \"Model de chat agent spațiu de lucru\",\n        description:\n          \"Modelul de chat specific care va fi utilizat pentru agentul @agent al acestui spațiu de lucru.\",\n      },\n      title: \"Model agent spațiu de lucru\",\n      description:\n        \"Modelul LLM specific care va fi utilizat pentru agentul @agent al acestui spațiu de lucru.\",\n      wait: \"-- se așteaptă modele --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG & memorie pe termen lung\",\n        description:\n          \"Permite agentului să valorifice documentele dumneavoastră locale pentru a răspunde la o interogare sau cereți-i agentului să „rețină” fragmente de conținut pentru a le putea recupera ulterior din memoria pe termen lung.\",\n      },\n      view: {\n        title: \"Vizualizează & rezumă documente\",\n        description:\n          \"Permite agentului să listeze și să rezume conținutul fișierelor din spațiul de lucru încorporate în prezent.\",\n      },\n      scrape: {\n        title: \"Extrage date de pe site-uri web (prin web scraping)\",\n        description:\n          \"Permite agentului să viziteze și să extragă conținutul site-urilor web (prin web scraping).\",\n      },\n      generate: {\n        title: \"Generează grafice\",\n        description:\n          \"Permite agentului implicit să genereze diverse tipuri de grafice din datele furnizate sau date în chat.\",\n      },\n      save: {\n        title: \"Generează & salvează fișiere în browser\",\n        description:\n          \"Permite agentului implicit să genereze și să scrie fișiere care se salvează și pot fi descărcate în browserul tău.\",\n      },\n      web: {\n        title: \"Căutare și navigare web live\",\n        description:\n          \"Permite-i agentului tău să caute pe internet pentru a răspunde la întrebările tale, conectându-l la un furnizor de servicii de căutare web (SERP).\",\n      },\n      sql: {\n        title: \"Conector SQL\",\n        description:\n          \"Permite-ți agentului să utilizeze SQL pentru a răspunde la întrebările tale, conectându-se la diverși furnizori de baze de date SQL.\",\n      },\n      default_skill:\n        \"Implicit, această funcție este activată, dar puteți dezactiva-o dacă nu doriți ca agentul să o utilizeze.\",\n    },\n    mcp: {\n      title: \"Servere MCP\",\n      \"loading-from-config\":\n        \"Încărcarea serverelor MCP din fișierul de configurare\",\n      \"learn-more\": \"Aflați mai multe despre serverele MCP.\",\n      \"no-servers-found\": \"Nu au fost găsite servere MCP.\",\n      \"tool-warning\":\n        \"Pentru cele mai bune rezultate, luați în considerare dezactivarea instrumentelor nedorite, pentru a economisi resurse.\",\n      \"stop-server\": \"Închideți serverul MCP\",\n      \"start-server\": \"Pornește serverul MCP\",\n      \"delete-server\": \"Șterge serverul MCP\",\n      \"tool-count-warning\":\n        \"Acest server MCP are activate<b> instrumentele menționate</b>, care vor consuma context în fiecare sesiune de chat.<br />Luați în considerare dezactivarea instrumentelor nedorite pentru a economisi context.\",\n      \"startup-command\": \"Comanda de pornire\",\n      command: \"Ordine\",\n      arguments: \"Argumente\",\n      \"not-running-warning\":\n        \"Acest server MCP nu este în funcționare – ar putea fi oprit sau ar putea întâmpina o eroare la pornire.\",\n      \"tool-call-arguments\": \"Argumente pentru apelarea unei funcții\",\n      \"tools-enabled\": \"instrumentele sunt activate\",\n    },\n    settings: {\n      title: \"Setări pentru abilitățile agenților\",\n      \"max-tool-calls\": {\n        title:\n          \"Numărul maxim de solicitări de instrument (Max Tool Calls Per Response)\",\n        description:\n          \"Numărul maxim de instrumente pe care un agent le poate utiliza în mod consecutiv pentru a genera un singur răspuns. Această funcție previne apelurile inutile ale instrumentelor și buclele infinite.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Selecție inteligentă a abilităților\",\n        \"beta-badge\": \"Beta\",\n        description:\n          \"Permite utilizarea nelimitată a instrumentelor și reduce utilizarea token-urilor cu până la 80% pentru fiecare interogare – AnythingLLM selectează automat abilitățile potrivite pentru fiecare solicitare.\",\n        \"max-tools\": {\n          title: \"Max Tools\",\n          description:\n            \"Numărul maxim de instrumente care pot fi selectate pentru fiecare interogare. Recomandăm stabilirea acestui parametru la valori mai mari pentru modelele cu un context mai amplu.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Conversații spațiu de lucru\",\n    description:\n      \"Acestea sunt toate conversațiile și mesajele înregistrate care au fost trimise de utilizatori, ordonate după data creării.\",\n    export: \"Exportă\",\n    table: {\n      id: \"ID\",\n      by: \"Trimis de\",\n      workspace: \"Spațiu de lucru\",\n      prompt: \"Prompt\",\n      response: \"Răspuns\",\n      at: \"Trimis la\",\n    },\n  },\n  customization: {\n    interface: {\n      title: \"Preferințe UI\",\n      description: \"Setează preferințele UI pentru AnythingLLM.\",\n    },\n    branding: {\n      title: \"Branding & White-labeling\",\n      description:\n        \"Personalizează-ți instanța AnythingLLM cu branding personalizat.\",\n    },\n    chat: {\n      title: \"Chat\",\n      description: \"Setează preferințele de chat pentru AnythingLLM.\",\n      auto_submit: {\n        title: \"Trimite automat intrarea vocală\",\n        description:\n          \"Trimite automat intrarea vocală după o perioadă de liniște\",\n      },\n      auto_speak: {\n        title: \"Rostește automat răspunsurile\",\n        description: \"Rostește automat răspunsurile de la AI\",\n      },\n      spellcheck: {\n        title: \"Activează verificarea ortografică\",\n        description:\n          \"Activează sau dezactivează verificarea ortografică în câmpul de introducere a chatului\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Temă\",\n        description: \"Selectează tema de culoare preferată pentru aplicație.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Arată bara de derulare\",\n        description:\n          \"Activează sau dezactivează bara de derulare în fereastra de chat.\",\n      },\n      \"support-email\": {\n        title: \"Email de suport\",\n        description:\n          \"Setează adresa de email de suport care ar trebui să fie accesibilă utilizatorilor atunci când au nevoie de ajutor.\",\n      },\n      \"app-name\": {\n        title: \"Nume aplicație\",\n        description:\n          \"Setează un nume care este afișat pe pagina de autentificare tuturor utilizatorilor.\",\n      },\n      \"display-language\": {\n        title: \"Limba de afișare\",\n        description:\n          \"Selectează limba preferată pentru a reda interfața AnythingLLM - atunci când traducerile sunt disponibile.\",\n      },\n      logo: {\n        title: \"Logo brand\",\n        description:\n          \"Încarcă logo-ul tău personalizat pentru a fi afișat pe toate paginile.\",\n        add: \"Adaugă un logo personalizat\",\n        recommended: \"Dimensiune recomandată: 800 x 200\",\n        remove: \"Elimină\",\n        replace: \"Înlocuiește\",\n      },\n      \"browser-appearance\": {\n        title: \"Aspect browser\",\n        description:\n          \"Personalizează aspectul tabului și titlului browserului când aplicația este deschisă.\",\n        tab: {\n          title: \"Titlu\",\n          description:\n            \"Setează un titlu personalizat pentru tab când aplicația este deschisă într-un browser.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description:\n            \"Folosește un favicon personalizat pentru tabul browserului.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Elemente subsol bară laterală\",\n        description:\n          \"Personalizează elementele din subsol afișate în partea de jos a barei laterale.\",\n        icon: \"Iconiță\",\n        link: \"Link\",\n      },\n      \"render-html\": {\n        title: \"Redarea HTML în chat\",\n        description:\n          \"Afișarea răspunsurilor HTML în răspunsurile asistentului.\\nAcest lucru poate duce la o calitate a răspunsurilor mult mai bună, dar poate și la riscuri potențiale de securitate.\",\n      },\n    },\n  },\n  api: {\n    title: \"Chei API\",\n    description:\n      \"Cheile API permit deținătorului să acceseze și să gestioneze programatic această instanță AnythingLLM.\",\n    link: \"Citește documentația API\",\n    generate: \"Generează o nouă cheie API\",\n    table: {\n      key: \"Cheie API\",\n      by: \"Creat de\",\n      created: \"Creat la\",\n    },\n  },\n  llm: {\n    title: \"Preferința LLM\",\n    description:\n      \"Acestea sunt credențialele și setările pentru furnizorul tău preferat de chat și embedding LLM. Este important ca aceste chei să fie actuale și corecte, altfel AnythingLLM nu va funcționa corect.\",\n    provider: \"Furnizor LLM\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Endpoint serviciu Azure\",\n        api_key: \"Cheie API\",\n        chat_deployment_name: \"Nume implementare chat\",\n        chat_model_token_limit: \"Limita token model chat\",\n        model_type: \"Tip model\",\n        default: \"Implicit\",\n        reasoning: \"Raționament\",\n        model_type_tooltip:\n          \"Dacă implementarea dvs. utilizează un model de raționament (o1, o1-mini, o3-mini, etc.), setați această opțiune la „Raționament”. În caz contrar, cererile dvs. de chat pot eșua.\",\n      },\n    },\n  },\n  transcription: {\n    title: \"Preferința modelului de transcriere\",\n    description:\n      \"Acestea sunt credențialele și setările pentru furnizorul tău preferat de model de transcriere. Este important ca aceste chei să fie actuale și corecte, altfel fișierele media și audio nu vor fi transcrise.\",\n    provider: \"Furnizor transcriere\",\n    \"warn-start\":\n      \"Utilizarea modelului local Whisper pe mașini cu RAM sau CPU limitat poate bloca AnythingLLM la procesarea fișierelor media.\",\n    \"warn-recommend\":\n      \"Recomandăm cel puțin 2GB de RAM și încărcarea fișierelor <10Mb.\",\n    \"warn-end\": \"Modelul încorporat se va descărca automat la prima utilizare.\",\n  },\n  embedding: {\n    title: \"Preferință embedding\",\n    \"desc-start\":\n      \"Atunci când utilizați un LLM care nu suportă nativ un motor de embedding - s-ar putea să fie necesar să specificați credențiale suplimentare pentru embedding text.\",\n    \"desc-end\":\n      \"Embedding-ul este procesul de transformare a textului în vectori. Aceste credențiale sunt necesare pentru a transforma fișierele și prompturile dvs. într-un format pe care AnythingLLM îl poate utiliza pentru procesare.\",\n    provider: {\n      title: \"Furnizor embedding\",\n    },\n  },\n  text: {\n    title: \"Preferințe de împărțire și fragmentare text\",\n    \"desc-start\":\n      \"Uneori, s-ar putea să doriți să modificați modul implicit în care documentele noi sunt împărțite și fragmentate înainte de a fi inserate în baza de date vectorială.\",\n    \"desc-end\":\n      \"Ar trebui să modificați această setare doar dacă înțelegeți cum funcționează împărțirea textului și efectele sale secundare.\",\n    size: {\n      title: \"Dimensiune fragment text\",\n      description:\n        \"Aceasta este lungimea maximă de caractere care poate fi prezentă într-un singur vector.\",\n      recommend: \"Lungimea maximă a modelului de embedding este\",\n    },\n    overlap: {\n      title: \"Suprapunere fragment text\",\n      description:\n        \"Aceasta este suprapunerea maximă de caractere care apare în timpul fragmentării între două fragmente de text adiacente.\",\n    },\n  },\n  security: {\n    title: \"Securitate\",\n    multiuser: {\n      title: \"Mod multi-utilizator\",\n      description:\n        \"Configurează instanța ta să suporte echipa activând modul multi-utilizator.\",\n      enable: {\n        \"is-enable\": \"Modul multi-utilizator este activat\",\n        enable: \"Activează modul multi-utilizator\",\n        description:\n          \"Implicit, vei fi singurul administrator. Ca administrator, va trebui să creezi conturi pentru toți utilizatorii sau administratorii noi. Nu pierde parola, deoarece doar un utilizator administrator poate reseta parolele.\",\n        username: \"Numele contului de administrator\",\n        password: \"Parola contului de administrator\",\n      },\n    },\n    password: {\n      title: \"Protecție prin parolă\",\n      description:\n        \"Protejează instanța AnythingLLM cu o parolă. Dacă o uiți, nu există metode de recuperare, deci asigură-te că o salvezi.\",\n      \"password-label\": \"Parola instanței\",\n    },\n  },\n  home: {\n    welcome: \"Bine ai venit\",\n    chooseWorkspace: \"Alege un spațiu de lucru pentru a începe să chatezi!\",\n    notAssigned:\n      \"Momentan nu te-ai atribuit la niciun spațiu de lucru.\\nContactează-ți administratorul pentru a solicita acces la un spațiu de lucru.\",\n    goToWorkspace: 'Mai departe la spațiul de lucru \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/ru/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"Добро пожаловать в\",\n      getStarted: \"Начать\",\n    },\n    llm: {\n      title: \"Предпочитаемые LLM\",\n      description:\n        \"AnythingLLM может работать с различными провайдерами LLM. Этот сервис будет обеспечивать обработку чата.\",\n    },\n    userSetup: {\n      title: \"Настройка пользователя\",\n      description: \"Настройте параметры пользователя.\",\n      howManyUsers: \"Сколько пользователей будут использовать этот экземпляр?\",\n      justMe: \"Только я\",\n      myTeam: \"Моя команда\",\n      instancePassword: \"Пароль экземпляра\",\n      setPassword: \"Хотите установить пароль?\",\n      passwordReq: \"Пароль должен содержать не менее 8 символов.\",\n      passwordWarn:\n        \"Важно сохранить этот пароль, так как способа его восстановления не существует.\",\n      adminUsername: \"Имя пользователя для учётной записи администратора\",\n      adminPassword: \"Пароль для учётной записи администратора\",\n      adminPasswordReq: \"Пароль должен содержать не менее 8 символов.\",\n      teamHint:\n        \"По умолчанию, вы будете единственным администратором. После завершения настройки вы сможете создавать учётные записи и приглашать других пользователей или администраторов. Не потеряйте пароль, так как только администраторы могут его сбросить.\",\n    },\n    data: {\n      title: \"Обработка данных и конфиденциальность\",\n      description:\n        \"Мы стремимся обеспечить прозрачность и контроль в отношении ваших персональных данных.\",\n      settingsHint: \"Эти настройки можно изменить в любое время в настройках.\",\n    },\n    survey: {\n      title: \"Добро пожаловать в AnythingLLM\",\n      description:\n        \"Помогите нам сделать AnythingLLM, созданным с учётом ваших потребностей (необязательно).\",\n      email: \"Какой у вас адрес электронной почты?\",\n      useCase: \"Для чего вы будете использовать AnythingLLM?\",\n      useCaseWork: \"Для работы\",\n      useCasePersonal: \"Для личного использования\",\n      useCaseOther: \"Другое\",\n      comment: \"Откуда вы узнали о AnythingLLM?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube и т.д. — сообщите, где вы о нас узнали!\",\n      skip: \"Пропустить опрос\",\n      thankYou: \"Спасибо за ваш отзыв!\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Имя рабочих пространств\",\n    user: \"Пользователь\",\n    selection: \"Выбор модели\",\n    saving: \"Сохранение...\",\n    save: \"Сохранить изменения\",\n    previous: \"Предыдущая страница\",\n    next: \"Следующая страница\",\n    optional: \"Необязательный\",\n    yes: \"Да\",\n    no: \"Нет\",\n    search: \"Поиск\",\n    username_requirements:\n      \"Имя пользователя должно содержать от 2 до 32 символов, начинаться со строчной буквы и содержать только строчные буквы, цифры, символы подчёркивания, дефисы и точки.\",\n    on: \"О\",\n    none: \"Нет\",\n    stopped: \"Остановлен\",\n    loading: \"Загрузка\",\n    refresh: \"Обновить\",\n  },\n  settings: {\n    title: \"Настройки экземпляра\",\n    invites: \"Приглашение\",\n    users: \"Пользователи\",\n    workspaces: \"Рабочие пространства\",\n    \"workspace-chats\": \"Чат рабочего пространства\",\n    customization: \"Внешний вид\",\n    \"api-keys\": \"API ключи\",\n    llm: \"Предпочтение LLM\",\n    transcription: \"Модель транскрипции\",\n    embedder: \"Настройки встраивания\",\n    \"text-splitting\": \"Разделение и сегментация текста\",\n    \"voice-speech\": \"Голос и Речь\",\n    \"vector-database\": \"Векторная база данных\",\n    embeds: \"Виджеты встраивания чата\",\n    security: \"Безопасность\",\n    \"event-logs\": \"Журналы событий\",\n    privacy: \"Конфиденциальность и данные\",\n    \"ai-providers\": \"Поставщики ИИ\",\n    \"agent-skills\": \"Навыки агента\",\n    admin: \"Администратор\",\n    tools: \"Инструменты\",\n    \"experimental-features\": \"Экспериментальные функции\",\n    contact: \"Связаться с Поддержкой\",\n    \"browser-extension\": \"Расширение браузера\",\n    \"system-prompt-variables\": \"Переменные системного запроса\",\n    interface: \"Предпочтения в пользовательском интерфейсе\",\n    branding: \"Брендинг и создание продуктов с собственной меткой.\",\n    chat: \"Чат\",\n    \"mobile-app\": \"AnythingLLM Mobile\",\n    \"community-hub\": {\n      title: \"Центр сообщества\",\n      trending: \"Изучите популярные темы\",\n      \"your-account\": \"Ваш аккаунт\",\n      \"import-item\": \"Импорт товара\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Добро пожаловать в\",\n      \"placeholder-username\": \"Имя пользователя\",\n      \"placeholder-password\": \"Пароль\",\n      login: \"Войти\",\n      validating: \"Проверка...\",\n      \"forgot-pass\": \"Забыли пароль\",\n      reset: \"Сбросить\",\n    },\n    \"sign-in\": \"Войти в ваш {{appName}} аккаунт.\",\n    \"password-reset\": {\n      title: \"Сброс пароля\",\n      description:\n        \"Предоставьте необходимую информацию ниже, чтобы сбросить ваш пароль.\",\n      \"recovery-codes\": \"Коды восстановления\",\n      \"back-to-login\": \"Вернуться к входу\",\n    },\n  },\n  \"new-workspace\": {\n    title: \"Новая Рабочая Область\",\n    placeholder: \"Моя Рабочая Область\",\n  },\n  \"workspaces—settings\": {\n    general: \"Общие настройки\",\n    chat: \"Настройки чата\",\n    vector: \"Векторная база данных\",\n    members: \"Участники\",\n    agent: \"Конфигурация агента\",\n  },\n  general: {\n    vector: {\n      title: \"Количество векторов\",\n      description: \"Общее количество векторов в вашей векторной базе данных.\",\n    },\n    names: {\n      description:\n        \"Это изменит только отображаемое имя вашего рабочего пространства.\",\n    },\n    message: {\n      title: \"Предлагаемые сообщения чата\",\n      description:\n        \"Настройте сообщения, которые будут предложены пользователям вашего рабочего пространства.\",\n      add: \"Добавить новое сообщение\",\n      save: \"Сохранить сообщения\",\n      heading: \"Объясните мне\",\n      body: \"преимущества AnythingLLM\",\n    },\n    delete: {\n      title: \"Удалить Рабочее Пространство\",\n      description:\n        \"Удалите это рабочее пространство и все его данные. Это удалит рабочее пространство для всех пользователей.\",\n      delete: \"Удалить рабочее пространство\",\n      deleting: \"Удаление рабочего пространства...\",\n      \"confirm-start\": \"Вы собираетесь удалить весь ваш\",\n      \"confirm-end\":\n        \"рабочее пространство. Это удалит все векторные встраивания в вашей векторной базе данных.\\n\\nОригинальные исходные файлы останутся нетронутыми. Это действие необратимо.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Поставщик LLM рабочего пространства\",\n      description:\n        \"Конкретный поставщик и модель LLM, которые будут использоваться для этого рабочего пространства. По умолчанию используется системный поставщик и настройки LLM.\",\n      search: \"Искать всех поставщиков LLM\",\n    },\n    model: {\n      title: \"Модель чата рабочего пространства\",\n      description:\n        \"Конкретная модель чата, которая будет использоваться для этого рабочего пространства. Если пусто, будет использоваться системное предпочтение LLM.\",\n    },\n    mode: {\n      title: \"Режим чата\",\n      chat: {\n        title: \"Чат\",\n        description:\n          \"предоставит ответы, используя общие знания, содержащиеся в LLM, и контекст документа, который был предоставлен.<br />Для использования инструментов необходимо использовать команду @agent.\",\n      },\n      query: {\n        title: \"Запрос\",\n        description:\n          \"предоставит ответы <b>только в том случае, если будет найден контекст документа.</b>Для использования инструментов необходимо использовать команду @agent.\",\n      },\n      automatic: {\n        title: \"Авто\",\n        description:\n          \"автоматически будет использовать инструменты, если модель и поставщик поддерживают вызов инструментов. <br />Если вызов инструментов не поддерживается, вам потребуется использовать команду `@agent` для использования инструментов.\",\n      },\n    },\n    history: {\n      title: \"История чата\",\n      \"desc-start\":\n        \"Количество предыдущих чатов, которые будут включены в краткосрочную память ответа.\",\n      recommend: \"Рекомендуем 20.\",\n      \"desc-end\":\n        \"Любое количество более 45 может привести к непрерывным сбоям чата в зависимости от размера сообщений.\",\n    },\n    prompt: {\n      title: \"Подсказка\",\n      description:\n        \"Подсказка, которая будет использоваться в этом рабочем пространстве. Определите контекст и инструкции для AI для создания ответа. Вы должны предоставить тщательно разработанную подсказку, чтобы AI мог генерировать релевантный и точный ответ.\",\n      history: {\n        title: \"История запросов системы\",\n        clearAll: \"Очистить всё\",\n        noHistory: \"Информация о предыдущих запросах недоступна.\",\n        restore: \"Восстановить\",\n        delete: \"Удалить\",\n        deleteConfirm: \"Вы уверены, что хотите удалить этот элемент истории?\",\n        clearAllConfirm:\n          \"Вы уверены, что хотите очистить всю историю? Это действие нельзя отменить.\",\n        expand: \"Расширять\",\n        publish: \"Опубликовать в Центре сообщества\",\n      },\n    },\n    refusal: {\n      title: \"Ответ об отказе в режиме запроса\",\n      \"desc-start\": \"В режиме\",\n      query: \"запроса\",\n      \"desc-end\":\n        \"вы можете вернуть пользовательский ответ об отказе, если контекст не найден.\",\n      \"tooltip-title\": \"Почему я это вижу?\",\n      \"tooltip-description\":\n        \"Вы находитесь в режиме запроса, который использует только информацию из ваших документов. Переключитесь в режим чата для более гибких бесед, или нажмите здесь, чтобы посетить нашу документацию и узнать больше о режимах чата.\",\n    },\n    temperature: {\n      title: \"Температура LLM\",\n      \"desc-start\":\n        \"Этот параметр контролирует, насколько 'креативными' будут ответы вашего LLM.\",\n      \"desc-end\":\n        \"Чем выше число, тем более креативные ответы. Для некоторых моделей это может привести к несвязным ответам при слишком высоких настройках.\",\n      hint: \"Большинство LLM имеют различные допустимые диапазоны значений. Проконсультируйтесь с вашим поставщиком LLM для получения этой информации.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Идентификатор векторной базы данных\",\n    snippets: {\n      title: \"Максимальное количество контекстных фрагментов\",\n      description:\n        \"Этот параметр контролирует максимальное количество контекстных фрагментов, которые будут отправлены LLM для каждого чата или запроса.\",\n      recommend: \"Рекомендуемое количество: 4\",\n    },\n    doc: {\n      title: \"Порог сходства документов\",\n      description:\n        \"Минимальная оценка сходства, необходимая для того, чтобы источник считался связанным с чатом. Чем выше число, тем более схожим должен быть источник с чатом.\",\n      zero: \"Без ограничений\",\n      low: \"Низкий (оценка сходства ≥ .25)\",\n      medium: \"Средний (оценка сходства ≥ .50)\",\n      high: \"Высокий (оценка сходства ≥ .75)\",\n    },\n    reset: {\n      reset: \"Сброс векторной базы данных\",\n      resetting: \"Очистка векторов...\",\n      confirm:\n        \"Вы собираетесь сбросить векторную базу данных этого рабочего пространства. Это удалит все текущие векторные встраивания.\\n\\nОригинальные исходные файлы останутся нетронутыми. Это действие необратимо.\",\n      error: \"Не удалось сбросить векторную базу данных рабочего пространства!\",\n      success: \"Векторная база данных рабочего пространства была сброшена!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"Производительность LLM, не поддерживающих вызовы инструментов, сильно зависит от возможностей и точности модели. Некоторые способности могут быть ограничены или не функционировать.\",\n    provider: {\n      title: \"Поставщик LLM агента рабочего пространства\",\n      description:\n        \"Конкретный поставщик и модель LLM, которые будут использоваться для агента @agent этого рабочего пространства.\",\n    },\n    mode: {\n      chat: {\n        title: \"Модель чата агента рабочего пространства\",\n        description:\n          \"Конкретная модель чата, которая будет использоваться для агента @agent этого рабочего пространства.\",\n      },\n      title: \"Модель агента рабочего пространства\",\n      description:\n        \"Конкретная модель LLM, которая будет использоваться для агента @agent этого рабочего пространства.\",\n      wait: \"-- ожидание моделей --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG и долговременная память\",\n        description:\n          \"Позвольте агенту использовать ваши локальные документы для ответа на запрос или попросите агента 'запомнить' части контента для долгосрочного извлечения из памяти.\",\n      },\n      view: {\n        title: \"Просмотр и резюмирование документов\",\n        description:\n          \"Позвольте агенту перечислять и резюмировать содержание файлов рабочего пространства, которые в данный момент встроены.\",\n      },\n      scrape: {\n        title: \"Сбор данных с веб-сайтов\",\n        description:\n          \"Позвольте агенту посещать и собирать содержимое веб-сайтов.\",\n      },\n      generate: {\n        title: \"Создание диаграмм\",\n        description:\n          \"Включите возможность создания различных типов диаграмм из предоставленных данных или данных, указанных в чате.\",\n      },\n      save: {\n        title: \"Создание и сохранение файлов в браузер\",\n        description:\n          \"Включите возможность создания и записи файлов, которые можно сохранить и загрузить в вашем браузере.\",\n      },\n      web: {\n        title: \"Поиск в Интернете и просмотр в реальном времени\",\n        description:\n          \"Предоставьте вашему агенту возможность искать информацию в интернете, чтобы отвечать на ваши вопросы, подключившись к провайдеру поисковой системы (SERP).\",\n      },\n      sql: {\n        title: \"Драйвер для работы с базой данных SQL\",\n        description:\n          \"Позвольте вашему агенту использовать SQL для ответа на ваши вопросы, подключившись к различным провайдерам баз данных.\",\n      },\n      default_skill:\n        \"По умолчанию, эта функция включена, но вы можете отключить ее, если не хотите, чтобы она была доступна для агента.\",\n    },\n    mcp: {\n      title: \"Серверы MCP\",\n      \"loading-from-config\": \"Загрузка серверов MCP из конфигурационного файла\",\n      \"learn-more\": \"Узнайте больше о серверах MCP.\",\n      \"no-servers-found\": \"Не найдено серверов MCP.\",\n      \"tool-warning\":\n        \"Для достижения наилучших результатов, рассмотрите возможность отключения неиспользуемых инструментов, чтобы сохранить контекст.\",\n      \"stop-server\": \"Остановить сервер MCP\",\n      \"start-server\": \"Запустить сервер MCP\",\n      \"delete-server\": \"Удалить сервер MCP\",\n      \"tool-count-warning\":\n        \"Этот сервер MCP имеет включенные <b> инструменты, которые потребляют контекст в каждом чате.</b> Рассмотрите возможность отключения нежелательных инструментов для экономии контекста.\",\n      \"startup-command\": \"Команда для запуска\",\n      command: \"Приказ\",\n      arguments: \"Аргументы\",\n      \"not-running-warning\":\n        \"Этот сервер MCP не работает – он может быть остановлен или возникла ошибка при запуске.\",\n      \"tool-call-arguments\": \"Аргументы для вызова функции\",\n      \"tools-enabled\": \"инструменты включены/активированы\",\n    },\n    settings: {\n      title: \"Настройки навыков агента\",\n      \"max-tool-calls\": {\n        title: \"Максимальное количество запросов к инструменту в одном ответе\",\n        description:\n          \"Максимальное количество инструментов, которые агент может использовать последовательно для генерации одного ответа. Это предотвращает чрезмерное использование инструментов и бесконечные циклы.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Интеллектуальный выбор навыков\",\n        \"beta-badge\": \"Бета-версия\",\n        description:\n          \"Позволяет использовать неограниченное количество инструментов и сократить использование токенов до 80% на запрос – AnythingLLM автоматически выбирает наиболее подходящие навыки для каждого запроса.\",\n        \"max-tools\": {\n          title: \"Инструменты Max\",\n          description:\n            \"Максимальное количество инструментов, которые можно выбрать для каждого запроса. Мы рекомендуем устанавливать это значение на более высокие значения для моделей с большим контекстом.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Чаты рабочего пространства\",\n    description:\n      \"Это все записанные чаты и сообщения, отправленные пользователями, упорядоченные по дате создания.\",\n    export: \"Экспорт\",\n    table: {\n      id: \"Идентификатор\",\n      by: \"Отправлено\",\n      workspace: \"Рабочее пространство\",\n      prompt: \"Подсказка\",\n      response: \"Ответ\",\n      at: \"Отправлено в\",\n    },\n  },\n  api: {\n    title: \"API ключи\",\n    description:\n      \"API ключи позволяют владельцу программно получать доступ к этому экземпляру AnythingLLM и управлять им.\",\n    link: \"Прочитать документацию по API\",\n    generate: \"Создать новый API ключ\",\n    table: {\n      key: \"API ключ\",\n      by: \"Создано\",\n      created: \"Создано\",\n    },\n  },\n  llm: {\n    title: \"Предпочтение LLM\",\n    description:\n      \"Это учетные данные и настройки для вашего предпочтительного поставщика чата и встраивания LLM. Важно, чтобы эти ключи были актуальными и правильными, иначе AnythingLLM не будет работать должным образом.\",\n    provider: \"Поставщик LLM\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Endpoint для сервиса Azure\",\n        api_key: \"Ключ API\",\n        chat_deployment_name: \"Название развертывания чата\",\n        chat_model_token_limit: \"Предел токенов для чата\",\n        model_type: \"Тип модели\",\n        default: \"Стандартные настройки.\",\n        reasoning: \"Обоснование\",\n        model_type_tooltip:\n          'Если ваш проект использует модель рассуждения (например, o1, o1-mini, o3-mini и т.д.), установите этот параметр в значение \"Reasoning\". В противном случае, ваши запросы в чат могут не выполняться.',\n      },\n    },\n  },\n  transcription: {\n    title: \"Предпочтение модели транскрипции\",\n    description:\n      \"Это учетные данные и настройки для вашего предпочтительного поставщика моделей транскрипции. Важно, чтобы эти ключи были актуальными и правильными, иначе медиафайлы и аудио не будут транскрибироваться.\",\n    provider: \"Поставщик транскрипции\",\n    \"warn-start\":\n      \"Использование локальной модели whisper на машинах с ограниченной оперативной памятью или процессором может привести к зависанию AnythingLLM при обработке медиафайлов.\",\n    \"warn-recommend\":\n      \"Мы рекомендуем минимум 2ГБ оперативной памяти и загружать файлы <10МБ.\",\n    \"warn-end\":\n      \"Встроенная модель будет автоматически загружена при первом использовании.\",\n  },\n  embedding: {\n    title: \"Настройки встраивания\",\n    \"desc-start\":\n      \"При использовании LLM, который не поддерживает встроенный механизм встраивания - возможно, потребуется дополнительно указать учетные данные для встраивания текста.\",\n    \"desc-end\":\n      \"Встраивание - это процесс превращения текста в векторы. Эти учетные данные необходимы для превращения ваших файлов и подсказок в формат, который AnythingLLM может использовать для обработки.\",\n    provider: {\n      title: \"Поставщик встраивания\",\n    },\n  },\n  text: {\n    title: \"Настройки разделения и сегментации текста\",\n    \"desc-start\":\n      \"Иногда может понадобиться изменить стандартный способ разделения и сегментации новых документов перед их вставкой в векторную базу данных.\",\n    \"desc-end\":\n      \"Следует изменять этот параметр только при полном понимании работы разделения текста и его побочных эффектов.\",\n    size: {\n      title: \"Размер сегмента текста\",\n      description:\n        \"Это максимальная длина символов, которые могут присутствовать в одном векторе.\",\n      recommend: \"Максимальная длина модели встраивания составляет\",\n    },\n    overlap: {\n      title: \"Перекрытие сегментов текста\",\n      description:\n        \"Это максимальное перекрытие символов, которое происходит при сегментации между двумя смежными сегментами текста.\",\n    },\n  },\n  vector: {\n    title: \"Векторная база данных\",\n    description:\n      \"Это учетные данные и настройки для того, как будет функционировать ваш экземпляр AnythingLLM. Важно, чтобы эти ключи были актуальными и правильными.\",\n    provider: {\n      title: \"Поставщик векторной базы данных\",\n      description: \"Настройка для LanceDB не требуется.\",\n    },\n  },\n  embeddable: {\n    title: \"Встраиваемые виджеты чата\",\n    description:\n      \"Встраиваемые виджеты чата - это интерфейсы чата, ориентированные на публичное использование и привязанные к одному рабочему пространству. Они позволяют создавать рабочие пространства, которые затем можно публиковать в Интернете.\",\n    create: \"Создать встраивание\",\n    table: {\n      workspace: \"Рабочее пространство\",\n      chats: \"Отправленные чаты\",\n      active: \"Активные домены\",\n      created: \"Создано\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Встраивание чатов\",\n    export: \"Экспорт\",\n    description:\n      \"Это все записанные чаты и сообщения от любого встраивания, которое вы опубликовали.\",\n    table: {\n      embed: \"Встраивание\",\n      sender: \"Отправитель\",\n      message: \"Сообщение\",\n      response: \"Ответ\",\n      at: \"Отправлено в\",\n    },\n  },\n  event: {\n    title: \"Журналы событий\",\n    description:\n      \"Просматривайте все действия и события, происходящие в этом экземпляре для мониторинга.\",\n    clear: \"Очистить журналы событий\",\n    table: {\n      type: \"Тип события\",\n      user: \"Пользователь\",\n      occurred: \"Произошло в\",\n    },\n  },\n  privacy: {\n    title: \"Конфиденциальность и обработка данных\",\n    description:\n      \"Это ваша конфигурация для того, как подключенные сторонние поставщики и AnythingLLM обрабатывают ваши данные.\",\n    anonymous: \"Анонимная телеметрия включена\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Поиск коннекторов данных\",\n    \"no-connectors\": \"Коннекторы данных не найдены.\",\n    github: {\n      name: \"Репозиторий GitHub\",\n      description:\n        \"Импортируйте весь публичный или приватный репозиторий GitHub одним кликом.\",\n      URL: \"URL репозитория GitHub\",\n      URL_explained: \"URL репозитория GitHub, который вы хотите собрать.\",\n      token: \"Токен доступа GitHub\",\n      optional: \"необязательно\",\n      token_explained: \"Токен доступа для предотвращения ограничения запросов.\",\n      token_explained_start: \"Без \",\n      token_explained_link1: \"личного токена доступа\",\n      token_explained_middle:\n        \", API GitHub может ограничить количество файлов для сбора из-за лимитов запросов. Вы можете \",\n      token_explained_link2: \"создать временный токен доступа\",\n      token_explained_end: \", чтобы избежать этой проблемы.\",\n      ignores: \"Игнорирование файлов\",\n      git_ignore:\n        \"Список в формате .gitignore для исключения определённых файлов при сборе. Нажмите Enter после каждой записи, которую хотите сохранить.\",\n      task_explained:\n        \"После завершения все файлы будут доступны для внедрения в рабочие пространства через выбор документов.\",\n      branch: \"Ветка, из которой нужно собрать файлы.\",\n      branch_loading: \"-- загрузка доступных веток --\",\n      branch_explained: \"Ветка, из которой нужно собрать файлы.\",\n      token_information:\n        \"Если не заполнить поле <b>Токен доступа GitHub</b>, этот коннектор данных сможет собрать только <b>файлы верхнего уровня</b> репозитория из-за ограничений публичного API GitHub.\",\n      token_personal:\n        \"Получите бесплатный личный токен доступа с аккаунтом GitHub здесь.\",\n    },\n    gitlab: {\n      name: \"Репозиторий GitLab\",\n      description:\n        \"Импортируйте весь публичный или приватный репозиторий GitLab одним кликом.\",\n      URL: \"URL репозитория GitLab\",\n      URL_explained: \"URL репозитория GitLab, который вы хотите собрать.\",\n      token: \"Токен доступа GitLab\",\n      optional: \"необязательно\",\n      token_description:\n        \"Выберите дополнительные сущности для получения через API GitLab.\",\n      token_explained_start: \"Без \",\n      token_explained_link1: \"личного токена доступа\",\n      token_explained_middle:\n        \", API GitLab может ограничить количество файлов для сбора из-за лимитов запросов. Вы можете \",\n      token_explained_link2: \"создать временный токен доступа\",\n      token_explained_end: \", чтобы избежать этой проблемы.\",\n      fetch_issues: \"Получать задачи как документы\",\n      ignores: \"Игнорирование файлов\",\n      git_ignore:\n        \"Список в формате .gitignore для исключения определённых файлов при сборе. Нажмите Enter после каждой записи, которую хотите сохранить.\",\n      task_explained:\n        \"После завершения все файлы будут доступны для внедрения в рабочие пространства через выбор документов.\",\n      branch: \"Ветка, из которой нужно собрать файлы\",\n      branch_loading: \"-- загрузка доступных веток --\",\n      branch_explained: \"Ветка, из которой нужно собрать файлы.\",\n      token_information:\n        \"Если не заполнить поле <b>Токен доступа GitLab</b>, этот коннектор данных сможет собрать только <b>файлы верхнего уровня</b> репозитория из-за ограничений публичного API GitLab.\",\n      token_personal:\n        \"Получите бесплатный личный токен доступа с аккаунтом GitLab здесь.\",\n    },\n    youtube: {\n      name: \"Транскрипция YouTube\",\n      description:\n        \"Импортируйте транскрипцию целого видео с YouTube по ссылке.\",\n      URL: \"URL видео на YouTube\",\n      URL_explained_start:\n        \"Введите URL любого видео с YouTube для получения транскрипции. Видео должно иметь \",\n      URL_explained_link: \"субтитры\",\n      URL_explained_end: \" (закрытые титры).\",\n      task_explained:\n        \"После завершения транскрипция будет доступна для внедрения в рабочие пространства через выбор документов.\",\n    },\n    \"website-depth\": {\n      name: \"Сбор ссылок с сайта\",\n      description: \"Соберите сайт и его подстраницы до заданной глубины.\",\n      URL: \"URL сайта\",\n      URL_explained: \"URL сайта, который вы хотите собрать.\",\n      depth: \"Глубина обхода\",\n      depth_explained:\n        \"Количество уровней вложенных ссылок, которое следует пройти от исходного URL.\",\n      max_pages: \"Максимальное количество страниц\",\n      max_pages_explained: \"Максимальное число ссылок для сбора.\",\n      task_explained:\n        \"После завершения весь собранный контент будет доступен для внедрения в рабочие пространства через выбор документов.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Импортируйте целую страницу Confluence одним кликом.\",\n      deployment_type: \"Тип развертывания Confluence\",\n      deployment_type_explained:\n        \"Укажите, размещён ли ваш Confluence в облаке Atlassian или на собственном сервере.\",\n      base_url: \"Базовый URL Confluence\",\n      base_url_explained: \"Это базовый URL вашего пространства Confluence.\",\n      space_key: \"Ключ пространства Confluence\",\n      space_key_explained:\n        \"Это ключ вашего пространства Confluence, который будет использоваться. Обычно начинается с ~\",\n      username: \"Имя пользователя Confluence\",\n      username_explained: \"Ваше имя пользователя в Confluence\",\n      auth_type: \"Тип аутентификации Confluence\",\n      auth_type_explained:\n        \"Выберите тип аутентификации для доступа к страницам Confluence.\",\n      auth_type_username: \"Имя пользователя и токен доступа\",\n      auth_type_personal: \"Личный токен доступа\",\n      token: \"Токен доступа Confluence\",\n      token_explained_start:\n        \"Необходимо предоставить токен доступа для аутентификации. Вы можете сгенерировать его \",\n      token_explained_link: \"здесь\",\n      token_desc: \"Токен доступа для аутентификации\",\n      pat_token: \"Личный токен доступа Confluence\",\n      pat_token_explained: \"Ваш личный токен доступа для Confluence.\",\n      task_explained:\n        \"После завершения содержимое страницы будет доступно для внедрения в рабочие пространства через выбор документов.\",\n      bypass_ssl: \"Обход проверки сертификата SSL\",\n      bypass_ssl_explained:\n        \"Включите эту опцию, чтобы обойти проверку сертификата SSL для экземпляров Confluence, размещенных на собственном сервере, с использованием самоподписанного сертификата.\",\n    },\n    manage: {\n      documents: \"Документы\",\n      \"data-connectors\": \"Коннекторы данных\",\n      \"desktop-only\":\n        \"Редактирование этих настроек доступно только на настольном устройстве. Пожалуйста, перейдите на ПК для продолжения.\",\n      dismiss: \"Закрыть\",\n      editing: \"Редактирование\",\n    },\n    directory: {\n      \"my-documents\": \"Мои документы\",\n      \"new-folder\": \"Новая папка\",\n      \"search-document\": \"Поиск документа\",\n      \"no-documents\": \"Нет документов\",\n      \"move-workspace\": \"Переместить в рабочее пространство\",\n      \"delete-confirmation\":\n        \"Вы уверены, что хотите удалить эти файлы и папки?\\nЭто действие удалит файлы из системы и автоматически уберёт их из всех рабочих пространств.\\nЭто действие необратимо.\",\n      \"removing-message\":\n        \"Удаляется {{count}} документов и {{folderCount}} папок. Пожалуйста, подождите.\",\n      \"move-success\": \"Успешно перемещено {{count}} документов.\",\n      no_docs: \"Нет документов\",\n      select_all: \"Выбрать всё\",\n      deselect_all: \"Снять выбор со всех\",\n      remove_selected: \"Удалить выбранные\",\n      costs: \"*Единоразовая стоимость за внедрение\",\n      save_embed: \"Сохранить и внедрить\",\n      \"total-documents_one\": \"{{count}} документ\",\n      \"total-documents_other\": \"{{count}} документы\",\n    },\n    upload: {\n      \"processor-offline\": \"Процессор документов недоступен\",\n      \"processor-offline-desc\":\n        \"Мы не можем загрузить ваши файлы, так как процессор документов недоступен. Пожалуйста, попробуйте позже.\",\n      \"click-upload\": \"Нажмите для загрузки или перетащите файл\",\n      \"file-types\":\n        \"поддерживаются текстовые файлы, CSV, таблицы, аудиофайлы и другое!\",\n      \"or-submit-link\": \"или отправьте ссылку\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"Загрузка...\",\n      \"fetch-website\": \"Получить сайт\",\n      \"privacy-notice\":\n        \"Эти файлы будут загружены в процессор документов на этом экземпляре AnythingLLM. Файлы не отправляются и не передаются третьим лицам.\",\n    },\n    pinning: {\n      what_pinning: \"Что такое закрепление документа?\",\n      pin_explained_block1:\n        \"Когда вы <b>закрепляете</b> документ в AnythingLLM, мы вставляем всё его содержимое в окно запроса, чтобы LLM мог полностью его понять.\",\n      pin_explained_block2:\n        \"Это работает лучше всего с <b>моделями с большим контекстом</b> или небольшими файлами, критичными для базы знаний.\",\n      pin_explained_block3:\n        \"Если по умолчанию ответы AnythingLLM вас не удовлетворяют, закрепление — отличный способ получить более качественные ответы одним кликом.\",\n      accept: \"Хорошо, понял\",\n    },\n    watching: {\n      what_watching: \"Что делает функция наблюдения за документом?\",\n      watch_explained_block1:\n        \"Когда вы <b>наблюдаете</b> за документом в AnythingLLM, мы <i>автоматически</i> синхронизируем его содержимое с оригинальным источником через регулярные интервалы. Это автоматически обновляет содержимое во всех рабочих пространствах, где используется этот файл.\",\n      watch_explained_block2:\n        \"Эта функция поддерживает только онлайн-контент и недоступна для документов, загруженных вручную.\",\n      watch_explained_block3_start:\n        \"Вы можете управлять наблюдением за документами через \",\n      watch_explained_block3_link: \"Файловый менеджер\",\n      watch_explained_block3_end: \" в режиме администратора.\",\n      accept: \"Хорошо, понял\",\n    },\n    obsidian: {\n      vault_location: \"Местоположение хранилища\",\n      vault_description:\n        \"Выберите папку вашего хранилища Obsidian, чтобы импортировать все заметки и их связи.\",\n      selected_files: \"Найдено {{count}} файлов в формате Markdown\",\n      importing: \"Импорт хранилища...\",\n      import_vault: \"Import Vault\",\n      processing_time:\n        \"Это может занять некоторое время, в зависимости от размера вашего хранилища.\",\n      vault_warning:\n        \"Чтобы избежать любых конфликтов, убедитесь, что ваша папка Obsidian не открыта в данный момент.\",\n    },\n  },\n  chat_window: {\n    send_message: \"Отправить сообщение\",\n    attach_file: \"Прикрепить файл к чату\",\n    text_size: \"Изменить размер текста.\",\n    microphone: \"Произнесите ваш запрос.\",\n    send: \"Отправить запрос в рабочее пространство\",\n    attachments_processing: \"Обработка вложений. Пожалуйста, подождите...\",\n    tts_speak_message: \"Сообщение TTS Speak\",\n    copy: \"Копировать\",\n    regenerate: \"Восстановить\",\n    regenerate_response: \"Перефразировать ответ\",\n    good_response: \"Хороший ответ\",\n    more_actions: \"Больше действий\",\n    fork: \"Вилка\",\n    delete: \"Удалить\",\n    cancel: \"Отменить\",\n    edit_prompt:\n      \"Пожалуйста, предоставьте текст, который необходимо отредактировать.\",\n    edit_response: \"Отредактируйте ответ\",\n    preset_reset_description: \"Очистите историю чата и начните новый чат\",\n    add_new_preset: \"Добавить новый шаблон\",\n    command: \"Команда\",\n    your_command: \"Ваш приказ\",\n    placeholder_prompt:\n      \"Это контент, который будет отображаться перед вашим запросом.\",\n    description: \"Описание\",\n    placeholder_description:\n      \"Отвечает стихотворением о больших языковых моделях.\",\n    save: \"Сохранить\",\n    small: \"Маленький\",\n    normal: \"Нормальный\",\n    large: \"Большой\",\n    workspace_llm_manager: {\n      search: \"Поиск поставщиков больших языковых моделей\",\n      loading_workspace_settings: \"Загрузка настроек рабочего пространства...\",\n      available_models: \"Доступные модели для {{provider}}\",\n      available_models_description:\n        \"Выберите модель, которую вы хотите использовать для этой рабочей среды.\",\n      save: \"Используйте эту модель.\",\n      saving:\n        \"Установка модели в качестве значения по умолчанию для рабочего пространства...\",\n      missing_credentials:\n        \"Этот поставщик не предоставляет никаких подтверждающих документов.\",\n      missing_credentials_description:\n        \"Нажмите, чтобы настроить учетные данные\",\n    },\n    submit: \"Отправить\",\n    edit_info_user:\n      '\"Отправить\" генерирует новый ответ от ИИ. \"Сохранить\" обновляет только ваше сообщение.',\n    edit_info_assistant:\n      \"Ваши изменения будут сохранены непосредственно в этом ответе.\",\n    see_less: \"Показать меньше\",\n    see_more: \"Узнать больше\",\n    tools: \"Инструменты\",\n    browse: \"Просматривать\",\n    text_size_label: \"Размер текста\",\n    select_model: \"Выберите модель\",\n    sources: \"Источники\",\n    document: \"Документ\",\n    similarity_match: \"соревнование; игра\",\n    source_count_one: \"{{count}} – ссылка\",\n    source_count_other: \"Ссылки на {{count}}\",\n    preset_exit_description: \"Прекратить текущую сессию работы с агентом\",\n    add_new: \"Добавить новое\",\n    edit: \"Редактировать\",\n    publish: \"Опубликовать\",\n    stop_generating: \"Прекратите генерацию ответа\",\n    pause_tts_speech_message:\n      \"Приостановить чтение текста с помощью синтезатора речи.\",\n    slash_commands: \"Команды, введенные сокращенной формой\",\n    agent_skills: \"Навыки агента\",\n    manage_agent_skills: \"Управление навыками агентов\",\n    agent_skills_disabled_in_session:\n      \"Невозможно изменять навыки во время активной сессии. Для завершения сессии сначала используйте команду /exit.\",\n    start_agent_session: \"Начать сеанс для агента\",\n    use_agent_session_to_use_tools:\n      \"Вы можете использовать инструменты в чате, начав сеанс с агентом, добавив '@agent' в начало вашего сообщения.\",\n  },\n  profile_settings: {\n    edit_account: \"Редактировать учётную запись\",\n    profile_picture: \"Изображение профиля\",\n    remove_profile_picture: \"Удалить изображение профиля\",\n    username: \"Имя пользователя\",\n    new_password: \"Новый пароль\",\n    password_description: \"Пароль должен содержать не менее 8 символов\",\n    cancel: \"Отмена\",\n    update_account: \"Обновить учётную запись\",\n    theme: \"Предпочтения темы\",\n    language: \"Предпочитаемый язык\",\n    failed_upload: \"Не удалось загрузить фотографию профиля: {{ошибка}}\",\n    upload_success: \"Фотография профиля загружена.\",\n    failed_remove: \"Не удалось удалить фотографию профиля: {{error}}\",\n    profile_updated: \"Профиль обновлен.\",\n    failed_update_user: \"Не удалось обновить данные пользователя: {{error}}\",\n    account: \"Счёт\",\n    support: \"Поддержка\",\n    signout: \"Выйти\",\n  },\n  customization: {\n    interface: {\n      title: \"Предпочтения в пользовательском интерфейсе\",\n      description:\n        \"Настройте свои предпочтения пользовательского интерфейса для AnythingLLM.\",\n    },\n    branding: {\n      title: \"Брендинг и создание продуктов с собственной меткой.\",\n      description:\n        \"Настройте свою версию AnythingLLM с использованием собственных брендинговых элементов.\",\n    },\n    chat: {\n      title: \"Чат\",\n      description: \"Настройте свои предпочтения для чата с AnythingLLM.\",\n      auto_submit: {\n        title: \"Автоматическая передача голосового ввода\",\n        description:\n          \"Автоматически отправлять голосовой ввод после периода тишины\",\n      },\n      auto_speak: {\n        title: \"Автоматические ответы\",\n        description:\n          \"Автоматическое воспроизведение ответов, сгенерированных ИИ.\",\n      },\n      spellcheck: {\n        title: \"Включить проверку правописания\",\n        description:\n          \"Включить или отключить проверку правописания в поле ввода сообщений\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Тема\",\n        description: \"Выберите предпочитаемую цветовую схему для приложения.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Показать полосу прокрутки\",\n        description: \"Включить или отключить полосу прокрутки в окне чата.\",\n      },\n      \"support-email\": {\n        title: \"Поддержка по электронной почте\",\n        description:\n          \"Укажите адрес электронной почты службы поддержки, к которому пользователи смогут обращаться за помощью.\",\n      },\n      \"app-name\": {\n        title: \"Имя:\\n\\nИмя:\",\n        description:\n          \"Укажите имя, которое будет отображаться на странице входа для всех пользователей.\",\n      },\n      \"display-language\": {\n        title: \"Язык отображения\",\n        description:\n          \"Выберите предпочитаемый язык для отображения пользовательского интерфейса AnythingLLM – когда доступны переводы.\",\n      },\n      logo: {\n        title: \"Логотип бренда\",\n        description:\n          \"Загрузите свой собственный логотип, чтобы он отображался на всех страницах.\",\n        add: \"Добавьте свой логотип\",\n        recommended: \"Рекомендуемый размер: 800 x 200\",\n        remove: \"Удалить\",\n        replace: \"Замените\",\n      },\n      \"browser-appearance\": {\n        title: \"Внешний вид браузера\",\n        description:\n          \"Настройте внешний вид вкладки и заголовка браузера при открытии приложения.\",\n        tab: {\n          title: \"Заголовок\",\n          description:\n            \"Установите пользовательское название вкладки при открытии приложения в браузере.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description:\n            \"Используйте пользовательский значок для вкладки браузера.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Элементы нижней части боковой панели\",\n        description:\n          \"Настройте элементы, отображаемые в нижней части боковой панели.\",\n        icon: \"Иконка\",\n        link: \"Ссылка\",\n      },\n      \"render-html\": {\n        title: \"Отображение HTML в чате\",\n        description:\n          \"Отображение HTML-ответов в ответах помощника.\\nЭто может привести к значительно более высокой степени соответствия ответа качеству, но также может привести к потенциальным проблемам с безопасностью.\",\n      },\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Создать агента\",\n      editWorkspace: \"Редактировать рабочее пространство\",\n      uploadDocument: \"Загрузить документ\",\n    },\n    greeting: \"Чем я могу вам помочь сегодня?\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Сочетания клавиш\",\n    shortcuts: {\n      settings: \"Открыть настройки\",\n      workspaceSettings: \"Открыть текущие настройки рабочего пространства\",\n      home: \"Вернуться на главную\",\n      workspaces: \"Управление рабочими пространствами\",\n      apiKeys: \"Настройки API-ключей\",\n      llmPreferences: \"Предпочтения LLM\",\n      chatSettings: \"Настройки чата\",\n      help: \"Показать справку о сочетаниях клавиш\",\n      showLLMSelector: \"Выбор рабочей среды для LLM\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Успех!\",\n        success_description:\n          \"Ваш системный запрос был опубликован в Центре сообщества!\",\n        success_thank_you: \"Спасибо за то, что поделились с сообществом!\",\n        view_on_hub: \"Просмотр в Центре сообщества\",\n        modal_title: \"Система оповещения\",\n        name_label: \"Имя:\\n\\nИмя:\",\n        name_description:\n          \"Это имя, которое отображается для вашего системного запроса.\",\n        name_placeholder: \"Моя системная подсказка\",\n        description_label: \"Описание\",\n        description_description:\n          \"Это описание вашего системного запроса. Используйте его для описания цели вашего системного запроса.\",\n        tags_label: \"Теги\",\n        tags_description:\n          \"Теги используются для обозначения вашего запроса в системе, чтобы облегчить поиск. Вы можете добавить несколько тегов. Максимум 5 тегов. Максимальная длина каждого тега – 20 символов.\",\n        tags_placeholder: \"Введите текст и нажмите Enter, чтобы добавить теги.\",\n        visibility_label: \"Видимость\",\n        public_description:\n          \"Эти подсказки доступны для просмотра всем пользователям.\",\n        private_description: \"Личные сообщения отображаются только вам.\",\n        publish_button: \"Опубликовать в Центре сообщества\",\n        submitting: \"Публикация...\",\n        prompt_label: \"Запрос\",\n        prompt_description:\n          \"Это фактический запрос, который будет использоваться для управления языковой моделью.\",\n        prompt_placeholder: \"Введите здесь запрос для вашей системы...\",\n      },\n      agent_flow: {\n        success_title: \"Успех!\",\n        success_description:\n          'Ваш профиль \"Agent Flow\" опубликован в Центре сообщества!',\n        success_thank_you: \"Спасибо за то, что поделились с сообществом!\",\n        view_on_hub: \"Просмотр в Центре сообщества\",\n        modal_title: \"Офис агента\",\n        name_label: \"Имя:\\n\\nИмя:\",\n        name_description:\n          \"Это имя, которое отображается для вашего сценария автоматизации.\",\n        name_placeholder: \"Мой агент – это...\",\n        description_label: \"Описание\",\n        description_description:\n          \"Это описание вашего процесса взаимодействия с агентом. Используйте его для описания цели вашего процесса взаимодействия с агентом.\",\n        tags_label: \"Теги\",\n        tags_description:\n          \"Теги используются для обозначения вашего процесса работы с агентами, чтобы упростить поиск. Вы можете добавить несколько тегов. Максимум 5 тегов. Максимальная длина каждого тега – 20 символов.\",\n        tags_placeholder: \"Введите текст и нажмите Enter, чтобы добавить теги.\",\n        visibility_label: \"Видимость\",\n        submitting: \"Публикация...\",\n        submit: \"Опубликовать в Центре сообщества\",\n        privacy_note:\n          \"Потоки данных всегда загружаются в режиме конфиденциальности, чтобы защитить любую конфиденциальную информацию. Вы можете изменить видимость потока в Центре сообщества после публикации. Пожалуйста, убедитесь, что ваш поток не содержит никакой конфиденциальной или личной информации перед публикацией.\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Требуется аутентификация\",\n          description:\n            \"Для публикации материалов необходимо сначала пройти аутентификацию в сообществе AnythingLLM.\",\n          button: \"Подключитесь к центру сообщества\",\n        },\n      },\n      slash_command: {\n        success_title: \"Успех!\",\n        success_description:\n          \"Ваш Slash-команда был опубликован в Центре сообщества!\",\n        success_thank_you: \"Спасибо за то, что поделились с сообществом!\",\n        view_on_hub: \"Просмотр в Центре сообщества\",\n        modal_title: \"Опубликуйте команду Slash\",\n        name_label: \"Имя:\\n\\nИмя:\",\n        name_description:\n          \"Это имя, которое будет отображаться для вашего команды.\",\n        name_placeholder: \"Мой Slash-команда\",\n        description_label: \"Описание\",\n        description_description:\n          \"Это описание вашего командного оператора. Используйте его для описания цели вашего командного оператора.\",\n        tags_label: \"Теги\",\n        tags_description:\n          \"Теги используются для обозначения вашего командного оператора, чтобы облегчить поиск. Вы можете добавить несколько тегов. Максимум 5 тегов. Максимальная длина каждого тега – 20 символов.\",\n        tags_placeholder: \"Введите текст и нажмите Enter, чтобы добавить теги.\",\n        visibility_label: \"Видимость\",\n        public_description: \"Общие команды, доступные для всех пользователей.\",\n        private_description: \"Приватные команды, доступные только вам.\",\n        publish_button: \"Опубликовать в Центре сообщества\",\n        submitting: \"Публикация...\",\n        prompt_label: \"Запрос\",\n        prompt_description:\n          'Это запрос, который будет использован при активации команды, содержащей символ \"/\".',\n        prompt_placeholder: \"Введите свой запрос здесь...\",\n      },\n    },\n  },\n  security: {\n    title: \"Безопасность\",\n    multiuser: {\n      title: \"Многопользовательский режим\",\n      description:\n        \"Настройте ваш экземпляр для поддержки вашей команды, активировав многопользовательский режим.\",\n      enable: {\n        \"is-enable\": \"Многопользовательский режим включен\",\n        enable: \"Включить многопользовательский режим\",\n        description:\n          \"По умолчанию, вы будете единственным администратором. Как администратор, вы должны будете создавать учетные записи для всех новых пользователей или администраторов. Не теряйте ваш пароль, так как только администратор может сбросить пароли.\",\n        username: \"Имя пользователя учетной записи администратора\",\n        password: \"Пароль учетной записи администратора\",\n      },\n    },\n    password: {\n      title: \"Защита паролем\",\n      description:\n        \"Защитите ваш экземпляр AnythingLLM паролем. Если вы забудете его, метода восстановления не существует, поэтому убедитесь, что вы сохранили этот пароль.\",\n      \"password-label\": \"Пароль экземпляра\",\n    },\n  },\n  home: {\n    welcome: \"Добро пожаловать\",\n    chooseWorkspace: \"Выберите рабочую область, чтобы начать чат!\",\n    notAssigned:\n      \"Вы не назначены ни к одной рабочей области.\\nСвяжитесь с администратором, чтобы запросить доступ к рабочей области.\",\n    goToWorkspace: 'Перейти к рабочей области \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/tr/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    survey: {\n      email: \"E-posta adresiniz nedir?\",\n      useCase: \"AnythingLLM'yi ne için kullanacaksınız?\",\n      useCaseWork: \"İş için\",\n      useCasePersonal: \"Kişisel kullanım için\",\n      useCaseOther: \"Diğer\",\n      comment: \"AnythingLLM'yi nasıl duydunuz?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube vb. - Bizi nasıl buldunuz?\",\n      skip: \"Anketi Atla\",\n      thankYou: \"Geri bildiriminiz için teşekkür ederiz!\",\n      title: \"AnythingLLM'ye Hoş Geldiniz\",\n      description:\n        \"AnythingLLM'yi ihtiyaçlarınıza göre oluşturmamıza yardımcı olun. İsteğe bağlı.\",\n    },\n    home: {\n      title: \"Hoş Geldiniz\",\n      getStarted: \"Başla\",\n    },\n    llm: {\n      title: \"LLM Tercihi\",\n      description:\n        \"AnythingLLM birçok LLM sağlayıcısıyla çalışabilir. Bu, sohbeti yöneten hizmet olacaktır.\",\n    },\n    userSetup: {\n      title: \"Kullanıcı Kurulumu\",\n      description: \"Kullanıcı ayarlarınızı yapılandırın.\",\n      howManyUsers: \"Bu örneği kaç kişi kullanacak?\",\n      justMe: \"Sadece ben\",\n      myTeam: \"Ekibim\",\n      instancePassword: \"Örnek Şifresi\",\n      setPassword: \"Bir şifre belirlemek ister misiniz?\",\n      passwordReq: \"Şifreler en az 8 karakter olmalıdır.\",\n      passwordWarn:\n        \"Kurtarma yöntemi olmadığı için bu şifreyi kaydetmeniz önemlidir.\",\n      adminUsername: \"Yönetici hesap kullanıcı adı\",\n      adminPassword: \"Yönetici hesap şifresi\",\n      adminPasswordReq: \"Şifreler en az 8 karakter olmalıdır.\",\n      teamHint:\n        \"Varsayılan olarak tek yönetici siz olacaksınız. Kurulum tamamlandığında, diğer kişileri kullanıcı veya yönetici olarak davet edebilirsiniz. Yalnızca yöneticiler şifreleri sıfırlayabildiğinden şifrenizi kaybetmeyin.\",\n    },\n    data: {\n      title: \"Veri İşleme & Gizlilik\",\n      description:\n        \"Kişisel verileriniz konusunda şeffaflık ve kontrol sağlamaya kararlıyız.\",\n      settingsHint:\n        \"Bu ayarlar istediğiniz zaman ayarlardan yeniden yapılandırılabilir.\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Çalışma Alanları Adı\",\n    user: \"Kullanıcı\",\n    selection: \"Model Seçimi\",\n    saving: \"Kaydediliyor...\",\n    save: \"Değişiklikleri Kaydet\",\n    previous: \"Önceki Sayfa\",\n    next: \"Sonraki Sayfa\",\n    optional: \"İsteğe bağlı\",\n    yes: \"Evet\",\n    no: \"Hayır\",\n    search: \"Ara\",\n    username_requirements:\n      \"Kullanıcı adı 2-32 karakter uzunluğunda olmalı, küçük harfle başlamalı ve yalnızca küçük harfler, rakamlar, alt çizgiler, tireler ve noktalar içermelidir.\",\n    on: \"On\",\n    none: \"Yok\",\n    stopped: \"Durdu\",\n    loading: \"Yükleniyor\",\n    refresh: \"Tazelemek\",\n  },\n  settings: {\n    title: \"Instance Ayarları\",\n    invites: \"Davetler\",\n    users: \"Kullanıcılar\",\n    workspaces: \"Çalışma Alanları\",\n    \"workspace-chats\": \"Çalışma Alanı Sohbetleri\",\n    customization: \"Özelleştirme\",\n    \"api-keys\": \"Geliştirici API\",\n    llm: \"LLM\",\n    transcription: \"Transkripsiyon\",\n    embedder: \"Gömme Aracı\",\n    \"text-splitting\": \"Metin Bölme & Parçalama\",\n    \"voice-speech\": \"Ses & Konuşma\",\n    \"vector-database\": \"Vektör Veritabanı\",\n    embeds: \"Sohbet Gömme\",\n    security: \"Güvenlik\",\n    \"event-logs\": \"Olay Kayıtları\",\n    privacy: \"Gizlilik & Veri\",\n    \"ai-providers\": \"Yapay Zeka Sağlayıcıları\",\n    \"agent-skills\": \"Ajan Becerileri\",\n    admin: \"Yönetici\",\n    tools: \"Araçlar\",\n    \"experimental-features\": \"Deneysel Özellikler\",\n    contact: \"Destekle İletişime Geçin\",\n    \"browser-extension\": \"Tarayıcı Uzantısı\",\n    \"system-prompt-variables\": \"Sistem Prompt Değişkenleri\",\n    interface: \"Arayüz Tercihleri\",\n    branding: \"Marka & Beyaz Etiketleme\",\n    chat: \"Sohbet\",\n    \"mobile-app\": \"AnythingLLM Mobil\",\n    \"community-hub\": {\n      title: \"Topluluk Merkezi\",\n      trending: \"Popüler olanları keşfedin\",\n      \"your-account\": \"Hesabınız\",\n      \"import-item\": \"İthal Edilen Ürün\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Hoş geldiniz\",\n      \"placeholder-username\": \"Kullanıcı Adı\",\n      \"placeholder-password\": \"Şifre\",\n      login: \"Giriş Yap\",\n      validating: \"Doğrulanıyor...\",\n      \"forgot-pass\": \"Şifremi Unuttum\",\n      reset: \"Sıfırla\",\n    },\n    \"sign-in\": \"{{appName}} hesabınıza giriş yapın.\",\n    \"password-reset\": {\n      title: \"Şifre Sıfırlama\",\n      description: \"Şifrenizi sıfırlamak için gerekli bilgileri aşağıya girin.\",\n      \"recovery-codes\": \"Kurtarma Kodları\",\n      \"back-to-login\": \"Girişe Geri Dön\",\n    },\n  },\n  \"new-workspace\": {\n    title: \"Yeni Çalışma Alanı\",\n    placeholder: \"Benim Çalışma Alanım\",\n  },\n  \"workspaces—settings\": {\n    general: \"Genel Ayarlar\",\n    chat: \"Sohbet Ayarları\",\n    vector: \"Vektör Veritabanı\",\n    members: \"Üyeler\",\n    agent: \"Ajan Yapılandırması\",\n  },\n  general: {\n    vector: {\n      title: \"Vektör Sayısı\",\n      description: \"Vektör veritabanınızdaki toplam vektör sayısı.\",\n    },\n    names: {\n      description:\n        \"Bu, yalnızca çalışma alanınızın görüntü adını değiştirecektir.\",\n    },\n    message: {\n      title: \"Önerilen Sohbet Mesajları\",\n      description:\n        \"Çalışma alanı kullanıcılarınıza önerilecek sohbet mesajlarını özelleştirin.\",\n      add: \"Yeni mesaj ekle\",\n      save: \"Mesajları Kaydet\",\n      heading: \"Bana açıkla\",\n      body: \"AnythingLLM'nin faydalarını\",\n    },\n    delete: {\n      title: \"Çalışma Alanını Sil\",\n      description:\n        \"Bu çalışma alanını ve tüm verilerini silin. Bu işlem, çalışma alanını tüm kullanıcılar için silecektir.\",\n      delete: \"Çalışma Alanını Sil\",\n      deleting: \"Çalışma Alanı Siliniyor...\",\n      \"confirm-start\": \"Tüm çalışma alanınızı silmek üzeresiniz\",\n      \"confirm-end\":\n        \". Bu, vektör veritabanınızdaki tüm vektör gömme verilerini kaldıracaktır.\\n\\nOrijinal kaynak dosyalar etkilenmeyecektir. Bu işlem geri alınamaz.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Çalışma Alanı LLM Sağlayıcısı\",\n      description:\n        \"Bu çalışma alanı için kullanılacak belirli LLM sağlayıcısı ve modeli. Varsayılan olarak sistem LLM sağlayıcısı ve ayarları kullanılır.\",\n      search: \"Tüm LLM sağlayıcılarını ara\",\n    },\n    model: {\n      title: \"Çalışma Alanı Sohbet Modeli\",\n      description:\n        \"Bu çalışma alanı için kullanılacak belirli sohbet modeli. Boş bırakılırsa, sistem LLM tercihi kullanılacaktır.\",\n    },\n    mode: {\n      title: \"Sohbet Modu\",\n      chat: {\n        title: \"Sohbet\",\n        description:\n          \"LLM'nin genel bilgisi ve bulunan doküman bağlamıyla cevaplar sunacaktır. Araçları kullanmak için @agent komutunu kullanmanız gerekecektir.\",\n      },\n      query: {\n        title: \"Sorgu\",\n        description:\n          \"yalnızca doküman bağlamı bulunursa yanıtlar sağlayacaktır.<b>İhtiyaç duyacağınız araçları kullanmak için @agent komutunu kullanmanız gerekecektir.</b>\",\n      },\n      automatic: {\n        title: \"Oto\",\n        description:\n          \"<br />Varsa, model ve sağlayıcı tarafından desteklenen yerel araçları otomatik olarak kullanacaktır. Yerel araç kullanımı desteklenmiyorsa, araçları kullanmak için @agent komutunu kullanmanız gerekecektir.\",\n      },\n    },\n    history: {\n      title: \"Sohbet Geçmişi\",\n      \"desc-start\":\n        \"Yanıta dahil edilecek önceki sohbetlerin sayısı (kısa süreli hafıza).\",\n      recommend: \"20 önerilir. \",\n      \"desc-end\":\n        \"45'ten fazlası, mesaj boyutuna göre sürekli sohbet hatalarına yol açabilir.\",\n    },\n    prompt: {\n      title: \"Komut (Prompt)\",\n      description:\n        \"Bu çalışma alanında kullanılacak komut. Yapay zekanın yanıt üretmesi için bağlam ve talimatları tanımlayın. Uygun ve doğru yanıtlar almak için özenle hazırlanmış bir komut sağlamalısınız.\",\n      history: {\n        title: \"Sistem Prompt Geçmişi\",\n        clearAll: \"Tümünü Temizle\",\n        noHistory: \"Sistem prompt geçmişi mevcut değil\",\n        restore: \"Geri Yükle\",\n        delete: \"Sil\",\n        deleteConfirm: \"Bu geçmiş öğesini silmek istediğinizden emin misiniz?\",\n        clearAllConfirm:\n          \"Tüm geçmişi temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz.\",\n        expand: \"Genişlet\",\n        publish: \"Topluluk Hub'ına Yayınla\",\n      },\n    },\n    refusal: {\n      title: \"Sorgu Modu Ret Yanıtı\",\n      \"desc-start\": \"Eğer\",\n      query: \"sorgu\",\n      \"desc-end\":\n        \"modunda bağlam bulunamazsa, özel bir ret yanıtı döndürmek isteyebilirsiniz.\",\n      \"tooltip-title\": \"Bunu neden görüyorum?\",\n      \"tooltip-description\":\n        \"Sorgu modundasınız; bu mod yalnızca belgelerinizdeki bilgileri kullanır. Daha esnek konuşmalar için sohbet moduna geçin veya sohbet modları hakkında daha fazla bilgi edinmek için belgelerimizi ziyaret etmek üzere buraya tıklayın.\",\n    },\n    temperature: {\n      title: \"LLM Sıcaklığı\",\n      \"desc-start\":\n        'Bu ayar, LLM yanıtlarının ne kadar \"yaratıcı\" olacağını kontrol eder.',\n      \"desc-end\":\n        \"Sayı yükseldikçe yaratıcı yanıtlar artar. Bazı modeller için bu değer çok yüksek ayarlandığında anlamsız yanıtlar ortaya çıkabilir.\",\n      hint: \"Çoğu LLM'in farklı kabul edilebilir değer aralıkları vardır. Ayrıntılar için LLM sağlayıcınıza danışın.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Vektör veritabanı tanımlayıcısı\",\n    snippets: {\n      title: \"Maksimum Bağlam Parçacıkları\",\n      description:\n        \"Bu ayar, sohbet veya sorgu başına LLM'e gönderilecek maksimum bağlam parçacığı sayısını kontrol eder.\",\n      recommend: \"Önerilen: 4\",\n    },\n    doc: {\n      title: \"Belge benzerlik eşiği\",\n      description:\n        \"Bir kaynağın sohbetle ilişkili sayılabilmesi için gereken minimum benzerlik puanı. Sayı yükseldikçe, kaynağın sohbete benzerliği de o kadar yüksek olmalıdır.\",\n      zero: \"Kısıtlama yok\",\n      low: \"Düşük (benzerlik puanı ≥ .25)\",\n      medium: \"Orta (benzerlik puanı ≥ .50)\",\n      high: \"Yüksek (benzerlik puanı ≥ .75)\",\n    },\n    reset: {\n      reset: \"Vektör veritabanını sıfırla\",\n      resetting: \"Vektörler temizleniyor...\",\n      confirm:\n        \"Bu çalışma alanının vektör veritabanını sıfırlamak üzeresiniz. Bu işlem, hâlihazırda gömülü olan tüm vektör verilerini kaldıracaktır.\\n\\nOrijinal kaynak dosyalar etkilenmeyecektir. Bu işlem geri alınamaz.\",\n      error: \"Çalışma alanının vektör veritabanı sıfırlanamadı!\",\n      success: \"Çalışma alanının vektör veritabanı sıfırlandı!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"Araç çağırmayı açıkça desteklemeyen LLM'lerin performansı, modelin yetenekleri ve doğruluğuna büyük ölçüde bağlıdır. Bazı beceriler kısıtlı veya işlevsiz olabilir.\",\n    provider: {\n      title: \"Çalışma Alanı Ajan LLM Sağlayıcısı\",\n      description:\n        \"Bu çalışma alanındaki @agent ajanı için kullanılacak spesifik LLM sağlayıcısı ve modeli.\",\n    },\n    mode: {\n      chat: {\n        title: \"Çalışma Alanı Ajan Sohbet Modeli\",\n        description:\n          \"Bu çalışma alanındaki @agent ajanı için kullanılacak spesifik sohbet modeli.\",\n      },\n      title: \"Çalışma Alanı Ajan Modeli\",\n      description:\n        \"Bu çalışma alanındaki @agent ajanı için kullanılacak spesifik LLM modeli.\",\n      wait: \"-- modeller bekleniyor --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG ve uzun vadeli hafıza\",\n        description:\n          'Ajana, yerel belgelerinizi kullanarak soruları yanıtlatma veya bazı içerikleri \"hatırlaması\" için uzun vadeli hafıza kullanma izni verin.',\n      },\n      view: {\n        title: \"Belgeleri görüntüleme & özetleme\",\n        description:\n          \"Ajana, çalışma alanında hâlihazırda gömülü olan dosyaları listeleyip özetleme izni verin.\",\n      },\n      scrape: {\n        title: \"Web sitelerini tarama\",\n        description:\n          \"Ajana, web sitelerini ziyaret edip içeriklerini tarama izni verin.\",\n      },\n      generate: {\n        title: \"Grafik oluşturma\",\n        description:\n          \"Varsayılan ajanın, sağlanan veya sohbette yer alan verilere göre çeşitli grafik türleri oluşturmasına izin verin.\",\n      },\n      save: {\n        title: \"Tarayıcıya dosya oluştur & kaydet\",\n        description:\n          \"Varsayılan ajanın, oluşturduğu dosyaları kaydetmesine ve tarayıcıda indirilebilir hale getirmesine izin verin.\",\n      },\n      web: {\n        title: \"Canlı web araması ve gezinme\",\n        description:\n          \"Ajantınızın, web arama (SERP) sağlayıcısıyla bağlantı kurarak, sorularınızı yanıtlamak için web'i aramasını sağlayın.\",\n      },\n      sql: {\n        title: \"SQL Bağlayıcı\",\n        description:\n          \"Temsilcinizin, çeşitli SQL veri tabanı sağlayıcılarına bağlanarak SQL'i kullanarak sorularınızı yanıtlamasına olanak tanıyın.\",\n      },\n      default_skill:\n        \"Varsayılan olarak bu özellik etkinleştirilmiştir, ancak ajanın kullanmasına izin vermek istemiyorsanız, bu özelliği devre dışı bırakabilirsiniz.\",\n    },\n    mcp: {\n      title: \"MCP Sunucuları\",\n      \"loading-from-config\": \"MCP sunarlarını yapılandırma dosyasından yükleme\",\n      \"learn-more\": \"MCP sunucuları hakkında daha fazla bilgi edinin.\",\n      \"no-servers-found\": \"Hiçbir MCP sunucusu bulunamadı.\",\n      \"tool-warning\":\n        \"En iyi performansı elde etmek için, gereksiz araçları devre dışı bırakarak bağlamı korumayı düşünebilirsiniz.\",\n      \"stop-server\": \"MCP sunucusunu durdurun\",\n      \"start-server\": \"MCP sunucusunu başlatın\",\n      \"delete-server\": \"MCP sunucusunu sil\",\n      \"tool-count-warning\":\n        \"Bu MCP sunucusu, <b> özelliklerini etkinleştirmiş durumda ve bu özellikler her etkileşimde bağlamı tüketebilir. </b> Bağlamı korumak için istenmeyen özellikleri devre dışı bırakmayı düşünebilirsiniz.\",\n      \"startup-command\": \"Başlangıç Komutu\",\n      command: \"Emir\",\n      arguments: \"Tartışmalar\",\n      \"not-running-warning\":\n        \"Bu MCP sunucusu çalışmıyor – olabilir ki durdurulmuş veya başlatma sırasında bir hata yaşıyor olabilir.\",\n      \"tool-call-arguments\": \"Araç çağrı argümanları\",\n      \"tools-enabled\": \"gerektiren araçlar etkinleştirildi\",\n    },\n    settings: {\n      title: \"Ajant Yetenek Ayarları\",\n      \"max-tool-calls\": {\n        title: \"Her yanıt için maksimum araç çağrı sayısı\",\n        description:\n          \"Bir ajantın, tek bir yanıt oluşturmak için zincirlemesini kullanabileceği maksimum araç sayısı. Bu, araçların kontrolsüz bir şekilde çağrılmasını ve sonsuz döngülerin oluşmasını engeller.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Akıllı Becerilerin Seçimi\",\n        \"beta-badge\": \"Beta\",\n        description:\n          'Her sorgu için sınırsız araç kullanımı ve \"cut token\" kullanımını %80\\'e kadar azaltma imkanı sunar — AnythingLLM, her talep için doğru becerileri otomatik olarak seçer.',\n        \"max-tools\": {\n          title: \"Max Araçları\",\n          description:\n            \"Her sorgu için seçilebilecek maksimum araç sayısı. Daha büyük bağlam modelleri için bu değeri daha yüksek bir değere ayarlamayı öneririz.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Çalışma Alanı Sohbetleri\",\n    description:\n      \"Bunlar, kullanıcılar tarafından gönderilen ve oluşturulma tarihlerine göre sıralanan tüm kayıtlı sohbetler ve mesajlardır.\",\n    export: \"Dışa Aktar\",\n    table: {\n      id: \"Id\",\n      by: \"Gönderen\",\n      workspace: \"Çalışma Alanı\",\n      prompt: \"Komut (Prompt)\",\n      response: \"Yanıt\",\n      at: \"Gönderilme Zamanı\",\n    },\n  },\n  api: {\n    title: \"API Anahtarları\",\n    description:\n      \"API anahtarları, bu AnythingLLM örneğine programatik olarak erişmeye ve yönetmeye olanak tanır.\",\n    link: \"API dokümantasyonunu okuyun\",\n    generate: \"Yeni API Anahtarı Oluştur\",\n    table: {\n      key: \"API Anahtarı\",\n      by: \"Oluşturan\",\n      created: \"Oluşturulma Tarihi\",\n    },\n  },\n  llm: {\n    title: \"LLM Tercihi\",\n    description:\n      \"Bu, tercih ettiğiniz LLM sohbet ve gömme sağlayıcısının kimlik bilgileri ile ayarlarıdır. Bu anahtarların güncel ve doğru olması önemlidir; aksi takdirde AnythingLLM doğru çalışmayacaktır.\",\n    provider: \"LLM Sağlayıcısı\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Azure Hizmet Uç Noktası\",\n        api_key: \"API Anahtarı\",\n        chat_deployment_name: \"Sohbet Dağıtım Adı\",\n        chat_model_token_limit: \"Sohbet Modeli Token Limiti\",\n        model_type: \"Model Türü\",\n        default: \"Varsayılan\",\n        reasoning: \"Mantıksal\",\n        model_type_tooltip:\n          'Dağıtımınız bir mantıksal model (o1, o1-mini, o3-mini vb.) kullanıyorsa, bunu \"Mantıksal\" olarak ayarlayın. Aksi takdirde sohbet istekleriniz başarısız olabilir.',\n      },\n    },\n  },\n  transcription: {\n    title: \"Transkripsiyon Model Tercihi\",\n    description:\n      \"Bu, tercih ettiğiniz transkripsiyon modeli sağlayıcısının kimlik bilgileri ve ayarlarıdır. Anahtarların güncel ve doğru olması önemlidir; aksi takdirde medya dosyaları ve sesler transkribe edilemez.\",\n    provider: \"Transkripsiyon Sağlayıcısı\",\n    \"warn-start\":\n      \"Sınırlı RAM veya CPU'ya sahip makinelerde yerel Whisper modelini kullanmak, medya dosyalarını işlerken AnythingLLM'nin duraksamasına neden olabilir.\",\n    \"warn-recommend\":\n      \"En az 2GB RAM öneriyoruz ve 10MB üzerinde dosya yüklememeye dikkat edin.\",\n    \"warn-end\":\n      \"Yerleşik model, ilk kullanımda otomatik olarak indirilecektir.\",\n  },\n  embedding: {\n    title: \"Gömme (Embedding) Tercihi\",\n    \"desc-start\":\n      \"Yerel olarak gömme mekanizmasını desteklemeyen bir LLM kullanıyorsanız, metinleri gömmek için ek kimlik bilgileri girmeniz gerekebilir.\",\n    \"desc-end\":\n      \"Gömme, metni vektörlere dönüştürme sürecidir. Dosyalarınızın ve komutlarınızın işlenebilmesi için AnythingLLM, bu kimlik bilgilerine ihtiyaç duyar.\",\n    provider: {\n      title: \"Embedding Sağlayıcısı\",\n    },\n  },\n  text: {\n    title: \"Metin Bölme & Parçalama Tercihleri\",\n    \"desc-start\":\n      \"Bazı durumlarda, yeni belgelerin vektör veritabanınıza eklenmeden önce hangi varsayılan yöntemle bölünüp parçalanacağını değiştirmek isteyebilirsiniz.\",\n    \"desc-end\":\n      \"Metin bölmenin nasıl çalıştığını ve olası yan etkilerini tam olarak bilmiyorsanız bu ayarı değiştirmemelisiniz.\",\n    size: {\n      title: \"Metin Parça Boyutu\",\n      description:\n        \"Tek bir vektörde bulunabilecek maksimum karakter uzunluğunu ifade eder.\",\n      recommend: \"Gömme modelinin maksimum karakter uzunluğu\",\n    },\n    overlap: {\n      title: \"Metin Parçalama Örtüşmesi\",\n      description:\n        \"İki bitişik metin parçası arasındaki, parçalama sırasında oluşabilecek maksimum karakter örtüşme miktarını belirtir.\",\n    },\n  },\n  vector: {\n    title: \"Vektör Veritabanı\",\n    description:\n      \"AnythingLLM örneğinizin nasıl çalışacağını belirleyen kimlik bilgileri ve ayarları burada bulunur. Bu anahtarların güncel ve doğru olması önemlidir.\",\n    provider: {\n      title: \"Vektör Veritabanı Sağlayıcısı\",\n      description: \"LanceDB için ek bir yapılandırma gerekmez.\",\n    },\n  },\n  embeddable: {\n    title: \"Gömülebilir Sohbet Widget'ları\",\n    description:\n      \"Gömülebilir sohbet widget'ları, herkese açık olan ve tek bir çalışma alanına bağlı sohbet arayüzleridir. Bu sayede oluşturduğunuz çalışma alanlarını dünyaya açık hâle getirebilirsiniz.\",\n    create: \"Gömme oluştur\",\n    table: {\n      workspace: \"Çalışma Alanı\",\n      chats: \"Gönderilen Sohbetler\",\n      active: \"Aktif Alan Adları\",\n      created: \"Oluşturulma Tarihi\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Gömme Sohbetler\",\n    export: \"Dışa Aktar\",\n    description:\n      \"Yayımladığınız herhangi bir gömme sohbetten gelen tüm kayıtlı sohbetler ve mesajlar burada bulunur.\",\n    table: {\n      embed: \"Gömme\",\n      sender: \"Gönderen\",\n      message: \"Mesaj\",\n      response: \"Yanıt\",\n      at: \"Gönderilme Zamanı\",\n    },\n  },\n  event: {\n    title: \"Olay Kayıtları\",\n    description:\n      \"Bu örnek üzerinde gerçekleşen tüm eylem ve olayları izlemek için görüntüleyin.\",\n    clear: \"Olay Kayıtlarını Temizle\",\n    table: {\n      type: \"Olay Türü\",\n      user: \"Kullanıcı\",\n      occurred: \"Gerçekleşme Zamanı\",\n    },\n  },\n  privacy: {\n    title: \"Gizlilik & Veri İşleme\",\n    description:\n      \"Bağlantılı üçüncü taraf sağlayıcılarla ve AnythingLLM ile verilerinizin nasıl ele alındığını burada yapılandırabilirsiniz.\",\n    anonymous: \"Anonim Telemetri Etkin\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Veri bağlayıcılarını ara\",\n    \"no-connectors\": \"Veri bağlayıcısı bulunamadı.\",\n    github: {\n      name: \"GitHub Deposu\",\n      description:\n        \"Tek tıklamayla tüm herkese açık veya özel GitHub deposunu içe aktarın.\",\n      URL: \"GitHub Depo URL'si\",\n      URL_explained: \"Toplamak istediğiniz GitHub deposunun URL'si.\",\n      token: \"GitHub Erişim Tokeni\",\n      optional: \"isteğe bağlı\",\n      token_explained: \"Hız sınırlamasını önlemek için erişim tokeni.\",\n      token_explained_start: \"Bir \",\n      token_explained_link1: \"Kişisel Erişim Tokeni\",\n      token_explained_middle:\n        \" olmadan GitHub API'si, hız sınırları nedeniyle toplanabilecek dosya sayısını sınırlayabilir. \",\n      token_explained_link2: \"Geçici bir Erişim Tokeni oluşturabilirsiniz\",\n      token_explained_end: \" bu sorunu önlemek için.\",\n      ignores: \"Dosya Yoksaymaları\",\n      git_ignore:\n        \"Toplama sırasında belirli dosyaları yoksaymak için .gitignore formatında liste. Kaydetmek istediğiniz her girişten sonra enter tuşuna basın.\",\n      task_explained:\n        \"Tamamlandığında, tüm dosyalar belge seçicide çalışma alanlarına gömülmeye hazır olacaktır.\",\n      branch: \"Dosyaları toplamak istediğiniz dal.\",\n      branch_loading: \"-- mevcut dallar yükleniyor --\",\n      branch_explained: \"Dosyaları toplamak istediğiniz dal.\",\n      token_information:\n        \"<b>GitHub Erişim Tokeni</b> doldurulmadan bu veri bağlayıcısı, GitHub'ın herkese açık API hız sınırları nedeniyle yalnızca deponun <b>üst düzey</b> dosyalarını toplayabilecektir.\",\n      token_personal:\n        \"Buradan ücretsiz bir Kişisel Erişim Tokeni alabilirsiniz.\",\n    },\n    gitlab: {\n      name: \"GitLab Deposu\",\n      description:\n        \"Tek tıklamayla tüm herkese açık veya özel GitLab deposunu içe aktarın.\",\n      URL: \"GitLab Depo URL'si\",\n      URL_explained: \"Toplamak istediğiniz GitLab deposunun URL'si.\",\n      token: \"GitLab Erişim Tokeni\",\n      optional: \"isteğe bağlı\",\n      token_description: \"GitLab API'sinden alınacak ek varlıkları seçin.\",\n      token_explained_start: \"Bir \",\n      token_explained_link1: \"Kişisel Erişim Tokeni\",\n      token_explained_middle:\n        \" olmadan GitLab API'si, hız sınırları nedeniyle toplanabilecek dosya sayısını sınırlayabilir. \",\n      token_explained_link2: \"Geçici bir Erişim Tokeni oluşturabilirsiniz\",\n      token_explained_end: \" bu sorunu önlemek için.\",\n      fetch_issues: \"Sorunları Belge Olarak Al\",\n      ignores: \"Dosya Yoksaymaları\",\n      git_ignore:\n        \"Toplama sırasında belirli dosyaları yoksaymak için .gitignore formatında liste. Kaydetmek istediğiniz her girişten sonra enter tuşuna basın.\",\n      task_explained:\n        \"Tamamlandığında, tüm dosyalar belge seçicide çalışma alanlarına gömülmeye hazır olacaktır.\",\n      branch: \"Dosyaları toplamak istediğiniz dal\",\n      branch_loading: \"-- mevcut dallar yükleniyor --\",\n      branch_explained: \"Dosyaları toplamak istediğiniz dal.\",\n      token_information:\n        \"<b>GitLab Erişim Tokeni</b> doldurulmadan bu veri bağlayıcısı, GitLab'ın herkese açık API hız sınırları nedeniyle yalnızca deponun <b>üst düzey</b> dosyalarını toplayabilecektir.\",\n      token_personal:\n        \"Buradan ücretsiz bir Kişisel Erişim Tokeni alabilirsiniz.\",\n    },\n    youtube: {\n      name: \"YouTube Transkripti\",\n      description:\n        \"Bir bağlantıdan tüm YouTube videosunun transkriptini içe aktarın.\",\n      URL: \"YouTube Video URL'si\",\n      URL_explained_start:\n        \"Transkriptini almak için herhangi bir YouTube videosunun URL'sini girin. Videonun \",\n      URL_explained_link: \"altyazıları\",\n      URL_explained_end: \" mevcut olmalıdır.\",\n      task_explained:\n        \"Tamamlandığında, transkript belge seçicide çalışma alanlarına gömülmeye hazır olacaktır.\",\n    },\n    \"website-depth\": {\n      name: \"Toplu Bağlantı Kazıyıcı\",\n      description:\n        \"Bir web sitesini ve alt bağlantılarını belirli bir derinliğe kadar kazıyın.\",\n      URL: \"Web Sitesi URL'si\",\n      URL_explained: \"Kazımak istediğiniz web sitesinin URL'si.\",\n      depth: \"Tarama Derinliği\",\n      depth_explained:\n        \"Bu, çalışanın kaynak URL'den takip edeceği alt bağlantı sayısıdır.\",\n      max_pages: \"Maksimum Sayfa\",\n      max_pages_explained: \"Kazınacak maksimum bağlantı sayısı.\",\n      task_explained:\n        \"Tamamlandığında, tüm kazınan içerik belge seçicide çalışma alanlarına gömülmeye hazır olacaktır.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Tek tıklamayla tüm Confluence sayfasını içe aktarın.\",\n      deployment_type: \"Confluence dağıtım türü\",\n      deployment_type_explained:\n        \"Confluence örneğinizin Atlassian bulutunda mı yoksa kendi sunucunuzda mı barındırıldığını belirleyin.\",\n      base_url: \"Confluence temel URL'si\",\n      base_url_explained: \"Bu, Confluence alanınızın temel URL'sidir.\",\n      space_key: \"Confluence alan anahtarı\",\n      space_key_explained:\n        \"Bu, kullanılacak confluence örneğinizin alan anahtarıdır. Genellikle ~ ile başlar\",\n      username: \"Confluence Kullanıcı Adı\",\n      username_explained: \"Confluence kullanıcı adınız\",\n      auth_type: \"Confluence Kimlik Doğrulama Türü\",\n      auth_type_explained:\n        \"Confluence sayfalarınıza erişmek için kullanmak istediğiniz kimlik doğrulama türünü seçin.\",\n      auth_type_username: \"Kullanıcı Adı ve Erişim Tokeni\",\n      auth_type_personal: \"Kişisel Erişim Tokeni\",\n      token: \"Confluence Erişim Tokeni\",\n      token_explained_start:\n        \"Kimlik doğrulama için bir erişim tokeni sağlamanız gerekiyor. \",\n      token_explained_link: \"Buradan\",\n      token_desc: \"Kimlik doğrulama için erişim tokeni\",\n      pat_token: \"Confluence Kişisel Erişim Tokeni\",\n      pat_token_explained: \"Confluence kişisel erişim tokeniniz.\",\n      task_explained:\n        \"Tamamlandığında, sayfa içeriği belge seçicide çalışma alanlarına gömülmeye hazır olacaktır.\",\n      bypass_ssl: \"SSL Sertifika Doğrulamasını Atla\",\n      bypass_ssl_explained:\n        \"Kendinden imzalı sertifikaya sahip kendi sunucunuzda barındırılan confluence örnekleri için SSL sertifika doğrulamasını atlamak için bu seçeneği etkinleştirin\",\n    },\n    manage: {\n      documents: \"Belgeler\",\n      \"data-connectors\": \"Veri Bağlayıcıları\",\n      \"desktop-only\":\n        \"Bu ayarları düzenlemek yalnızca masaüstü cihazda mümkündür. Devam etmek için lütfen bu sayfaya masaüstünüzden erişin.\",\n      dismiss: \"Kapat\",\n      editing: \"Düzenleniyor\",\n    },\n    directory: {\n      \"my-documents\": \"Belgelerim\",\n      \"new-folder\": \"Yeni Klasör\",\n      \"search-document\": \"Belge ara\",\n      \"no-documents\": \"Belge Yok\",\n      \"move-workspace\": \"Çalışma Alanına Taşı\",\n      \"delete-confirmation\":\n        \"Bu dosyaları ve klasörleri silmek istediğinizden emin misiniz?\\nBu, dosyaları sistemden kaldıracak ve mevcut çalışma alanlarından otomatik olarak silecektir.\\nBu işlem geri alınamaz.\",\n      \"removing-message\":\n        \"{{count}} belge ve {{folderCount}} klasör kaldırılıyor. Lütfen bekleyin.\",\n      \"move-success\": \"{{count}} belge başarıyla taşındı.\",\n      no_docs: \"Belge Yok\",\n      select_all: \"Tümünü Seç\",\n      deselect_all: \"Tümünün Seçimini Kaldır\",\n      remove_selected: \"Seçilenleri Kaldır\",\n      costs: \"*Gömmeler için tek seferlik maliyet\",\n      save_embed: \"Kaydet ve Göm\",\n      \"total-documents_one\": \"{{count}} belgesi\",\n      \"total-documents_other\": \"{{count}} belgeleri\",\n    },\n    upload: {\n      \"processor-offline\": \"Belge İşleyici Kullanılamıyor\",\n      \"processor-offline-desc\":\n        \"Belge işleyici çevrimdışı olduğu için şu anda dosyalarınızı yükleyemiyoruz. Lütfen daha sonra tekrar deneyin.\",\n      \"click-upload\": \"Yüklemek için tıklayın veya sürükleyip bırakın\",\n      \"file-types\":\n        \"metin dosyaları, csv'ler, elektronik tablolar, ses dosyaları ve daha fazlasını destekler!\",\n      \"or-submit-link\": \"veya bir bağlantı gönderin\",\n      \"placeholder-link\": \"https://ornek.com\",\n      fetching: \"Alınıyor...\",\n      \"fetch-website\": \"Web sitesini al\",\n      \"privacy-notice\":\n        \"Bu dosyalar, bu AnythingLLM örneğinde çalışan belge işleyiciye yüklenecektir. Bu dosyalar üçüncü taraflarla paylaşılmaz.\",\n    },\n    pinning: {\n      what_pinning: \"Belge sabitleme nedir?\",\n      pin_explained_block1:\n        \"AnythingLLM'de bir belgeyi <b>sabitlediğinizde</b>, belgenin tüm içeriğini LLM'nin tam olarak anlaması için prompt pencerenize enjekte ederiz.\",\n      pin_explained_block2:\n        \"Bu, <b>büyük bağlam modelleri</b> veya bilgi tabanı için kritik olan küçük dosyalarla en iyi şekilde çalışır.\",\n      pin_explained_block3:\n        \"AnythingLLM'den varsayılan olarak istediğiniz yanıtları alamıyorsanız, sabitleme tek tıklamayla daha yüksek kaliteli yanıtlar almanın harika bir yoludur.\",\n      accept: \"Tamam, anladım\",\n    },\n    watching: {\n      what_watching: \"Bir belgeyi izlemek ne yapar?\",\n      watch_explained_block1:\n        \"AnythingLLM'de bir belgeyi <b>izlediğinizde</b>, belge içeriğinizi orijinal kaynağından düzenli aralıklarla <i>otomatik olarak</i> senkronize ederiz. Bu, dosyanın yönetildiği her çalışma alanında içeriği otomatik olarak günceller.\",\n      watch_explained_block2:\n        \"Bu özellik şu anda yalnızca çevrimiçi tabanlı içeriği desteklemektedir ve manuel olarak yüklenen belgeler için kullanılamayacaktır.\",\n      watch_explained_block3_start: \"Hangi belgelerin izlendiğini \",\n      watch_explained_block3_link: \"Dosya yöneticisi\",\n      watch_explained_block3_end: \" yönetici görünümünden yönetebilirsiniz.\",\n      accept: \"Tamam, anladım\",\n    },\n    obsidian: {\n      vault_location: \"Kasa Konumu\",\n      vault_description:\n        \"Tüm notları ve bağlantılarını içe aktarmak için Obsidian kasa klasörünüzü seçin.\",\n      selected_files: \"{{count}} markdown dosyası bulundu\",\n      importing: \"Kasa içe aktarılıyor...\",\n      import_vault: \"Kasayı İçe Aktar\",\n      processing_time:\n        \"Bu işlem kasanızın boyutuna bağlı olarak biraz zaman alabilir.\",\n      vault_warning:\n        \"Herhangi bir çakışmayı önlemek için Obsidian kasanızın şu anda açık olmadığından emin olun.\",\n    },\n  },\n  chat_window: {\n    send_message: \"Mesaj gönderin\",\n    attach_file: \"Bu sohbete bir dosya ekleyin\",\n    text_size: \"Metin boyutunu değiştirin.\",\n    microphone: \"Promptunuzu söyleyin.\",\n    send: \"Çalışma alanına prompt mesajı gönderin\",\n    attachments_processing: \"Ekler işleniyor. Lütfen bekleyin...\",\n    tts_speak_message: \"TTS Mesajı Seslendir\",\n    copy: \"Kopyala\",\n    regenerate: \"Yeniden Oluştur\",\n    regenerate_response: \"Yanıtı yeniden oluştur\",\n    good_response: \"İyi yanıt\",\n    more_actions: \"Daha fazla eylem\",\n    fork: \"Çatalla\",\n    delete: \"Sil\",\n    cancel: \"İptal\",\n    edit_prompt: \"Promptu düzenle\",\n    edit_response: \"Yanıtı düzenle\",\n    preset_reset_description:\n      \"Sohbet geçmişinizi temizleyin ve yeni bir sohbet başlatın\",\n    add_new_preset: \" Yeni Ön Ayar Ekle\",\n    command: \"Komut\",\n    your_command: \"sizin-komutunuz\",\n    placeholder_prompt: \"Bu, promptunuzun önüne enjekte edilecek içeriktir.\",\n    description: \"Açıklama\",\n    placeholder_description: \"LLM'ler hakkında bir şiirle yanıt verir.\",\n    save: \"Kaydet\",\n    small: \"Küçük\",\n    normal: \"Normal\",\n    large: \"Büyük\",\n    workspace_llm_manager: {\n      search: \"LLM sağlayıcılarını ara\",\n      loading_workspace_settings: \"Çalışma alanı ayarları yükleniyor...\",\n      available_models: \"{{provider}} için Mevcut Modeller\",\n      available_models_description:\n        \"Bu çalışma alanı için kullanılacak bir model seçin.\",\n      save: \"Bu modeli kullan\",\n      saving: \"Model çalışma alanı varsayılanı olarak ayarlanıyor...\",\n      missing_credentials: \"Bu sağlayıcının kimlik bilgileri eksik!\",\n      missing_credentials_description:\n        \"Kimlik bilgilerini ayarlamak için tıklayın\",\n    },\n    submit: \"Gönder\",\n    edit_info_user:\n      '\"Gönder\" seçeneği, yapay zeka yanıtını yeniden oluşturur. \"Kaydet\" seçeneği, yalnızca sizin mesajınızı günceller.',\n    edit_info_assistant:\n      \"Yaptığınız değişiklikler doğrudan bu yanıtın içine kaydedilecektir.\",\n    see_less: \"Daha az\",\n    see_more: \"Daha Fazla\",\n    tools: \"Araçlar\",\n    browse: \"Gezin\",\n    text_size_label: \"Metin Boyutu\",\n    select_model: \"Model Seçimi\",\n    sources: \"Kaynaklar\",\n    document: \"Belge\",\n    similarity_match: \"maç\",\n    source_count_one: \"{{count}} ile ilgili bilgi\",\n    source_count_other: \"{{count}} referansları\",\n    preset_exit_description: \"Mevcut ajan oturumunu durdurun\",\n    add_new: \"Yeni ekle\",\n    edit: \"Düzenle\",\n    publish: \"Yayınla\",\n    stop_generating: \"Yanıt üretmeyi durdurun\",\n    pause_tts_speech_message: \"Mesajın metin okuma (TTS) özelliğini durdur\",\n    slash_commands: \"Komut Satırı Komutları\",\n    agent_skills: \"Ajansın Yetenekleri\",\n    manage_agent_skills: \"Temsilcinin becerilerini yönetin\",\n    agent_skills_disabled_in_session:\n      \"Aktif bir ajan oturumunda becerileri değiştirilemez. İlk olarak /exit komutunu kullanarak oturumu sonlandırın.\",\n    start_agent_session: \"Temsilci Oturumu Başlat\",\n    use_agent_session_to_use_tools:\n      'Çatınızdaki araçları kullanmak için, isteminizin başında \"@agent\" ile bir ajan oturumu başlatabilirsiniz.',\n  },\n  profile_settings: {\n    edit_account: \"Hesabı Düzenle\",\n    profile_picture: \"Profil Resmi\",\n    remove_profile_picture: \"Profil Resmini Kaldır\",\n    username: \"Kullanıcı Adı\",\n    new_password: \"Yeni Şifre\",\n    password_description: \"Şifre en az 8 karakter uzunluğunda olmalıdır\",\n    cancel: \"İptal\",\n    update_account: \"Hesabı Güncelle\",\n    theme: \"Tema Tercihi\",\n    language: \"Tercih edilen dil\",\n    failed_upload: \"Profil resmi yüklenemedi: {{error}}\",\n    upload_success: \"Profil resmi yüklendi.\",\n    failed_remove: \"Profil resmi kaldırılamadı: {{error}}\",\n    profile_updated: \"Profil güncellendi.\",\n    failed_update_user: \"Kullanıcı güncellenemedi: {{error}}\",\n    account: \"Hesap\",\n    support: \"Destek\",\n    signout: \"Çıkış Yap\",\n  },\n  customization: {\n    interface: {\n      title: \"Arayüz Tercihleri\",\n      description: \"AnythingLLM için arayüz tercihlerinizi ayarlayın.\",\n    },\n    branding: {\n      title: \"Marka & Beyaz Etiketleme\",\n      description:\n        \"AnythingLLM örneğinizi özel markalamayla beyaz etiketleyin.\",\n    },\n    chat: {\n      title: \"Sohbet\",\n      description: \"AnythingLLM için sohbet tercihlerinizi ayarlayın.\",\n      auto_submit: {\n        title: \"Konuşma Girişini Otomatik Gönder\",\n        description:\n          \"Bir sessizlik süresinden sonra konuşma girişini otomatik olarak gönderin\",\n      },\n      auto_speak: {\n        title: \"Yanıtları Otomatik Seslendir\",\n        description: \"AI yanıtlarını otomatik olarak seslendirin\",\n      },\n      spellcheck: {\n        title: \"Yazım Denetimini Etkinleştir\",\n        description:\n          \"Sohbet giriş alanında yazım denetimini etkinleştirin veya devre dışı bırakın\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Tema\",\n        description: \"Uygulama için tercih ettiğiniz renk temasını seçin.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Kaydırma Çubuğunu Göster\",\n        description:\n          \"Sohbet penceresinde kaydırma çubuğunu etkinleştirin veya devre dışı bırakın.\",\n      },\n      \"support-email\": {\n        title: \"Destek E-postası\",\n        description:\n          \"Kullanıcıların yardıma ihtiyaç duyduğunda erişebilecekleri destek e-posta adresini ayarlayın.\",\n      },\n      \"app-name\": {\n        title: \"Ad\",\n        description:\n          \"Giriş sayfasında tüm kullanıcılara gösterilen bir ad ayarlayın.\",\n      },\n      \"display-language\": {\n        title: \"Görüntüleme Dili\",\n        description:\n          \"AnythingLLM'nin kullanıcı arayüzünü görüntülemek için tercih edilen dili seçin - çeviriler mevcut olduğunda.\",\n      },\n      logo: {\n        title: \"Marka Logosu\",\n        description: \"Tüm sayfalarda göstermek için özel logonuzu yükleyin.\",\n        add: \"Özel logo ekle\",\n        recommended: \"Önerilen boyut: 800 x 200\",\n        remove: \"Kaldır\",\n        replace: \"Değiştir\",\n      },\n      \"browser-appearance\": {\n        title: \"Tarayıcı Görünümü\",\n        description:\n          \"Uygulama açıkken tarayıcı sekmesinin ve başlığının görünümünü özelleştirin.\",\n        tab: {\n          title: \"Başlık\",\n          description:\n            \"Uygulama bir tarayıcıda açıkken özel bir sekme başlığı ayarlayın.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description: \"Tarayıcı sekmesi için özel bir favicon kullanın.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Kenar Çubuğu Alt Bilgi Öğeleri\",\n        description:\n          \"Kenar çubuğunun altında görüntülenen alt bilgi öğelerini özelleştirin.\",\n        icon: \"Simge\",\n        link: \"Bağlantı\",\n      },\n      \"render-html\": {\n        title: \"Sohbette HTML Görüntüle\",\n        description:\n          \"Asistan yanıtlarında HTML yanıtlarını görüntüleyin.\\nBu, çok daha yüksek kaliteli yanıt sağlayabilir, ancak potansiyel güvenlik risklerine de yol açabilir.\",\n      },\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Bir temsilci oluşturun\",\n      editWorkspace: \"Çalışma Alanını Düzenle\",\n      uploadDocument: \"Bir belge yükleyin\",\n    },\n    greeting: \"Bugün size nasıl yardımcı olabilirim?\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Klavye Kısayolları\",\n    shortcuts: {\n      settings: \"Ayarları Aç\",\n      workspaceSettings: \"Mevcut Çalışma Alanı Ayarlarını Aç\",\n      home: \"Ana Sayfaya Git\",\n      workspaces: \"Çalışma Alanlarını Yönet\",\n      apiKeys: \"API Anahtarları Ayarları\",\n      llmPreferences: \"LLM Tercihleri\",\n      chatSettings: \"Sohbet Ayarları\",\n      help: \"Klavye kısayolları yardımını göster\",\n      showLLMSelector: \"Çalışma alanı LLM Seçicisini Göster\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Başarılı!\",\n        success_description: \"Sistem Promptunuz Topluluk Hub'ına yayınlandı!\",\n        success_thank_you: \"Topluluğa paylaştığınız için teşekkür ederiz!\",\n        view_on_hub: \"Topluluk Hub'ında Görüntüle\",\n        modal_title: \"Sistem Promptu Yayınla\",\n        name_label: \"Ad\",\n        name_description: \"Bu, sistem promptunuzun görüntü adıdır.\",\n        name_placeholder: \"Sistem Promptum\",\n        description_label: \"Açıklama\",\n        description_description:\n          \"Bu, sistem promptunuzun açıklamasıdır. Sistem promptunuzun amacını açıklamak için bunu kullanın.\",\n        tags_label: \"Etiketler\",\n        tags_description:\n          \"Etiketler, sistem promptunuzu daha kolay aramak için etiketlemek amacıyla kullanılır. Birden fazla etiket ekleyebilirsiniz. Maksimum 5 etiket. Etiket başına maksimum 20 karakter.\",\n        tags_placeholder: \"Yazın ve etiket eklemek için Enter'a basın\",\n        visibility_label: \"Görünürlük\",\n        public_description: \"Herkese açık sistem promptları herkese görünür.\",\n        private_description: \"Özel sistem promptları yalnızca size görünür.\",\n        publish_button: \"Topluluk Hub'ına Yayınla\",\n        submitting: \"Yayınlanıyor...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Bu, LLM'yi yönlendirmek için kullanılacak gerçek sistem promptudur.\",\n        prompt_placeholder: \"Sistem promptunuzu buraya girin...\",\n      },\n      agent_flow: {\n        success_title: \"Başarılı!\",\n        success_description: \"Ajan Akışınız Topluluk Hub'ına yayınlandı!\",\n        success_thank_you: \"Topluluğa paylaştığınız için teşekkür ederiz!\",\n        view_on_hub: \"Topluluk Hub'ında Görüntüle\",\n        modal_title: \"Ajan Akışı Yayınla\",\n        name_label: \"Ad\",\n        name_description: \"Bu, ajan akışınızın görüntü adıdır.\",\n        name_placeholder: \"Ajan Akışım\",\n        description_label: \"Açıklama\",\n        description_description:\n          \"Bu, ajan akışınızın açıklamasıdır. Ajan akışınızın amacını açıklamak için bunu kullanın.\",\n        tags_label: \"Etiketler\",\n        tags_description:\n          \"Etiketler, ajan akışınızı daha kolay aramak için etiketlemek amacıyla kullanılır. Birden fazla etiket ekleyebilirsiniz. Maksimum 5 etiket. Etiket başına maksimum 20 karakter.\",\n        tags_placeholder: \"Yazın ve etiket eklemek için Enter'a basın\",\n        visibility_label: \"Görünürlük\",\n        submitting: \"Yayınlanıyor...\",\n        submit: \"Topluluk Hub'ına Yayınla\",\n        privacy_note:\n          \"Ajan akışları, hassas verileri korumak için her zaman özel olarak yüklenir. Yayınladıktan sonra Topluluk Hub'ında görünürlüğü değiştirebilirsiniz. Lütfen yayınlamadan önce akışınızın hassas veya özel bilgi içermediğini doğrulayın.\",\n      },\n      slash_command: {\n        success_title: \"Başarılı!\",\n        success_description:\n          \"Eğik Çizgi Komutunuz Topluluk Hub'ına yayınlandı!\",\n        success_thank_you: \"Topluluğa paylaştığınız için teşekkür ederiz!\",\n        view_on_hub: \"Topluluk Hub'ında Görüntüle\",\n        modal_title: \"Eğik Çizgi Komutu Yayınla\",\n        name_label: \"Ad\",\n        name_description: \"Bu, eğik çizgi komutunuzun görüntü adıdır.\",\n        name_placeholder: \"Eğik Çizgi Komutum\",\n        description_label: \"Açıklama\",\n        description_description:\n          \"Bu, eğik çizgi komutunuzun açıklamasıdır. Eğik çizgi komutunuzun amacını açıklamak için bunu kullanın.\",\n        tags_label: \"Etiketler\",\n        tags_description:\n          \"Etiketler, eğik çizgi komutunuzu daha kolay aramak için etiketlemek amacıyla kullanılır. Birden fazla etiket ekleyebilirsiniz. Maksimum 5 etiket. Etiket başına maksimum 20 karakter.\",\n        tags_placeholder: \"Yazın ve etiket eklemek için Enter'a basın\",\n        visibility_label: \"Görünürlük\",\n        public_description:\n          \"Herkese açık eğik çizgi komutları herkese görünür.\",\n        private_description: \"Özel eğik çizgi komutları yalnızca size görünür.\",\n        publish_button: \"Topluluk Hub'ına Yayınla\",\n        submitting: \"Yayınlanıyor...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Bu, eğik çizgi komutu tetiklendiğinde kullanılacak prompttur.\",\n        prompt_placeholder: \"Promptunuzu buraya girin...\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Kimlik Doğrulama Gerekli\",\n          description:\n            \"Öğeleri yayınlamadan önce AnythingLLM Topluluk Hub'ına kimlik doğrulaması yapmanız gerekir.\",\n          button: \"Topluluk Hub'ına Bağlan\",\n        },\n      },\n    },\n  },\n  security: {\n    title: \"Güvenlik\",\n    multiuser: {\n      title: \"Çoklu Kullanıcı Modu\",\n      description:\n        \"Takımınızı desteklemek için örneğinizi yapılandırın ve Çoklu Kullanıcı Modunu etkinleştirin.\",\n      enable: {\n        \"is-enable\": \"Çoklu Kullanıcı Modu Etkin\",\n        enable: \"Çoklu Kullanıcı Modunu Etkinleştir\",\n        description:\n          \"Varsayılan olarak tek yönetici sizsiniz. Yönetici olarak yeni kullanıcılar veya yöneticiler için hesap oluşturmanız gerekir. Şifrenizi kaybetmeyin çünkü yalnızca bir Yönetici kullanıcı şifreleri sıfırlayabilir.\",\n        username: \"Yönetici hesap kullanıcı adı\",\n        password: \"Yönetici hesap şifresi\",\n      },\n    },\n    password: {\n      title: \"Şifre Koruması\",\n      description:\n        \"AnythingLLM örneğinizi bir şifre ile koruyun. Bu şifreyi unutmanız hâlinde kurtarma yöntemi yoktur, bu yüzden mutlaka güvende saklayın.\",\n      \"password-label\": \"Örnek şifresi\",\n    },\n  },\n  home: {\n    welcome: \"Hoşgeldiniz\",\n    chooseWorkspace: \"Bir çalışma alanı seçerek sohbete başlayın!\",\n    notAssigned:\n      \"Şu anda hiçbir çalışma alanına atanmamışsınız.\\nBir çalışma alanına erişmek için yöneticinize başvurun.\",\n    goToWorkspace: 'Çalışma alanına git \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/verifyTranslations.mjs",
    "content": "/* global process */\nimport { resources } from \"./resources.js\";\nconst languageNames = new Intl.DisplayNames(Object.keys(resources), {\n  type: \"language\",\n});\n\nfunction langDisplayName(lang) {\n  return languageNames.of(lang);\n}\n\nfunction compareStructures(lang, a, b, subdir = null) {\n  //if a and b aren't the same type, they can't be equal\n  if (typeof a !== typeof b && a !== null && b !== null) {\n    console.log(\"Invalid type comparison\", [\n      {\n        lang,\n        a: typeof a,\n        b: typeof b,\n        values: {\n          a,\n          b,\n        },\n        ...(!!subdir ? { subdir } : {}),\n      },\n    ]);\n    return false;\n  }\n\n  // Need the truthy guard because\n  // typeof null === 'object'\n  if (a && typeof a === \"object\") {\n    var keysA = Object.keys(a).sort(),\n      keysB = Object.keys(b).sort();\n\n    //if a and b are objects with different no of keys, unequal\n    if (keysA.length !== keysB.length) {\n      console.log(\"Keys are missing!\", {\n        [lang]: keysA,\n        en: keysB,\n        ...(!!subdir ? { subdir } : {}),\n        diff: {\n          added: keysB.filter((key) => !keysA.includes(key)),\n          removed: keysA.filter((key) => !keysB.includes(key)),\n        },\n      });\n      return false;\n    }\n\n    //if keys aren't all the same, unequal\n    if (\n      !keysA.every(function (k, i) {\n        return k === keysB[i];\n      })\n    ) {\n      console.log(\"Keys are not equal!\", {\n        [lang]: keysA,\n        en: keysB,\n        ...(!!subdir ? { subdir } : {}),\n      });\n      return false;\n    }\n\n    //recurse on the values for each key\n    return keysA.every(function (key) {\n      //if we made it here, they have identical keys\n      return compareStructures(lang, a[key], b[key], key);\n    });\n\n    //for primitives just ignore since we don't check values.\n  } else {\n    return true;\n  }\n}\n\nconst failed = [];\nconst TRANSLATIONS = {};\nfor (const [lang, { common }] of Object.entries(resources))\n  TRANSLATIONS[lang] = common;\nconst PRIMARY = { ...TRANSLATIONS[\"en\"] };\ndelete TRANSLATIONS[\"en\"];\n\nconsole.log(\n  `The following translation files will be verified: [${Object.keys(\n    TRANSLATIONS\n  ).join(\",\")}]`\n);\nfor (const [lang, translations] of Object.entries(TRANSLATIONS)) {\n  const passed = compareStructures(lang, translations, PRIMARY);\n  console.log(`${langDisplayName(lang)} (${lang}): ${passed ? \"✅\" : \"❌\"}`);\n  !passed && failed.push(lang);\n}\n\nif (failed.length !== 0)\n  throw new Error(\n    `The following translations files are INVALID and need fixing. Please see logs`,\n    failed\n  );\nconsole.log(\n  `👍 All translation files located match the schema defined by the English file!`\n);\nprocess.exit(0);\n"
  },
  {
    "path": "frontend/src/locales/vn/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    survey: {\n      email: \"Email của bạn là gì?\",\n      useCase: \"Bạn sẽ sử dụng AnythingLLM để làm gì?\",\n      useCaseWork: \"Cho công việc\",\n      useCasePersonal: \"Cho mục đích cá nhân\",\n      useCaseOther: \"Khác\",\n      comment: \"Bạn biết đến AnythingLLM như thế nào?\",\n      commentPlaceholder:\n        \"Reddit, Twitter, GitHub, YouTube, v.v. - Hãy cho chúng tôi biết bạn tìm thấy chúng tôi như thế nào!\",\n      skip: \"Bỏ qua Khảo sát\",\n      thankYou: \"Cảm ơn phản hồi của bạn!\",\n      title: \"Chào mừng đến với AnythingLLM\",\n      description:\n        \"Giúp chúng tôi xây dựng AnythingLLM phù hợp với nhu cầu của bạn. Tùy chọn.\",\n    },\n    home: {\n      title: \"Chào mừng đến\",\n      getStarted: \"Bắt đầu\",\n    },\n    llm: {\n      title: \"Tùy chọn LLM\",\n      description:\n        \"AnythingLLM có thể hoạt động với nhiều nhà cung cấp LLM. Đây sẽ là dịch vụ xử lý trò chuyện.\",\n    },\n    userSetup: {\n      title: \"Thiết lập Người dùng\",\n      description: \"Cấu hình cài đặt người dùng của bạn.\",\n      howManyUsers: \"Có bao nhiêu người sẽ sử dụng phiên bản này?\",\n      justMe: \"Chỉ mình tôi\",\n      myTeam: \"Nhóm của tôi\",\n      instancePassword: \"Mật khẩu Phiên bản\",\n      setPassword: \"Bạn có muốn thiết lập mật khẩu không?\",\n      passwordReq: \"Mật khẩu phải có ít nhất 8 ký tự.\",\n      passwordWarn:\n        \"Điều quan trọng là phải lưu mật khẩu này vì không có phương pháp khôi phục.\",\n      adminUsername: \"Tên người dùng tài khoản Quản trị viên\",\n      adminPassword: \"Mật khẩu tài khoản Quản trị viên\",\n      adminPasswordReq: \"Mật khẩu phải có ít nhất 8 ký tự.\",\n      teamHint:\n        \"Theo mặc định, bạn sẽ là quản trị viên duy nhất. Sau khi hoàn tất thiết lập, bạn có thể tạo và mời người khác làm người dùng hoặc quản trị viên. Không được mất mật khẩu vì chỉ quản trị viên mới có thể đặt lại mật khẩu.\",\n    },\n    data: {\n      title: \"Xử lý Dữ liệu & Quyền riêng tư\",\n      description:\n        \"Chúng tôi cam kết minh bạch và kiểm soát khi liên quan đến dữ liệu cá nhân của bạn.\",\n      settingsHint:\n        \"Các cài đặt này có thể được cấu hình lại bất cứ lúc nào trong cài đặt.\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"Tên không gian làm việc\",\n    user: \"Người dùng\",\n    selection: \"Lựa chọn mô hình\",\n    saving: \"Đang lưu...\",\n    save: \"Lưu thay đổi\",\n    previous: \"Trang trước\",\n    next: \"Trang tiếp theo\",\n    optional: \"Tùy chọn\",\n    yes: \"Có\",\n    no: \"Không\",\n    search: \"Tìm kiếm\",\n    username_requirements:\n      \"Tên người dùng phải có 2-32 ký tự, bắt đầu bằng chữ cái thường và chỉ chứa chữ cái thường, số, dấu gạch dưới, dấu gạch ngang và dấu chấm.\",\n    on: \"Về\",\n    none: \"Không\",\n    stopped: \"Dừng\",\n    loading: \"Đang tải\",\n    refresh: \"Tái tạo\",\n  },\n  settings: {\n    title: \"Cài đặt hệ thống\",\n    invites: \"Lời mời\",\n    users: \"Người dùng\",\n    workspaces: \"Không gian làm việc\",\n    \"workspace-chats\": \"Hội thoại không gian làm việc\",\n    customization: \"Tùy chỉnh\",\n    \"api-keys\": \"API nhà phát triển\",\n    llm: \"LLM\",\n    transcription: \"Chuyển đổi giọng nói\",\n    embedder: \"Nhúng dữ liệu\",\n    \"text-splitting\": \"Chia nhỏ & Tách văn bản\",\n    \"voice-speech\": \"Giọng nói & Phát âm\",\n    \"vector-database\": \"Cơ sở dữ liệu Vector\",\n    embeds: \"Nhúng hội thoại\",\n    security: \"Bảo mật\",\n    \"event-logs\": \"Nhật ký sự kiện\",\n    privacy: \"Quyền riêng tư & Dữ liệu\",\n    \"ai-providers\": \"Nhà cung cấp AI\",\n    \"agent-skills\": \"Kỹ năng của Agent\",\n    admin: \"Quản trị viên\",\n    tools: \"Công cụ\",\n    \"experimental-features\": \"Tính năng thử nghiệm\",\n    contact: \"Liên hệ hỗ trợ\",\n    \"browser-extension\": \"Tiện ích trình duyệt\",\n    \"system-prompt-variables\": \"Biến System Prompt\",\n    interface: \"Tùy chọn Giao diện\",\n    branding: \"Thương hiệu & Nhãn trắng\",\n    chat: \"Trò chuyện\",\n    \"mobile-app\": \"AnythingLLM Di động\",\n    \"community-hub\": {\n      title: \"Trung tâm cộng đồng\",\n      trending: \"Khám phá các nội dung đang thịnh hành\",\n      \"your-account\": \"Tài khoản của bạn\",\n      \"import-item\": \"Nhập hàng\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"Chào mừng đến với\",\n      \"placeholder-username\": \"Tên người dùng\",\n      \"placeholder-password\": \"Mật khẩu\",\n      login: \"Đăng nhập\",\n      validating: \"Đang xác thực...\",\n      \"forgot-pass\": \"Quên mật khẩu\",\n      reset: \"Đặt lại\",\n    },\n    \"sign-in\": \"Đăng nhập vào {{appName}} tài khoản của bạn.\",\n    \"password-reset\": {\n      title: \"Đặt lại Mật khẩu\",\n      description: \"Cung cấp thông tin cần thiết dưới đây để đặt lại mật khẩu.\",\n      \"recovery-codes\": \"Mã khôi phục\",\n      \"back-to-login\": \"Quay lại Đăng nhập\",\n    },\n  },\n  \"new-workspace\": {\n    title: \"Không gian làm việc mới\",\n    placeholder: \"Không gian làm việc của tôi\",\n  },\n  \"workspaces—settings\": {\n    general: \"Cài đặt chung\",\n    chat: \"Cài đặt Trò chuyện\",\n    vector: \"Cơ sở dữ liệu Vector\",\n    members: \"Thành viên\",\n    agent: \"Cấu hình Agent\",\n  },\n  general: {\n    vector: {\n      title: \"Số lượng Vector\",\n      description: \"Tổng số vector trong cơ sở dữ liệu vector của bạn.\",\n    },\n    names: {\n      description:\n        \"Điều này chỉ thay đổi tên hiển thị của không gian làm việc.\",\n    },\n    message: {\n      title: \"Tin nhắn trò chuyện được gợi ý\",\n      description:\n        \"Tùy chỉnh các tin nhắn sẽ được gợi ý cho người dùng không gian làm việc của bạn.\",\n      add: \"Thêm tin nhắn mới\",\n      save: \"Lưu Tin nhắn\",\n      heading: \"Giải thích cho tôi\",\n      body: \"các lợi ích của AnythingLLM\",\n    },\n    delete: {\n      title: \"Xóa không gian làm việc\",\n      description:\n        \"Xóa không gian làm việc này và tất cả dữ liệu của nó. Điều này sẽ xóa không gian làm việc cho tất cả người dùng.\",\n      delete: \"Xóa không gian làm việc\",\n      deleting: \"Đang xóa Không gian làm việc...\",\n      \"confirm-start\": \"Bạn sắp xóa toàn bộ\",\n      \"confirm-end\":\n        \"không gian làm việc. Điều này sẽ xóa tất cả vector embedding trong cơ sở dữ liệu vector của bạn.\\n\\nCác tệp nguồn gốc sẽ không bị ảnh hưởng. Hành động này không thể hoàn tác.\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"Nhà cung cấp LLM Không gian làm việc\",\n      description:\n        \"Nhà cung cấp LLM và mô hình cụ thể sẽ được sử dụng cho không gian làm việc này. Theo mặc định, nó sử dụng nhà cung cấp LLM hệ thống và cài đặt.\",\n      search: \"Tìm kiếm tất cả nhà cung cấp LLM\",\n    },\n    model: {\n      title: \"Mô hình Trò chuyện Không gian làm việc\",\n      description:\n        \"Mô hình trò chuyện cụ thể sẽ được sử dụng cho không gian làm việc này. Nếu để trống, sẽ sử dụng tùy chọn LLM hệ thống.\",\n    },\n    mode: {\n      title: \"Chế độ trò chuyện\",\n      chat: {\n        title: \"Trò chuyện\",\n        description:\n          \"sẽ cung cấp câu trả lời dựa trên kiến thức chung của LLM và ngữ cảnh tài liệu được cung cấp.<br />Bạn cần sử dụng lệnh @agent để sử dụng các công cụ.\",\n      },\n      query: {\n        title: \"Truy vấn\",\n        description:\n          \"sẽ cung cấp câu trả lời <b>chỉ</b> khi ngữ cảnh của tài liệu được tìm thấy.<br />Bạn cần sử dụng lệnh @agent để sử dụng các công cụ.\",\n      },\n      automatic: {\n        title: \"Tự động\",\n        description:\n          \"sẽ tự động sử dụng các công cụ nếu mô hình và nhà cung cấp hỗ trợ gọi công cụ gốc.<br />Nếu không hỗ trợ gọi công cụ gốc, bạn sẽ cần sử dụng lệnh `@agent` để sử dụng các công cụ.\",\n      },\n    },\n    history: {\n      title: \"Lịch sử Trò chuyện\",\n      \"desc-start\":\n        \"Số lượng cuộc trò chuyện trước đó sẽ được bao gồm trong bộ nhớ ngắn hạn của phản hồi.\",\n      recommend: \"Khuyến nghị 20. \",\n      \"desc-end\":\n        \"Bất kỳ số nào lớn hơn 45 có thể dẫn đến lỗi trò chuyện liên tục tùy thuộc vào kích thước tin nhắn.\",\n    },\n    prompt: {\n      title: \"Prompt\",\n      description:\n        \"Nhập vào đây prompt cho không gian làm việc này. Định nghĩa ngữ cảnh và hướng dẫn cho AI để tạo ra một phản hồi liên quan và chính xác.\",\n      history: {\n        title: \"Lịch sử System Prompt\",\n        clearAll: \"Xóa Tất cả\",\n        noHistory: \"Không có lịch sử system prompt\",\n        restore: \"Khôi phục\",\n        delete: \"Xóa\",\n        deleteConfirm: \"Bạn có chắc chắn muốn xóa mục lịch sử này?\",\n        clearAllConfirm:\n          \"Bạn có chắc chắn muốn xóa tất cả lịch sử? Hành động này không thể hoàn tác.\",\n        expand: \"Mở rộng\",\n        publish: \"Đăng lên Community Hub\",\n      },\n    },\n    refusal: {\n      title: \"Phản hồi từ chối chế độ truy vấn\",\n      \"desc-start\": \"Khi ở chế độ\",\n      query: \"truy vấn\",\n      \"desc-end\":\n        \", bạn có thể muốn trả về phản hồi từ chối tùy chỉnh khi không tìm thấy ngữ cảnh.\",\n      \"tooltip-title\": \"Tại sao tôi thấy điều này?\",\n      \"tooltip-description\":\n        \"Bạn đang ở chế độ truy vấn, chỉ sử dụng thông tin từ tài liệu của bạn. Chuyển sang chế độ trò chuyện để có cuộc trò chuyện linh hoạt hơn, hoặc nhấp vào đây để truy cập tài liệu của chúng tôi để tìm hiểu thêm về các chế độ trò chuyện.\",\n    },\n    temperature: {\n      title: \"Nhiệt độ LLM\",\n      \"desc-start\": 'Cài đặt này kiểm soát mức độ \"sáng tạo\" của phản hồi LLM.',\n      \"desc-end\":\n        \"Số càng cao thì càng sáng tạo. Đối với một số mô hình, điều này có thể dẫn đến phản hồi không mạch lạc khi đặt quá cao.\",\n      hint: \"Hầu hết các LLM có các phạm vi giá trị hợp lệ khác nhau. Tham khảo nhà cung cấp LLM của bạn để biết thông tin đó.\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"Định danh cơ sở dữ liệu vector\",\n    snippets: {\n      title: \"Đoạn Ngữ cảnh Tối đa\",\n      description:\n        \"Cài đặt này kiểm soát số lượng đoạn ngữ cảnh tối đa sẽ được gửi đến LLM cho mỗi cuộc trò chuyện hoặc truy vấn.\",\n      recommend: \"Khuyến nghị: 4\",\n    },\n    doc: {\n      title: \"Ngưỡng tương đồng tài liệu\",\n      description:\n        \"Điểm tương đồng tối thiểu cần thiết để một nguồn được coi là liên quan đến cuộc trò chuyện. Số càng cao, nguồn phải càng tương tự với cuộc trò chuyện.\",\n      zero: \"Không hạn chế\",\n      low: \"Thấp (điểm tương đồng ≥ .25)\",\n      medium: \"Trung bình (điểm tương đồng ≥ .50)\",\n      high: \"Cao (điểm tương đồng ≥ .75)\",\n    },\n    reset: {\n      reset: \"Đặt lại Cơ sở dữ liệu Vector\",\n      resetting: \"Đang xóa vectors...\",\n      confirm:\n        \"Bạn sắp đặt lại cơ sở dữ liệu vector của không gian làm việc này. Điều này sẽ xóa tất cả vector embedding hiện đang được nhúng.\\n\\nCác tệp nguồn gốc sẽ không bị ảnh hưởng. Hành động này không thể hoàn tác.\",\n      error: \"Không thể đặt lại cơ sở dữ liệu vector của không gian làm việc!\",\n      success: \"Cơ sở dữ liệu vector của không gian làm việc đã được đặt lại!\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"Hiệu suất của các LLM không hỗ trợ rõ ràng việc gọi công cụ phụ thuộc rất nhiều vào khả năng và độ chính xác của mô hình. Một số khả năng có thể bị hạn chế hoặc không hoạt động.\",\n    provider: {\n      title: \"Nhà cung cấp LLM cho Agent Không gian làm việc\",\n      description:\n        \"Nhà cung cấp LLM & mô hình cụ thể sẽ được sử dụng cho @agent agent của không gian làm việc này.\",\n    },\n    mode: {\n      chat: {\n        title: \"Mô hình Trò chuyện cho Agent Không gian làm việc\",\n        description:\n          \"Mô hình trò chuyện cụ thể sẽ được sử dụng cho @agent agent của không gian làm việc này.\",\n      },\n      title: \"Mô hình Agent Không gian làm việc\",\n      description:\n        \"Mô hình LLM cụ thể sẽ được sử dụng cho @agent agent của không gian làm việc này.\",\n      wait: \"-- đang chờ mô hình --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG & bộ nhớ dài hạn\",\n        description:\n          'Cho phép agent sử dụng tài liệu cục bộ của bạn để trả lời truy vấn hoặc yêu cầu agent \"ghi nhớ\" các phần nội dung để truy xuất bộ nhớ dài hạn.',\n      },\n      view: {\n        title: \"Xem & tóm tắt tài liệu\",\n        description:\n          \"Cho phép agent liệt kê và tóm tắt nội dung của các tệp không gian làm việc hiện đang được nhúng.\",\n      },\n      scrape: {\n        title: \"Thu thập dữ liệu website\",\n        description:\n          \"Cho phép agent truy cập và thu thập nội dung của các website.\",\n      },\n      generate: {\n        title: \"Tạo biểu đồ\",\n        description:\n          \"Cho phép agent mặc định tạo các loại biểu đồ khác nhau từ dữ liệu được cung cấp hoặc đưa ra trong trò chuyện.\",\n      },\n      save: {\n        title: \"Tạo & lưu tệp\",\n        description:\n          \"Cho phép agent mặc định tạo và ghi vào các tệp có thể lưu vào máy tính của bạn.\",\n      },\n      web: {\n        title: \"Tìm kiếm web trực tiếp và duyệt web\",\n        description:\n          \"Cho phép đại lý của bạn tìm kiếm trên web để trả lời các câu hỏi của bạn bằng cách kết nối với nhà cung cấp dịch vụ tìm kiếm trên web (SERP).\",\n      },\n      sql: {\n        title: \"Kết nối SQL\",\n        description:\n          \"Cho phép đại lý của bạn sử dụng SQL để trả lời các câu hỏi của bạn bằng cách kết nối với nhiều nhà cung cấp cơ sở dữ liệu SQL khác nhau.\",\n      },\n      default_skill:\n        \"Theo mặc định, kỹ năng này được kích hoạt, nhưng bạn có thể tắt nó nếu không muốn nó được sử dụng bởi người đại diện.\",\n    },\n    mcp: {\n      title: \"Máy chủ MCP\",\n      \"loading-from-config\": \"Tải các máy chủ MCP từ tệp cấu hình\",\n      \"learn-more\": \"Tìm hiểu thêm về máy chủ MCP.\",\n      \"no-servers-found\": \"Không tìm thấy máy chủ MCP.\",\n      \"tool-warning\":\n        \"Để đạt hiệu suất tốt nhất, hãy cân nhắc việc tắt các công cụ không cần thiết để tiết kiệm tài nguyên.\",\n      \"stop-server\": \"Tắt máy chủ MCP\",\n      \"start-server\": \"Khởi động máy chủ MCP\",\n      \"delete-server\": \"Xóa máy chủ MCP\",\n      \"tool-count-warning\":\n        \"Máy chủ MCP này có các công cụ <b> được kích hoạt, {{count}} và chúng sẽ tiêu thụ ngữ cảnh trong mọi cuộc trò chuyện.</b> Hãy cân nhắc việc tắt các công cụ không cần thiết để tiết kiệm ngữ cảnh.\",\n      \"startup-command\": \"Lệnh khởi động\",\n      command: \"Lệnh\",\n      arguments: \"Luận điểm\",\n      \"not-running-warning\":\n        \"Máy chủ MCP này không hoạt động – có thể nó đã bị tắt hoặc đang gặp lỗi khi khởi động.\",\n      \"tool-call-arguments\": \"Tham số khi gọi hàm/thao tác\",\n      \"tools-enabled\": \"các công cụ đã được kích hoạt\",\n    },\n    settings: {\n      title: \"Cài đặt kỹ năng của đại lý\",\n      \"max-tool-calls\": {\n        title: \"Số lượng lệnh gọi công cụ tối đa cho mỗi phản hồi\",\n        description:\n          \"Số lượng công cụ tối đa mà một người dùng có thể liên kết để tạo ra một phản hồi duy nhất. Điều này ngăn chặn việc gọi công cụ quá mức và tạo ra các vòng lặp vô hạn.\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"Lựa chọn kỹ năng thông minh\",\n        \"beta-badge\": \"Phiên bản thử nghiệm\",\n        description:\n          \"Cho phép sử dụng không giới hạn các công cụ và giảm mức sử dụng token lên đến 80% cho mỗi truy vấn – AnythingLLM tự động chọn các kỹ năng phù hợp nhất cho mỗi yêu cầu.\",\n        \"max-tools\": {\n          title: \"Công cụ Max\",\n          description:\n            \"Số lượng công cụ tối đa có thể chọn cho mỗi truy vấn. Chúng tôi khuyến nghị đặt giá trị này thành các giá trị lớn hơn đối với các mô hình có ngữ cảnh lớn hơn.\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"Hội thoại không gian làm việc\",\n    description:\n      \"Đây là tất cả các cuộc trò chuyện và tin nhắn đã được ghi lại được gửi bởi người dùng, sắp xếp theo ngày tạo.\",\n    export: \"Xuất\",\n    table: {\n      id: \"Id\",\n      by: \"Gửi bởi\",\n      workspace: \"Không gian làm việc\",\n      prompt: \"Prompt\",\n      response: \"Phản hồi\",\n      at: \"Gửi lúc\",\n    },\n  },\n  api: {\n    title: \"Khóa API\",\n    description:\n      \"Khóa API cho phép người sở hữu truy cập và quản lý phiên bản AnythingLLM này theo chương trình.\",\n    link: \"Đọc tài liệu API\",\n    generate: \"Tạo Khóa API Mới\",\n    table: {\n      key: \"Khóa API\",\n      by: \"Tạo bởi\",\n      created: \"Ngày tạo\",\n    },\n  },\n  llm: {\n    title: \"Tùy chọn LLM\",\n    description:\n      \"Đây là thông tin đăng nhập và cài đặt cho nhà cung cấp LLM trò chuyện & nhúng ưa thích của bạn. Điều quan trọng là các khóa này phải chính xác, nếu không AnythingLLM sẽ không hoạt động đúng.\",\n    provider: \"Nhà cung cấp LLM\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Điểm cuối Dịch vụ Azure\",\n        api_key: \"Khóa API\",\n        chat_deployment_name: \"Tên Triển khai Trò chuyện\",\n        chat_model_token_limit: \"Giới hạn Token Mô hình Trò chuyện\",\n        model_type: \"Loại Mô hình\",\n        default: \"Mặc định\",\n        reasoning: \"Lý luận\",\n        model_type_tooltip:\n          'Nếu triển khai của bạn sử dụng mô hình lý luận (o1, o1-mini, o3-mini, v.v.), hãy đặt thành \"Lý luận\". Nếu không, yêu cầu trò chuyện của bạn có thể thất bại.',\n      },\n    },\n  },\n  transcription: {\n    title: \"Tùy chọn Mô hình Chuyển đổi giọng nói\",\n    description:\n      \"Đây là thông tin đăng nhập và cài đặt cho nhà cung cấp mô hình chuyển đổi giọng nói ưa thích của bạn. Điều quan trọng là các khóa này phải chính xác, nếu không tệp media và âm thanh sẽ không được chuyển đổi.\",\n    provider: \"Nhà cung cấp Chuyển đổi giọng nói\",\n    \"warn-start\":\n      \"Sử dụng mô hình whisper cục bộ trên máy có RAM hoặc CPU hạn chế có thể làm AnythingLLM bị treo khi xử lý tệp media.\",\n    \"warn-recommend\":\n      \"Chúng tôi khuyến nghị ít nhất 2GB RAM và tải lên tệp <10Mb.\",\n    \"warn-end\": \"Mô hình tích hợp sẽ tự động tải xuống khi sử dụng lần đầu.\",\n  },\n  embedding: {\n    title: \"Tùy chọn nhúng\",\n    \"desc-start\":\n      \"Khi sử dụng LLM không hỗ trợ bộ máy nhúng nguyên bản - bạn có thể cần chỉ định thêm thông tin đăng nhập để nhúng văn bản.\",\n    \"desc-end\":\n      \"Nhúng là quá trình chuyển đổi văn bản thành vector. Thông tin đăng nhập này cần thiết để chuyển đổi tệp và prompt của bạn thành định dạng mà AnythingLLM có thể sử dụng để xử lý.\",\n    provider: {\n      title: \"Nhà cung cấp Nhúng\",\n    },\n  },\n  text: {\n    title: \"Tùy chọn chia nhỏ và tách văn bản\",\n    \"desc-start\":\n      \"Đôi khi, bạn có thể muốn thay đổi cách mặc định mà các tài liệu mới được chia nhỏ và tách trước khi được chèn vào cơ sở dữ liệu vector của bạn.\",\n    \"desc-end\":\n      \"Bạn chỉ nên sửa đổi cài đặt này nếu bạn hiểu cách chia văn bản hoạt động và các tác động phụ của nó.\",\n    size: {\n      title: \"Kích thước Đoạn Văn bản\",\n      description:\n        \"Đây là độ dài tối đa của các ký tự có thể có trong một vector đơn.\",\n      recommend: \"Độ dài tối đa của mô hình nhúng là\",\n    },\n    overlap: {\n      title: \"Độ Chồng lấp Đoạn Văn bản\",\n      description:\n        \"Đây là độ chồng lấp tối đa của các ký tự xảy ra trong quá trình tách giữa hai đoạn văn bản liền kề.\",\n    },\n  },\n  vector: {\n    title: \"Cơ sở dữ liệu Vector\",\n    description:\n      \"Đây là thông tin đăng nhập và cài đặt cho cách phiên bản AnythingLLM của bạn sẽ hoạt động. Điều quan trọng là các khóa này phải chính xác.\",\n    provider: {\n      title: \"Nhà cung cấp Cơ sở dữ liệu Vector\",\n      description: \"Không cần cấu hình cho LanceDB.\",\n    },\n  },\n  embeddable: {\n    title: \"Tiện ích hội thoại nhúng\",\n    description:\n      \"Tiện ích trò chuyện nhúng là giao diện trò chuyện công khai được liên kết với một không gian làm việc duy nhất. Điều này cho phép bạn xây dựng không gian làm việc mà sau đó bạn có thể xuất bản ra thế giới.\",\n    create: \"Tạo nhúng\",\n    table: {\n      workspace: \"Không gian làm việc\",\n      chats: \"Trò chuyện đã gửi\",\n      active: \"Tên miền Hoạt động\",\n      created: \"Ngày tạo\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"Lịch sử Nhúng Trò chuyện\",\n    export: \"Xuất\",\n    description:\n      \"Đây là tất cả các cuộc trò chuyện và tin nhắn đã được ghi lại từ bất kỳ nhúng nào mà bạn đã xuất bản.\",\n    table: {\n      embed: \"Nhúng\",\n      sender: \"Người gửi\",\n      message: \"Tin nhắn\",\n      response: \"Phản hồi\",\n      at: \"Gửi lúc\",\n    },\n  },\n  event: {\n    title: \"Nhật ký sự kiện\",\n    description:\n      \"Xem tất cả các hành động và sự kiện đang xảy ra trên phiên bản này để giám sát.\",\n    clear: \"Xóa Nhật ký sự kiện\",\n    table: {\n      type: \"Loại Sự kiện\",\n      user: \"Người dùng\",\n      occurred: \"Xảy ra lúc\",\n    },\n  },\n  privacy: {\n    title: \"Quyền riêng tư & Xử lý Dữ liệu\",\n    description:\n      \"Đây là cấu hình của bạn về cách các nhà cung cấp bên thứ ba được kết nối và AnythingLLM xử lý dữ liệu của bạn.\",\n    anonymous: \"Đã Bật Telemetry Ẩn danh\",\n  },\n  connectors: {\n    \"search-placeholder\": \"Tìm kiếm trình kết nối dữ liệu\",\n    \"no-connectors\": \"Không tìm thấy trình kết nối dữ liệu.\",\n    github: {\n      name: \"Kho GitHub\",\n      description:\n        \"Nhập toàn bộ kho GitHub công khai hoặc riêng tư chỉ với một cú nhấp chuột.\",\n      URL: \"URL Kho GitHub\",\n      URL_explained: \"URL của kho GitHub bạn muốn thu thập.\",\n      token: \"Token Truy cập GitHub\",\n      optional: \"tùy chọn\",\n      token_explained: \"Token truy cập để ngăn giới hạn tốc độ.\",\n      token_explained_start: \"Nếu không có \",\n      token_explained_link1: \"Token Truy cập Cá nhân\",\n      token_explained_middle:\n        \", API GitHub có thể giới hạn số lượng tệp có thể thu thập do giới hạn tốc độ. Bạn có thể \",\n      token_explained_link2: \"tạo Token Truy cập tạm thời\",\n      token_explained_end: \" để tránh vấn đề này.\",\n      ignores: \"Bỏ qua Tệp\",\n      git_ignore:\n        \"Danh sách theo định dạng .gitignore để bỏ qua các tệp cụ thể trong quá trình thu thập. Nhấn enter sau mỗi mục bạn muốn lưu.\",\n      task_explained:\n        \"Khi hoàn tất, tất cả các tệp sẽ có sẵn để nhúng vào không gian làm việc trong bộ chọn tài liệu.\",\n      branch: \"Nhánh bạn muốn thu thập tệp.\",\n      branch_loading: \"-- đang tải các nhánh có sẵn --\",\n      branch_explained: \"Nhánh bạn muốn thu thập tệp.\",\n      token_information:\n        \"Nếu không điền <b>Token Truy cập GitHub</b>, trình kết nối dữ liệu này chỉ có thể thu thập các tệp <b>cấp cao nhất</b> của kho do giới hạn tốc độ API công khai của GitHub.\",\n      token_personal:\n        \"Nhận Token Truy cập Cá nhân miễn phí với tài khoản GitHub tại đây.\",\n    },\n    gitlab: {\n      name: \"Kho GitLab\",\n      description:\n        \"Nhập toàn bộ kho GitLab công khai hoặc riêng tư chỉ với một cú nhấp chuột.\",\n      URL: \"URL Kho GitLab\",\n      URL_explained: \"URL của kho GitLab bạn muốn thu thập.\",\n      token: \"Token Truy cập GitLab\",\n      optional: \"tùy chọn\",\n      token_description: \"Chọn các thực thể bổ sung để lấy từ API GitLab.\",\n      token_explained_start: \"Nếu không có \",\n      token_explained_link1: \"Token Truy cập Cá nhân\",\n      token_explained_middle:\n        \", API GitLab có thể giới hạn số lượng tệp có thể thu thập do giới hạn tốc độ. Bạn có thể \",\n      token_explained_link2: \"tạo Token Truy cập tạm thời\",\n      token_explained_end: \" để tránh vấn đề này.\",\n      fetch_issues: \"Lấy Issues dưới dạng Tài liệu\",\n      ignores: \"Bỏ qua Tệp\",\n      git_ignore:\n        \"Danh sách theo định dạng .gitignore để bỏ qua các tệp cụ thể trong quá trình thu thập. Nhấn enter sau mỗi mục bạn muốn lưu.\",\n      task_explained:\n        \"Khi hoàn tất, tất cả các tệp sẽ có sẵn để nhúng vào không gian làm việc trong bộ chọn tài liệu.\",\n      branch: \"Nhánh bạn muốn thu thập tệp\",\n      branch_loading: \"-- đang tải các nhánh có sẵn --\",\n      branch_explained: \"Nhánh bạn muốn thu thập tệp.\",\n      token_information:\n        \"Nếu không điền <b>Token Truy cập GitLab</b>, trình kết nối dữ liệu này chỉ có thể thu thập các tệp <b>cấp cao nhất</b> của kho do giới hạn tốc độ API công khai của GitLab.\",\n      token_personal:\n        \"Nhận Token Truy cập Cá nhân miễn phí với tài khoản GitLab tại đây.\",\n    },\n    youtube: {\n      name: \"Bản ghi YouTube\",\n      description: \"Nhập bản ghi của toàn bộ video YouTube từ một liên kết.\",\n      URL: \"URL Video YouTube\",\n      URL_explained_start:\n        \"Nhập URL của bất kỳ video YouTube nào để lấy bản ghi. Video phải có \",\n      URL_explained_link: \"phụ đề đóng\",\n      URL_explained_end: \" có sẵn.\",\n      task_explained:\n        \"Khi hoàn tất, bản ghi sẽ có sẵn để nhúng vào không gian làm việc trong bộ chọn tài liệu.\",\n    },\n    \"website-depth\": {\n      name: \"Trình thu thập Liên kết Hàng loạt\",\n      description:\n        \"Thu thập một website và các liên kết con của nó đến một độ sâu nhất định.\",\n      URL: \"URL Website\",\n      URL_explained: \"URL của website bạn muốn thu thập.\",\n      depth: \"Độ sâu Thu thập\",\n      depth_explained:\n        \"Đây là số lượng liên kết con mà worker sẽ theo dõi từ URL gốc.\",\n      max_pages: \"Số trang Tối đa\",\n      max_pages_explained: \"Số lượng liên kết tối đa để thu thập.\",\n      task_explained:\n        \"Khi hoàn tất, tất cả nội dung đã thu thập sẽ có sẵn để nhúng vào không gian làm việc trong bộ chọn tài liệu.\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"Nhập toàn bộ trang Confluence chỉ với một cú nhấp chuột.\",\n      deployment_type: \"Loại triển khai Confluence\",\n      deployment_type_explained:\n        \"Xác định phiên bản Confluence của bạn được lưu trữ trên đám mây Atlassian hay tự lưu trữ.\",\n      base_url: \"URL cơ sở Confluence\",\n      base_url_explained: \"Đây là URL cơ sở của không gian Confluence của bạn.\",\n      space_key: \"Khóa không gian Confluence\",\n      space_key_explained:\n        \"Đây là khóa không gian của phiên bản confluence của bạn sẽ được sử dụng. Thường bắt đầu bằng ~\",\n      username: \"Tên người dùng Confluence\",\n      username_explained: \"Tên người dùng Confluence của bạn\",\n      auth_type: \"Loại Xác thực Confluence\",\n      auth_type_explained:\n        \"Chọn loại xác thực bạn muốn sử dụng để truy cập các trang Confluence của mình.\",\n      auth_type_username: \"Tên người dùng và Token Truy cập\",\n      auth_type_personal: \"Token Truy cập Cá nhân\",\n      token: \"Token Truy cập Confluence\",\n      token_explained_start:\n        \"Bạn cần cung cấp token truy cập để xác thực. Bạn có thể tạo token truy cập \",\n      token_explained_link: \"tại đây\",\n      token_desc: \"Token truy cập để xác thực\",\n      pat_token: \"Token Truy cập Cá nhân Confluence\",\n      pat_token_explained: \"Token truy cập cá nhân Confluence của bạn.\",\n      task_explained:\n        \"Khi hoàn tất, nội dung trang sẽ có sẵn để nhúng vào không gian làm việc trong bộ chọn tài liệu.\",\n      bypass_ssl: \"Bỏ qua Xác thực Chứng chỉ SSL\",\n      bypass_ssl_explained:\n        \"Bật tùy chọn này để bỏ qua xác thực chứng chỉ SSL cho các phiên bản confluence tự lưu trữ với chứng chỉ tự ký\",\n    },\n    manage: {\n      documents: \"Tài liệu\",\n      \"data-connectors\": \"Trình kết nối Dữ liệu\",\n      \"desktop-only\":\n        \"Chỉnh sửa các cài đặt này chỉ có sẵn trên thiết bị máy tính để bàn. Vui lòng truy cập trang này trên máy tính để bàn của bạn để tiếp tục.\",\n      dismiss: \"Đóng\",\n      editing: \"Đang chỉnh sửa\",\n    },\n    directory: {\n      \"my-documents\": \"Tài liệu của tôi\",\n      \"new-folder\": \"Thư mục Mới\",\n      \"search-document\": \"Tìm kiếm tài liệu\",\n      \"no-documents\": \"Không có Tài liệu\",\n      \"move-workspace\": \"Di chuyển đến Không gian làm việc\",\n      \"delete-confirmation\":\n        \"Bạn có chắc chắn muốn xóa các tệp và thư mục này?\\nĐiều này sẽ xóa các tệp khỏi hệ thống và tự động xóa chúng khỏi bất kỳ không gian làm việc hiện có nào.\\nHành động này không thể hoàn tác.\",\n      \"removing-message\":\n        \"Đang xóa {{count}} tài liệu và {{folderCount}} thư mục. Vui lòng chờ.\",\n      \"move-success\": \"Đã di chuyển thành công {{count}} tài liệu.\",\n      no_docs: \"Không có Tài liệu\",\n      select_all: \"Chọn Tất cả\",\n      deselect_all: \"Bỏ chọn Tất cả\",\n      remove_selected: \"Xóa Đã chọn\",\n      costs: \"*Chi phí một lần cho việc nhúng\",\n      save_embed: \"Lưu và Nhúng\",\n      \"total-documents_one\": \"{{count}}\",\n      \"total-documents_other\": \"{{count}}\",\n    },\n    upload: {\n      \"processor-offline\": \"Trình xử lý Tài liệu Không khả dụng\",\n      \"processor-offline-desc\":\n        \"Chúng tôi không thể tải lên tệp của bạn ngay bây giờ vì trình xử lý tài liệu đang ngoại tuyến. Vui lòng thử lại sau.\",\n      \"click-upload\": \"Nhấp để tải lên hoặc kéo và thả\",\n      \"file-types\":\n        \"hỗ trợ tệp văn bản, csv, bảng tính, tệp âm thanh và hơn thế nữa!\",\n      \"or-submit-link\": \"hoặc gửi liên kết\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"Đang lấy...\",\n      \"fetch-website\": \"Lấy website\",\n      \"privacy-notice\":\n        \"Các tệp này sẽ được tải lên trình xử lý tài liệu đang chạy trên phiên bản AnythingLLM này. Các tệp này không được gửi hoặc chia sẻ với bên thứ ba.\",\n    },\n    pinning: {\n      what_pinning: \"Ghim tài liệu là gì?\",\n      pin_explained_block1:\n        \"Khi bạn <b>ghim</b> một tài liệu trong AnythingLLM, chúng tôi sẽ đưa toàn bộ nội dung của tài liệu vào cửa sổ prompt của bạn để LLM hiểu đầy đủ.\",\n      pin_explained_block2:\n        \"Điều này hoạt động tốt nhất với <b>mô hình ngữ cảnh lớn</b> hoặc các tệp nhỏ quan trọng với cơ sở kiến thức của nó.\",\n      pin_explained_block3:\n        \"Nếu bạn không nhận được câu trả lời mong muốn từ AnythingLLM theo mặc định, ghim là một cách tuyệt vời để có được câu trả lời chất lượng cao hơn chỉ với một cú nhấp chuột.\",\n      accept: \"Ok, tôi hiểu rồi\",\n    },\n    watching: {\n      what_watching: \"Theo dõi tài liệu làm gì?\",\n      watch_explained_block1:\n        \"Khi bạn <b>theo dõi</b> một tài liệu trong AnythingLLM, chúng tôi sẽ <i>tự động</i> đồng bộ nội dung tài liệu của bạn từ nguồn gốc theo các khoảng thời gian đều đặn. Điều này sẽ tự động cập nhật nội dung trong mọi không gian làm việc nơi tệp này được quản lý.\",\n      watch_explained_block2:\n        \"Tính năng này hiện chỉ hỗ trợ nội dung dựa trên trực tuyến và sẽ không khả dụng cho các tài liệu được tải lên thủ công.\",\n      watch_explained_block3_start:\n        \"Bạn có thể quản lý những tài liệu nào đang được theo dõi từ \",\n      watch_explained_block3_link: \"Trình quản lý tệp\",\n      watch_explained_block3_end: \" chế độ xem quản trị.\",\n      accept: \"Ok, tôi hiểu rồi\",\n    },\n    obsidian: {\n      vault_location: \"Vị trí Kho\",\n      vault_description:\n        \"Chọn thư mục kho Obsidian của bạn để nhập tất cả ghi chú và kết nối của chúng.\",\n      selected_files: \"Tìm thấy {{count}} tệp markdown\",\n      importing: \"Đang nhập kho...\",\n      import_vault: \"Nhập Kho\",\n      processing_time:\n        \"Điều này có thể mất một lúc tùy thuộc vào kích thước kho của bạn.\",\n      vault_warning:\n        \"Để tránh xung đột, hãy đảm bảo kho Obsidian của bạn hiện không mở.\",\n    },\n  },\n  chat_window: {\n    send_message: \"Gửi tin nhắn\",\n    attach_file: \"Đính kèm tệp vào cuộc trò chuyện này\",\n    text_size: \"Thay đổi kích thước văn bản.\",\n    microphone: \"Nói prompt của bạn.\",\n    send: \"Gửi tin nhắn prompt đến không gian làm việc\",\n    attachments_processing: \"Đang xử lý tệp đính kèm. Vui lòng chờ...\",\n    tts_speak_message: \"TTS Đọc tin nhắn\",\n    copy: \"Sao chép\",\n    regenerate: \"Tạo lại\",\n    regenerate_response: \"Tạo lại phản hồi\",\n    good_response: \"Phản hồi tốt\",\n    more_actions: \"Thêm hành động\",\n    fork: \"Rẽ nhánh\",\n    delete: \"Xóa\",\n    cancel: \"Hủy\",\n    edit_prompt: \"Chỉnh sửa prompt\",\n    edit_response: \"Chỉnh sửa phản hồi\",\n    preset_reset_description:\n      \"Xóa lịch sử trò chuyện và bắt đầu cuộc trò chuyện mới\",\n    add_new_preset: \" Thêm Cài đặt sẵn Mới\",\n    command: \"Lệnh\",\n    your_command: \"lệnh-của-bạn\",\n    placeholder_prompt: \"Đây là nội dung sẽ được đưa vào trước prompt của bạn.\",\n    description: \"Mô tả\",\n    placeholder_description: \"Phản hồi bằng một bài thơ về LLM.\",\n    save: \"Lưu\",\n    small: \"Nhỏ\",\n    normal: \"Bình thường\",\n    large: \"Lớn\",\n    workspace_llm_manager: {\n      search: \"Tìm kiếm nhà cung cấp LLM\",\n      loading_workspace_settings: \"Đang tải cài đặt không gian làm việc...\",\n      available_models: \"Mô hình Có sẵn cho {{provider}}\",\n      available_models_description:\n        \"Chọn một mô hình để sử dụng cho không gian làm việc này.\",\n      save: \"Sử dụng mô hình này\",\n      saving: \"Đang đặt mô hình làm mặc định không gian làm việc...\",\n      missing_credentials: \"Nhà cung cấp này thiếu thông tin đăng nhập!\",\n      missing_credentials_description: \"Nhấp để thiết lập thông tin đăng nhập\",\n    },\n    submit: \"Gửi\",\n    edit_info_user:\n      '\"Gửi\" sẽ tạo lại phản hồi của AI. \"Lưu\" chỉ cập nhật tin nhắn của bạn.',\n    edit_info_assistant:\n      \"Các thay đổi của bạn sẽ được lưu trực tiếp vào phản hồi này.\",\n    see_less: \"Xem ít hơn\",\n    see_more: \"Xem thêm\",\n    tools: \"Dụng cụ\",\n    browse: \"Duyệt\",\n    text_size_label: \"Kích thước văn bản\",\n    select_model: \"Chọn mẫu\",\n    sources: \"Nguồn\",\n    document: \"Tài liệu\",\n    similarity_match: \"trận đấu\",\n    source_count_one: \"{{count}} tham khảo\",\n    source_count_other: \"{{count}} – Tham khảo\",\n    preset_exit_description: \"Dừng lại phiên làm việc hiện tại\",\n    add_new: \"Thêm mới\",\n    edit: \"Chỉnh sửa\",\n    publish: \"Đăng tải\",\n    stop_generating: \"Dừng tạo ra phản hồi\",\n    pause_tts_speech_message: \"Tạm dừng phát giọng đọc của tin nhắn\",\n    slash_commands: \"Lệnh tắt/bật\",\n    agent_skills: \"Kỹ năng của đại lý\",\n    manage_agent_skills: \"Quản lý kỹ năng của đại lý\",\n    agent_skills_disabled_in_session:\n      \"Không thể thay đổi kỹ năng trong khi đang tham gia phiên làm việc. Trước tiên, hãy sử dụng lệnh /exit để kết thúc phiên làm việc.\",\n    start_agent_session: \"Bắt đầu phiên làm việc với đại lý\",\n    use_agent_session_to_use_tools:\n      \"Bạn có thể sử dụng các công cụ trong cuộc trò chuyện bằng cách bắt đầu một phiên với trợ lý bằng cách sử dụng '@agent' ở đầu yêu cầu của bạn.\",\n  },\n  profile_settings: {\n    edit_account: \"Chỉnh sửa Tài khoản\",\n    profile_picture: \"Ảnh Hồ sơ\",\n    remove_profile_picture: \"Xóa Ảnh Hồ sơ\",\n    username: \"Tên người dùng\",\n    new_password: \"Mật khẩu Mới\",\n    password_description: \"Mật khẩu phải có ít nhất 8 ký tự\",\n    cancel: \"Hủy\",\n    update_account: \"Cập nhật Tài khoản\",\n    theme: \"Tùy chọn Giao diện\",\n    language: \"Ngôn ngữ ưa thích\",\n    failed_upload: \"Không thể tải lên ảnh hồ sơ: {{error}}\",\n    upload_success: \"Đã tải lên ảnh hồ sơ.\",\n    failed_remove: \"Không thể xóa ảnh hồ sơ: {{error}}\",\n    profile_updated: \"Hồ sơ đã được cập nhật.\",\n    failed_update_user: \"Không thể cập nhật người dùng: {{error}}\",\n    account: \"Tài khoản\",\n    support: \"Hỗ trợ\",\n    signout: \"Đăng xuất\",\n  },\n  customization: {\n    interface: {\n      title: \"Tùy chọn Giao diện\",\n      description: \"Đặt tùy chọn giao diện của bạn cho AnythingLLM.\",\n    },\n    branding: {\n      title: \"Thương hiệu & Nhãn trắng\",\n      description:\n        \"Nhãn trắng phiên bản AnythingLLM của bạn với thương hiệu tùy chỉnh.\",\n    },\n    chat: {\n      title: \"Trò chuyện\",\n      description: \"Đặt tùy chọn trò chuyện của bạn cho AnythingLLM.\",\n      auto_submit: {\n        title: \"Tự động Gửi Đầu vào Giọng nói\",\n        description:\n          \"Tự động gửi đầu vào giọng nói sau một khoảng thời gian im lặng\",\n      },\n      auto_speak: {\n        title: \"Tự động Đọc Phản hồi\",\n        description: \"Tự động đọc phản hồi từ AI\",\n      },\n      spellcheck: {\n        title: \"Bật Kiểm tra Chính tả\",\n        description:\n          \"Bật hoặc tắt kiểm tra chính tả trong trường nhập trò chuyện\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"Giao diện\",\n        description: \"Chọn giao diện màu ưa thích của bạn cho ứng dụng.\",\n      },\n      \"show-scrollbar\": {\n        title: \"Hiện Thanh cuộn\",\n        description: \"Bật hoặc tắt thanh cuộn trong cửa sổ trò chuyện.\",\n      },\n      \"support-email\": {\n        title: \"Email Hỗ trợ\",\n        description:\n          \"Đặt địa chỉ email hỗ trợ mà người dùng có thể truy cập khi họ cần trợ giúp.\",\n      },\n      \"app-name\": {\n        title: \"Tên\",\n        description:\n          \"Đặt tên được hiển thị trên trang đăng nhập cho tất cả người dùng.\",\n      },\n      \"display-language\": {\n        title: \"Ngôn ngữ Hiển thị\",\n        description:\n          \"Chọn ngôn ngữ ưa thích để hiển thị giao diện người dùng của AnythingLLM - khi bản dịch có sẵn.\",\n      },\n      logo: {\n        title: \"Logo Thương hiệu\",\n        description:\n          \"Tải lên logo tùy chỉnh của bạn để hiển thị trên tất cả các trang.\",\n        add: \"Thêm logo tùy chỉnh\",\n        recommended: \"Kích thước khuyến nghị: 800 x 200\",\n        remove: \"Xóa\",\n        replace: \"Thay thế\",\n      },\n      \"browser-appearance\": {\n        title: \"Giao diện Trình duyệt\",\n        description:\n          \"Tùy chỉnh giao diện của tab trình duyệt và tiêu đề khi ứng dụng đang mở.\",\n        tab: {\n          title: \"Tiêu đề\",\n          description:\n            \"Đặt tiêu đề tab tùy chỉnh khi ứng dụng đang mở trong trình duyệt.\",\n        },\n        favicon: {\n          title: \"Favicon\",\n          description: \"Sử dụng favicon tùy chỉnh cho tab trình duyệt.\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"Mục Chân trang Thanh bên\",\n        description: \"Tùy chỉnh các mục chân trang hiển thị ở cuối thanh bên.\",\n        icon: \"Biểu tượng\",\n        link: \"Liên kết\",\n      },\n      \"render-html\": {\n        title: \"Hiển thị HTML trong trò chuyện\",\n        description:\n          \"Hiển thị phản hồi HTML trong các phản hồi của trợ lý.\\nĐiều này có thể mang lại chất lượng phản hồi cao hơn nhiều, nhưng cũng có thể dẫn đến các rủi ro bảo mật tiềm ẩn.\",\n      },\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"Tạo một đại lý\",\n      editWorkspace: \"Chỉnh sửa không gian làm việc\",\n      uploadDocument: \"Tải lên một tài liệu\",\n    },\n    greeting: \"Hôm nay tôi có thể giúp gì cho bạn?\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"Phím tắt\",\n    shortcuts: {\n      settings: \"Mở Cài đặt\",\n      workspaceSettings: \"Mở Cài đặt Không gian làm việc Hiện tại\",\n      home: \"Đi đến Trang chủ\",\n      workspaces: \"Quản lý Không gian làm việc\",\n      apiKeys: \"Cài đặt Khóa API\",\n      llmPreferences: \"Tùy chọn LLM\",\n      chatSettings: \"Cài đặt Trò chuyện\",\n      help: \"Hiện trợ giúp phím tắt\",\n      showLLMSelector: \"Hiện Bộ chọn LLM không gian làm việc\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"Thành công!\",\n        success_description:\n          \"System Prompt của bạn đã được đăng lên Community Hub!\",\n        success_thank_you: \"Cảm ơn bạn đã chia sẻ với Cộng đồng!\",\n        view_on_hub: \"Xem trên Community Hub\",\n        modal_title: \"Đăng System Prompt\",\n        name_label: \"Tên\",\n        name_description: \"Đây là tên hiển thị của system prompt của bạn.\",\n        name_placeholder: \"System Prompt của tôi\",\n        description_label: \"Mô tả\",\n        description_description:\n          \"Đây là mô tả của system prompt của bạn. Sử dụng điều này để mô tả mục đích của system prompt của bạn.\",\n        tags_label: \"Thẻ\",\n        tags_description:\n          \"Thẻ được sử dụng để gắn nhãn system prompt của bạn để dễ tìm kiếm hơn. Bạn có thể thêm nhiều thẻ. Tối đa 5 thẻ. Tối đa 20 ký tự mỗi thẻ.\",\n        tags_placeholder: \"Nhập và nhấn Enter để thêm thẻ\",\n        visibility_label: \"Hiển thị\",\n        public_description:\n          \"System prompt công khai hiển thị cho tất cả mọi người.\",\n        private_description: \"System prompt riêng tư chỉ hiển thị cho bạn.\",\n        publish_button: \"Đăng lên Community Hub\",\n        submitting: \"Đang đăng...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Đây là system prompt thực tế sẽ được sử dụng để hướng dẫn LLM.\",\n        prompt_placeholder: \"Nhập system prompt của bạn ở đây...\",\n      },\n      agent_flow: {\n        success_title: \"Thành công!\",\n        success_description:\n          \"Luồng Agent của bạn đã được đăng lên Community Hub!\",\n        success_thank_you: \"Cảm ơn bạn đã chia sẻ với Cộng đồng!\",\n        view_on_hub: \"Xem trên Community Hub\",\n        modal_title: \"Đăng Luồng Agent\",\n        name_label: \"Tên\",\n        name_description: \"Đây là tên hiển thị của luồng agent của bạn.\",\n        name_placeholder: \"Luồng Agent của tôi\",\n        description_label: \"Mô tả\",\n        description_description:\n          \"Đây là mô tả của luồng agent của bạn. Sử dụng điều này để mô tả mục đích của luồng agent của bạn.\",\n        tags_label: \"Thẻ\",\n        tags_description:\n          \"Thẻ được sử dụng để gắn nhãn luồng agent của bạn để dễ tìm kiếm hơn. Bạn có thể thêm nhiều thẻ. Tối đa 5 thẻ. Tối đa 20 ký tự mỗi thẻ.\",\n        tags_placeholder: \"Nhập và nhấn Enter để thêm thẻ\",\n        visibility_label: \"Hiển thị\",\n        submitting: \"Đang đăng...\",\n        submit: \"Đăng lên Community Hub\",\n        privacy_note:\n          \"Luồng agent luôn được tải lên dưới dạng riêng tư để bảo vệ bất kỳ dữ liệu nhạy cảm nào. Bạn có thể thay đổi khả năng hiển thị trong Community Hub sau khi đăng. Vui lòng xác minh luồng của bạn không chứa bất kỳ thông tin nhạy cảm hoặc riêng tư nào trước khi đăng.\",\n      },\n      slash_command: {\n        success_title: \"Thành công!\",\n        success_description:\n          \"Lệnh Gạch chéo của bạn đã được đăng lên Community Hub!\",\n        success_thank_you: \"Cảm ơn bạn đã chia sẻ với Cộng đồng!\",\n        view_on_hub: \"Xem trên Community Hub\",\n        modal_title: \"Đăng Lệnh Gạch chéo\",\n        name_label: \"Tên\",\n        name_description: \"Đây là tên hiển thị của lệnh gạch chéo của bạn.\",\n        name_placeholder: \"Lệnh Gạch chéo của tôi\",\n        description_label: \"Mô tả\",\n        description_description:\n          \"Đây là mô tả của lệnh gạch chéo của bạn. Sử dụng điều này để mô tả mục đích của lệnh gạch chéo của bạn.\",\n        tags_label: \"Thẻ\",\n        tags_description:\n          \"Thẻ được sử dụng để gắn nhãn lệnh gạch chéo của bạn để dễ tìm kiếm hơn. Bạn có thể thêm nhiều thẻ. Tối đa 5 thẻ. Tối đa 20 ký tự mỗi thẻ.\",\n        tags_placeholder: \"Nhập và nhấn Enter để thêm thẻ\",\n        visibility_label: \"Hiển thị\",\n        public_description:\n          \"Lệnh gạch chéo công khai hiển thị cho tất cả mọi người.\",\n        private_description: \"Lệnh gạch chéo riêng tư chỉ hiển thị cho bạn.\",\n        publish_button: \"Đăng lên Community Hub\",\n        submitting: \"Đang đăng...\",\n        prompt_label: \"Prompt\",\n        prompt_description:\n          \"Đây là prompt sẽ được sử dụng khi lệnh gạch chéo được kích hoạt.\",\n        prompt_placeholder: \"Nhập prompt của bạn ở đây...\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"Yêu cầu Xác thực\",\n          description:\n            \"Bạn cần xác thực với AnythingLLM Community Hub trước khi đăng các mục.\",\n          button: \"Kết nối với Community Hub\",\n        },\n      },\n    },\n  },\n  security: {\n    title: \"Bảo mật\",\n    multiuser: {\n      title: \"Chế độ Đa Người dùng\",\n      description:\n        \"Thiết lập phiên bản của bạn để hỗ trợ nhóm bằng cách kích hoạt Chế độ Đa Người dùng.\",\n      enable: {\n        \"is-enable\": \"Chế độ Đa Người dùng đã Được Bật\",\n        enable: \"Bật Chế độ Đa Người dùng\",\n        description:\n          \"Theo mặc định, bạn sẽ là quản trị viên duy nhất. Với tư cách quản trị viên, bạn sẽ cần tạo tài khoản cho tất cả người dùng hoặc quản trị viên mới. Không được mất mật khẩu vì chỉ người dùng Quản trị viên mới có thể đặt lại mật khẩu.\",\n        username: \"Tên người dùng tài khoản Quản trị viên\",\n        password: \"Mật khẩu tài khoản Quản trị viên\",\n      },\n    },\n    password: {\n      title: \"Bảo vệ Mật khẩu\",\n      description:\n        \"Bảo vệ phiên bản AnythingLLM của bạn bằng mật khẩu. Nếu bạn quên mật khẩu này, không có phương pháp khôi phục nên hãy đảm bảo lưu mật khẩu này.\",\n      \"password-label\": \"Mật khẩu của phiên bản\",\n    },\n  },\n  home: {\n    welcome: \"Chào mừng bạn\",\n    chooseWorkspace: \"Chọn một khu vực làm việc để bắt đầu trò chuyện!\",\n    notAssigned:\n      \"Bạn hiện không được giao việc nào.\\nLiên hệ với quản trị viên của bạn để yêu cầu truy cập vào khu vực làm việc.\",\n    goToWorkspace: 'Chuyển đến khu vực làm việc \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/zh/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"欢迎使用\",\n      getStarted: \"开始\",\n    },\n    llm: {\n      title: \"LLM 偏好\",\n      description:\n        \"AnythingLLM 可以与多家 LLM 提供商合作。这将是处理聊天的服务。\",\n    },\n    userSetup: {\n      title: \"用户设置\",\n      description: \"配置你的用户设置。\",\n      howManyUsers: \"将有多少用户使用此实例？\",\n      justMe: \"只有我\",\n      myTeam: \"我的团队\",\n      instancePassword: \"实例密码\",\n      setPassword: \"你想要设置密码吗？\",\n      passwordReq: \"密码必须至少包含 8 个字符。\",\n      passwordWarn: \"保存此密码很重要，因为没有恢复方法。\",\n      adminUsername: \"管理员账户用户名\",\n      adminPassword: \"管理员账户密码\",\n      adminPasswordReq: \"密码必须至少包含 8 个字符。\",\n      teamHint:\n        \"默认情况下，你将是唯一的管理员。完成初始设置后，你可以创建和邀请其他人成为用户或管理员。不要丢失你的密码，因为只有管理员可以重置密码。\",\n    },\n    data: {\n      title: \"数据处理与隐私\",\n      description: \"我们致力于在涉及你的个人数据时提供透明度和控制权。\",\n      settingsHint: \"这些设置可以随时在设置中重新配置。\",\n    },\n    survey: {\n      title: \"欢迎使用 AnythingLLM\",\n      description: \"帮助我们为你的需求打造 AnythingLLM。可选。\",\n      email: \"你的电子邮件是什么？\",\n      useCase: \"你将如何使用 AnythingLLM？\",\n      useCaseWork: \"用于工作\",\n      useCasePersonal: \"用于个人使用\",\n      useCaseOther: \"其他\",\n      comment: \"你是如何听说 AnythingLLM 的？\",\n      commentPlaceholder:\n        \"Reddit，Twitter，GitHub，YouTube 等 - 让我们知道你是如何找到我们的！\",\n      skip: \"跳过调查\",\n      thankYou: \"感谢你的反馈！\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"工作区名称\",\n    user: \"用户\",\n    selection: \"模型选择\",\n    save: \"保存更改\",\n    saving: \"保存中...\",\n    previous: \"上一页\",\n    next: \"下一页\",\n    optional: \"可选\",\n    yes: \"是\",\n    no: \"否\",\n    search: \"搜索\",\n    username_requirements:\n      \"用户名必须为 2-32 个字符，以小写字母开头，只能包含小写字母、数字、下划线、连字符和句点。\",\n    on: \"关于\",\n    none: \"没有\",\n    stopped: \"停止\",\n    loading: \"正在加载…\",\n    refresh: \"重新开始；更新\",\n  },\n  settings: {\n    title: \"设置\",\n    invites: \"邀请\",\n    users: \"用户\",\n    workspaces: \"工作区\",\n    \"workspace-chats\": \"对话历史记录\",\n    customization: \"外观\",\n    interface: \"界面偏好\",\n    branding: \"品牌与白标签化\",\n    chat: \"聊天\",\n    \"api-keys\": \"开发者API\",\n    llm: \"大语言模型（LLM）\",\n    transcription: \"转录模型\",\n    embedder: \"嵌入器（Embedder）\",\n    \"text-splitting\": \"文本分割\",\n    \"voice-speech\": \"语音和讲话\",\n    \"vector-database\": \"向量数据库\",\n    embeds: \"嵌入式对话\",\n    security: \"用户与安全\",\n    \"event-logs\": \"事件日志\",\n    privacy: \"隐私与数据\",\n    \"ai-providers\": \"人工智能提供商\",\n    \"agent-skills\": \"代理技能\",\n    admin: \"管理员\",\n    tools: \"工具\",\n    \"experimental-features\": \"实验功能\",\n    contact: \"联系支持\",\n    \"browser-extension\": \"浏览器扩展\",\n    \"system-prompt-variables\": \"系统提示变量\",\n    \"mobile-app\": \"AnythingLLM 移动版\",\n    \"community-hub\": {\n      title: \"社区中心\",\n      trending: \"探索热门\",\n      \"your-account\": \"您的账户\",\n      \"import-item\": \"进口商品\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"欢迎！\",\n      \"placeholder-username\": \"请输入用户名\",\n      \"placeholder-password\": \"请输入密码\",\n      login: \"登录\",\n      validating: \"正在验证...\",\n      \"forgot-pass\": \"忘记密码\",\n      reset: \"重置\",\n    },\n    \"sign-in\": \"登录你的 {{appName}} 账户\",\n    \"password-reset\": {\n      title: \"重置密码\",\n      description: \"请提供以下必要信息以重置你的密码。\",\n      \"recovery-codes\": \"恢复代码\",\n      \"back-to-login\": \"返回登录\",\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"创建代理\",\n      editWorkspace: \"编辑工作区\",\n      uploadDocument: \"上传文件\",\n    },\n    greeting: \"今天我能帮您什么？\",\n  },\n  \"new-workspace\": {\n    title: \"新工作区\",\n    placeholder: \"我的工作区\",\n  },\n  \"workspaces—settings\": {\n    general: \"通用设置\",\n    chat: \"聊天设置\",\n    vector: \"向量数据库\",\n    members: \"成员\",\n    agent: \"代理配置\",\n  },\n  general: {\n    vector: {\n      title: \"向量数量\",\n      description: \"向量数据库中的总向量数。\",\n    },\n    names: {\n      description: \"这只会更改工作区的显示名称。\",\n    },\n    message: {\n      title: \"建议的聊天消息\",\n      description: \"自定义将向你的工作区用户建议的消息。\",\n      add: \"添加新消息\",\n      save: \"保存消息\",\n      heading: \"向我解释\",\n      body: \"AnythingLLM 的好处\",\n    },\n    delete: {\n      title: \"删除工作区\",\n      description: \"删除此工作区及其所有数据。这将删除所有用户的工作区。\",\n      delete: \"删除工作区\",\n      deleting: \"正在删除工作区...\",\n      \"confirm-start\": \"你即将删除整个\",\n      \"confirm-end\":\n        \"工作区。这将删除矢量数据库中的所有矢量嵌入。\\n\\n原始源文件将保持不变。此操作是不可逆转的。\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"工作区 LLM 提供者\",\n      description:\n        \"将用于此工作区的特定 LLM 提供商和模型。默认情况下，它使用系统 LLM 提供程序和设置。\",\n      search: \"搜索所有 LLM 提供商\",\n    },\n    model: {\n      title: \"工作区聊天模型\",\n      description:\n        \"将用于此工作区的特定聊天模型。如果为空，将使用系统 LLM 首选项。\",\n    },\n    mode: {\n      title: \"聊天模式\",\n      chat: {\n        title: \"聊天\",\n        description:\n          \"将提供答案，利用LLM的通用知识和相关文档的上下文信息。您需要使用 `@agent` 命令来使用工具。\",\n      },\n      query: {\n        title: \"查询\",\n        description:\n          \"将在找到文档上下文时提供答案 <b>仅限</b>。您需要使用 @agent 命令来使用工具。\",\n      },\n      automatic: {\n        title: \"自动\",\n        description:\n          \"如果模型和提供商支持原生工具调用，则会自动使用这些工具。<br />如果不支持原生工具调用，则需要使用 `@agent` 命令来使用工具。\",\n      },\n    },\n    history: {\n      title: \"聊天历史记录\",\n      \"desc-start\": \"将包含在响应的短期记忆中的先前聊天的数量。\",\n      recommend: \"推荐 20。\",\n      \"desc-end\":\n        \"任何超过 45 的值都可能导致连续聊天失败，具体取决于消息大小。\",\n    },\n    prompt: {\n      title: \"系统提示词\",\n      description:\n        \"将在此工作区上使用的提示词。定义 AI 生成响应的上下文和指令。你应该提供精心设计的提示，以便人工智能可以生成相关且准确的响应。\",\n      history: {\n        title: \"系统提示词历史\",\n        clearAll: \"全部清除\",\n        noHistory: \"没有可用的系统提示词历史记录\",\n        restore: \"恢复\",\n        delete: \"删除\",\n        deleteConfirm: \"您确定要删除此历史记录吗？\",\n        clearAllConfirm: \"您确定要清除所有历史记录吗？此操作无法撤消。\",\n        expand: \"展开\",\n        publish: \"发布到社区中心\",\n      },\n    },\n    refusal: {\n      title: \"查询模式拒绝响应\",\n      \"desc-start\": \"当处于\",\n      query: \"查询\",\n      \"desc-end\": \"模式时，当未找到上下文时，你可能希望返回自定义拒绝响应。\",\n      \"tooltip-title\": \"我为什麽会看到这个?\",\n      \"tooltip-description\":\n        \"您处于查询模式，此模式仅使用您文件中的信息。切换到聊天模式以进行更灵活的对话，或点击此处访问我们的文件以了解更多关于聊天模式的信息。\",\n    },\n    temperature: {\n      title: \"LLM 温度\",\n      \"desc-start\": \"此设置控制你的 LLM 回答的“创意”程度\",\n      \"desc-end\":\n        \"数字越高越有创意。对于某些模型，如果设置得太高，可能会导致响应不一致。\",\n      hint: \"大多数 LLM 都有各种可接受的有效值范围。请咨询你的LLM提供商以获取该信息。\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"向量数据库标识符\",\n    snippets: {\n      title: \"最大上下文片段\",\n      description:\n        \"此设置控制每次聊天或查询将发送到 LLM 的上下文片段的最大数量。\",\n      recommend: \"推荐: 4\",\n    },\n    doc: {\n      title: \"文档相似性阈值\",\n      description:\n        \"源被视为与聊天相关所需的最低相似度分数。数字越高，来源与聊天就越相似。\",\n      zero: \"无限制\",\n      low: \"低（相似度分数 ≥ .25）\",\n      medium: \"中（相似度分数 ≥ .50）\",\n      high: \"高（相似度分数 ≥ .75）\",\n    },\n    reset: {\n      reset: \"重置向量数据库\",\n      resetting: \"清除向量...\",\n      confirm:\n        \"你将重置此工作区的矢量数据库。这将删除当前嵌入的所有矢量嵌入。\\n\\n原始源文件将保持不变。此操作是不可逆转的。\",\n      success: \"向量数据库已重置。\",\n      error: \"无法重置工作区向量数据库！\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"不明确支持工具调用的 LLMs 的性能高度依赖于模型的功能和准确性。有些能力可能受到限制或不起作用。\",\n    provider: {\n      title: \"工作区代理 LLM 提供商\",\n      description: \"将用于此工作区的 @agent 代理的特定 LLM 提供商和模型。\",\n    },\n    mode: {\n      chat: {\n        title: \"工作区代理聊天模型\",\n        description: \"将用于此工作区的 @agent 代理的特定聊天模型。\",\n      },\n      title: \"工作区代理模型\",\n      description: \"将用于此工作区的 @agent 代理的特定 LLM 模型。\",\n      wait: \"-- 等待模型 --\",\n    },\n    skill: {\n      rag: {\n        title: \"检索增强生成和长期记忆\",\n        description:\n          '允许代理利用你的本地文档来回答查询，或要求代理\"记住\"长期记忆检索的内容片段。',\n      },\n      view: {\n        title: \"查看和总结文档\",\n        description: \"允许代理列出和总结当前嵌入的工作区文件的内容。\",\n      },\n      scrape: {\n        title: \"抓取网站\",\n        description: \"允许代理访问和抓取网站的内容。\",\n      },\n      generate: {\n        title: \"生成图表\",\n        description: \"使默认代理能够从提供的数据或聊天中生成各种类型的图表。\",\n      },\n      save: {\n        title: \"生成并保存文件到浏览器\",\n        description:\n          \"使默认代理能够生成并写入文件，这些文件可以保存并在你的浏览器中下载。\",\n      },\n      web: {\n        title: \"实时网络搜索和浏览\",\n        description:\n          \"通过连接到搜索引擎（SERP）提供商，让您的代理能够搜索互联网来回答您的问题。\",\n      },\n      sql: {\n        title: \"SQL 连接器\",\n        description:\n          \"让您的代理能够利用 SQL 来回答您的问题，只需连接到各种 SQL 数据库提供商即可。\",\n      },\n      default_skill:\n        \"默认情况下，这项技能已启用。但是，如果您不想让该技能被代理使用，您可以将其禁用。\",\n    },\n    mcp: {\n      title: \"MCP 服务器\",\n      \"loading-from-config\": \"从配置文件加载 MCP 服务器\",\n      \"learn-more\": \"了解更多关于 MCP 服务器的信息。\",\n      \"no-servers-found\": \"未找到任何 MCP 服务器\",\n      \"tool-warning\": \"为了获得最佳性能，建议禁用不必要的工具，以节省上下文。\",\n      \"stop-server\": \"停止 MCP 服务器\",\n      \"start-server\": \"启动 MCP 服务器\",\n      \"delete-server\": \"删除 MCP 服务器\",\n      \"tool-count-warning\":\n        \"这个 MCP 服务器启用了 <b> 工具，这些工具会在每次聊天中使用上下文信息。</b> 建议禁用不需要的工具，以节省上下文。<br />\",\n      \"startup-command\": \"启动命令\",\n      command: \"命令\",\n      arguments: \"争论\",\n      \"not-running-warning\":\n        \"这个 MCP 服务器目前处于停止状态，可能是因为在启动时出现了错误或被手动停止。\",\n      \"tool-call-arguments\": \"工具调用的参数\",\n      \"tools-enabled\": \"工具已启用\",\n    },\n    settings: {\n      title: \"代理技能设置\",\n      \"max-tool-calls\": {\n        title: \"每个回复的最大请求次数\",\n        description:\n          \"单个代理可以使用的最大工具数量，用于生成单个响应。 这样可以防止工具调用数量过多，从而避免无限循环。\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"智能技能选择\",\n        \"beta-badge\": \"β 版本\",\n        description:\n          \"实现无限工具和按查询减少高达 80% 的 Token 使用量——AnythingLLM 能够自动选择最合适的技能，以应对每个提示。\",\n        \"max-tools\": {\n          title: \"麦克斯工具\",\n          description:\n            \"可以选取的工具的最大数量，用于每个查询。我们建议将此值设置为较高的值，以便在处理大型上下文模型时。\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"工作区聊天历史记录\",\n    description: \"这些是用户发送的所有聊天记录和消息，按创建日期排序。\",\n    export: \"导出\",\n    table: {\n      id: \"编号\",\n      by: \"发送者\",\n      workspace: \"工作区\",\n      prompt: \"提示词\",\n      response: \"响应\",\n      at: \"发送时间\",\n    },\n  },\n  customization: {\n    interface: {\n      title: \"界面偏好设置\",\n      description: \"设置您的 AnythingLLM 界面偏好。\",\n    },\n    branding: {\n      title: \"品牌与白标设置\",\n      description: \"使用自定义品牌对白标您的 AnythingLLM 实例。\",\n    },\n    chat: {\n      title: \"聊天\",\n      description: \"设置您的 AnythingLLM 聊天偏好。\",\n      auto_submit: {\n        title: \"自动提交语音输入\",\n        description: \"在静音一段时间后自动提交语音输入\",\n      },\n      auto_speak: {\n        title: \"自动语音回复\",\n        description: \"自动朗读 AI 的回复内容\",\n      },\n      spellcheck: {\n        title: \"启用拼写检查\",\n        description: \"在聊天输入框中启用或禁用拼写检查\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"主题\",\n        description: \"选择您偏好的应用配色主题。\",\n      },\n      \"show-scrollbar\": {\n        title: \"显示滚动条\",\n        description: \"启用或禁用聊天窗口中的滚动条。\",\n      },\n      \"support-email\": {\n        title: \"客服邮箱\",\n        description: \"设置用户在需要帮助时可联系的客服邮箱地址。\",\n      },\n      \"app-name\": {\n        title: \"名称\",\n        description: \"设置所有用户在登录页面看到的名称。\",\n      },\n      \"display-language\": {\n        title: \"显示语言\",\n        description: \"选择显示 AnythingLLM 界面所用的语言（若有翻译可用）。\",\n      },\n      logo: {\n        title: \"品牌标志\",\n        description: \"上传您的自定义标志以在所有页面展示。\",\n        add: \"添加自定义标志\",\n        recommended: \"推荐尺寸：800 x 200\",\n        remove: \"移除\",\n        replace: \"替换\",\n      },\n      \"browser-appearance\": {\n        title: \"浏览器外观\",\n        description: \"自定义应用打开时浏览器标签和标题的外观。\",\n        tab: {\n          title: \"标题\",\n          description: \"设置应用在浏览器中打开时的自定义标签标题。\",\n        },\n        favicon: {\n          title: \"网站图标\",\n          description: \"为浏览器标签使用自定义网站图标。\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"侧边栏底部项目\",\n        description: \"自定义显示在侧边栏底部的项目。\",\n        icon: \"图标\",\n        link: \"链接\",\n      },\n      \"render-html\": {\n        title: \"在聊天中渲染 HTML\",\n        description:\n          \"在助手回复中呈现 HTML 响应。\\n这可以显著提高回复的质量，但也可能带来潜在的安全风险。\",\n      },\n    },\n  },\n  api: {\n    title: \"API 密钥\",\n    description: \"API 密钥允许持有者以编程方式访问和管理此 AnythingLLM 实例。\",\n    link: \"阅读 API 文档\",\n    generate: \"生成新的 API 密钥\",\n    table: {\n      key: \"API 密钥\",\n      by: \"创建者\",\n      created: \"创建时间\",\n    },\n  },\n  llm: {\n    title: \"LLM 首选项\",\n    description:\n      \"这些是你首选的 LLM 聊天和嵌入提供商的凭据和设置。重要的是，确保这些密钥是最新的和正确的，否则 AnythingLLM 将无法正常运行。\",\n    provider: \"LLM 提供商\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Azure 服务端点\",\n        api_key: \"API 密钥\",\n        chat_deployment_name: \"聊天部署名称\",\n        chat_model_token_limit: \"聊天模型令牌限制\",\n        model_type: \"模型类型\",\n        default: \"预设\",\n        reasoning: \"推理\",\n        model_type_tooltip:\n          \"如果您的部署使用了推理模型（例如 o1、o1-mini、o3-mini 等），请将此选项设置为“推理”。否则，您的聊天请求可能会失败。\",\n      },\n    },\n  },\n  transcription: {\n    title: \"转录模型首选项\",\n    description:\n      \"这些是你的首选转录模型提供商的凭据和设置。重要的是这些密钥是最新且正确的，否则媒体文件和音频将无法转录。\",\n    provider: \"转录提供商\",\n    \"warn-start\":\n      \"在 RAM 或 CPU 有限的计算机上使用本地耳语模型可能会在处理媒体文件时停止 AnythingLLM。\",\n    \"warn-recommend\": \"我们建议至少 2GB RAM 并上传 <10Mb 的文件。\",\n    \"warn-end\": \"内置模型将在首次使用时自动下载。\",\n  },\n  embedding: {\n    title: \"嵌入首选项\",\n    \"desc-start\":\n      \"当使用本身不支持嵌入引擎的 LLM 时，你可能需要额外指定用于嵌入文本的凭据。\",\n    \"desc-end\":\n      \"嵌入是将文本转换为矢量的过程。需要这些凭据才能将你的文件和提示转换为 AnythingLLM 可以用来处理的格式。\",\n    provider: {\n      title: \"嵌入引擎提供商\",\n    },\n  },\n  text: {\n    title: \"文本拆分和分块首选项\",\n    \"desc-start\":\n      \"有时，你可能希望更改新文档在插入到矢量数据库之前拆分和分块的默认方式。\",\n    \"desc-end\": \"只有在了解文本拆分的工作原理及其副作用时，才应修改此设置。\",\n    size: {\n      title: \"文本块大小\",\n      description: \"这是单个向量中可以存在的字符的最大长度。\",\n      recommend: \"嵌入模型的最大长度为\",\n    },\n    overlap: {\n      title: \"文本块重叠\",\n      description: \"这是在两个相邻文本块之间分块期间发生的最大字符重叠。\",\n    },\n  },\n  vector: {\n    title: \"向量数据库\",\n    description:\n      \"这些是 AnythingLLM 实例如何运行的凭据和设置。重要的是，这些密钥是最新的和正确的。\",\n    provider: {\n      title: \"向量数据库提供商\",\n      description: \"LanceDB 不需要任何配置。\",\n    },\n  },\n  embeddable: {\n    title: \"可嵌入的聊天小部件\",\n    description:\n      \"可嵌入的聊天小部件是与单个工作区绑定的面向公众的聊天界面。这些允许你构建工作区，然后你可以将其发布到全世界。\",\n    create: \"创建嵌入式对话\",\n    table: {\n      workspace: \"工作区\",\n      chats: \"已发送聊天\",\n      active: \"活动域\",\n      created: \"建立\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"嵌入的聊天历史纪录\",\n    export: \"导出\",\n    description: \"这些是你发布的任何嵌入的所有记录的聊天和消息。\",\n    table: {\n      embed: \"嵌入\",\n      sender: \"发送者\",\n      message: \"消息\",\n      response: \"响应\",\n      at: \"发送时间\",\n    },\n  },\n  event: {\n    title: \"事件日志\",\n    description: \"查看此实例上发生的所有操作和事件以进行监控。\",\n    clear: \"清除事件日志\",\n    table: {\n      type: \"事件类型\",\n      user: \"用户\",\n      occurred: \"发生时间\",\n    },\n  },\n  privacy: {\n    title: \"隐私和数据处理\",\n    description:\n      \"这是你对如何处理连接的第三方提供商和AnythingLLM的数据的配置。\",\n    anonymous: \"启用匿名遥测\",\n  },\n  connectors: {\n    \"search-placeholder\": \"搜索数据连接器\",\n    \"no-connectors\": \"未找到数据连接器。\",\n    github: {\n      name: \"GitHub 仓库\",\n      description: \"一键导入整个公共或私有的 GitHub 仓库。\",\n      URL: \"GitHub 仓库链接\",\n      URL_explained: \"您希望收集的 GitHub 仓库链接。\",\n      token: \"GitHub 访问令牌\",\n      optional: \"可选\",\n      token_explained: \"用于避免速率限制的访问令牌。\",\n      token_explained_start: \"如果没有 \",\n      token_explained_link1: \"个人访问令牌\",\n      token_explained_middle:\n        \"，由于 GitHub API 的速率限制，可能无法收集所有文件。您可以 \",\n      token_explained_link2: \"创建临时访问令牌\",\n      token_explained_end: \" 来避免此问题。\",\n      ignores: \"文件忽略列表\",\n      git_ignore:\n        \".gitignore 格式的列表，用于在收集过程中忽略特定文件。输入后按回车保存每一项。\",\n      task_explained: \"完成后，所有文件将可用于在文档选择器中嵌入至工作区。\",\n      branch: \"您希望收集文件的分支。\",\n      branch_loading: \"-- 正在加载可用分支 --\",\n      branch_explained: \"您希望收集文件的分支。\",\n      token_information:\n        \"如果未填写 <b>GitHub 访问令牌</b>，由于 GitHub 的公共 API 限制，此数据连接器将只能收集仓库的 <b>顶层</b> 文件。\",\n      token_personal: \"在此处使用 GitHub 账户获取免费的个人访问令牌。\",\n    },\n    gitlab: {\n      name: \"GitLab 仓库\",\n      description: \"一键导入整个公共或私有的 GitLab 仓库。\",\n      URL: \"GitLab 仓库链接\",\n      URL_explained: \"您希望收集的 GitLab 仓库链接。\",\n      token: \"GitLab 访问令牌\",\n      optional: \"可选\",\n      token_description: \"选择要从 GitLab API 获取的额外实体。\",\n      token_explained_start: \"如果没有 \",\n      token_explained_link1: \"个人访问令牌\",\n      token_explained_middle:\n        \"，由于 GitLab API 的速率限制，可能无法收集所有文件。您可以 \",\n      token_explained_link2: \"创建临时访问令牌\",\n      token_explained_end: \" 来避免此问题。\",\n      fetch_issues: \"将问题作为文档获取\",\n      ignores: \"文件忽略列表\",\n      git_ignore:\n        \".gitignore 格式的列表，用于在收集过程中忽略特定文件。输入后按回车保存每一项。\",\n      task_explained: \"完成后，所有文件将可用于在文档选择器中嵌入至工作区。\",\n      branch: \"您希望收集文件的分支\",\n      branch_loading: \"-- 正在加载可用分支 --\",\n      branch_explained: \"您希望收集文件的分支。\",\n      token_information:\n        \"如果未填写 <b>GitLab 访问令牌</b>，由于 GitLab 的公共 API 限制，此数据连接器将只能收集仓库的 <b>顶层</b> 文件。\",\n      token_personal: \"在此处使用 GitLab 账户获取免费的个人访问令牌。\",\n    },\n    youtube: {\n      name: \"YouTube 字幕\",\n      description: \"通过链接导入整个 YouTube 视频的转录内容。\",\n      URL: \"YouTube 视频链接\",\n      URL_explained_start:\n        \"输入任何 YouTube 视频的链接以获取其转录内容。视频必须启用 \",\n      URL_explained_link: \"隐藏字幕\",\n      URL_explained_end: \" 功能。\",\n      task_explained: \"完成后，转录内容将可用于在文档选择器中嵌入至工作区。\",\n    },\n    \"website-depth\": {\n      name: \"批量链接爬虫\",\n      description: \"爬取一个网站及其指定深度的子链接。\",\n      URL: \"网站链接\",\n      URL_explained: \"您希望爬取的网站链接。\",\n      depth: \"爬取深度\",\n      depth_explained: \"这是爬虫从起始链接向下跟踪的子链接层级数量。\",\n      max_pages: \"最大页面数\",\n      max_pages_explained: \"要爬取的最大链接数。\",\n      task_explained:\n        \"完成后，所有抓取的内容将可用于在文档选择器中嵌入至工作区。\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"一键导入整个 Confluence 页面。\",\n      deployment_type: \"Confluence 部署类型\",\n      deployment_type_explained:\n        \"判断您的 Confluence 实例是部署在 Atlassian 云端还是自托管。\",\n      base_url: \"Confluence 基础链接\",\n      base_url_explained: \"这是您 Confluence 空间的基础链接。\",\n      space_key: \"Confluence 空间标识\",\n      space_key_explained:\n        \"您将使用的 Confluence 实例空间标识，通常以 ~ 开头。\",\n      username: \"Confluence 用户名\",\n      username_explained: \"您的 Confluence 用户名\",\n      auth_type: \"Confluence 认证方式\",\n      auth_type_explained: \"选择您希望用于访问 Confluence 页面内容的认证方式。\",\n      auth_type_username: \"用户名和访问令牌\",\n      auth_type_personal: \"个人访问令牌\",\n      token: \"Confluence 访问令牌\",\n      token_explained_start:\n        \"您需要提供访问令牌用于认证。您可以在此生成访问令牌\",\n      token_explained_link: \"此处\",\n      token_desc: \"用于认证的访问令牌\",\n      pat_token: \"Confluence 个人访问令牌\",\n      pat_token_explained: \"您的 Confluence 个人访问令牌。\",\n      task_explained: \"完成后，页面内容将可用于在文档选择器中嵌入至工作区。\",\n      bypass_ssl: \"绕过 SSL 证书验证\",\n      bypass_ssl_explained:\n        \"启用此选项以绕过对自托管 Confluence 实例的 SSL 证书验证，特别是使用自签名证书的情况。\",\n    },\n    manage: {\n      documents: \"文档\",\n      \"data-connectors\": \"数据连接器\",\n      \"desktop-only\":\n        \"这些设置只能在桌面设备上编辑。请使用桌面访问此页面以继续操作。\",\n      dismiss: \"关闭\",\n      editing: \"正在编辑\",\n    },\n    directory: {\n      \"my-documents\": \"我的文档\",\n      \"new-folder\": \"新建文件夹\",\n      \"search-document\": \"搜索文档\",\n      \"no-documents\": \"暂无文档\",\n      \"move-workspace\": \"移动到工作区\",\n      \"delete-confirmation\":\n        \"您确定要删除这些文件和文件夹吗？\\n这将从系统中移除这些文件，并自动将其从所有关联工作区中移除。\\n此操作无法撤销。\",\n      \"removing-message\":\n        \"正在删除 {{count}} 个文档和 {{folderCount}} 个文件夹，请稍候。\",\n      \"move-success\": \"成功移动了 {{count}} 个文档。\",\n      no_docs: \"暂无文档\",\n      select_all: \"全选\",\n      deselect_all: \"取消全选\",\n      remove_selected: \"移除所选\",\n      costs: \"*嵌入时一次性费用\",\n      save_embed: \"保存并嵌入\",\n      \"total-documents_one\": \"{{count}} 文件\",\n      \"total-documents_other\": \"{{count}} 类型的文件\",\n    },\n    upload: {\n      \"processor-offline\": \"文档处理器不可用\",\n      \"processor-offline-desc\":\n        \"当前文档处理器离线，无法上传文件。请稍后再试。\",\n      \"click-upload\": \"点击上传或拖放文件\",\n      \"file-types\": \"支持文本文件、CSV、电子表格、音频文件等！\",\n      \"or-submit-link\": \"或提交链接\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"正在获取...\",\n      \"fetch-website\": \"获取网站\",\n      \"privacy-notice\":\n        \"这些文件将被上传到此 AnythingLLM 实例上的文档处理器。这些文件不会发送或共享给第三方。\",\n    },\n    pinning: {\n      what_pinning: \"什么是文档固定？\",\n      pin_explained_block1:\n        \"当您在 AnythingLLM 中<b>固定</b>一个文档时，我们会将整个文档内容注入到您的提示窗口中，让 LLM 能够完全理解它。\",\n      pin_explained_block2:\n        \"这在 <b>大上下文模型</b> 或关键的小文件中效果最佳。\",\n      pin_explained_block3:\n        \"如果默认情况下无法从 AnythingLLM 获取满意的答案，固定文档是提高答案质量的好方法。\",\n      accept: \"好的，知道了\",\n    },\n    watching: {\n      what_watching: \"什么是监控文档？\",\n      watch_explained_block1:\n        \"当您在 AnythingLLM 中<b>监控</b>一个文档时，我们会<i>自动</i>按定期间隔从其原始来源同步文档内容。系统会自动更新在所有使用该文档的工作区中的内容。\",\n      watch_explained_block2:\n        \"此功能当前仅支持在线内容，不适用于手动上传的文档。\",\n      watch_explained_block3_start: \"您可以在 \",\n      watch_explained_block3_link: \"文件管理器\",\n      watch_explained_block3_end: \" 管理视图中管理被监控的文档。\",\n      accept: \"好的，知道了\",\n    },\n    obsidian: {\n      vault_location: \"仓库位置\",\n      vault_description:\n        \"选择你的 Obsidian 仓库文件夹，以导入所有笔记及其关联。\",\n      selected_files: \"找到 {{count}} 个 Markdown 文件\",\n      importing: \"正在导入保险库…\",\n      import_vault: \"导入保险库\",\n      processing_time: \"根据你的仓库大小，这可能需要一些时间。\",\n      vault_warning: \"为避免冲突，请确保你的 Obsidian 仓库当前未被打开。\",\n    },\n  },\n  chat_window: {\n    send_message: \"发送消息\",\n    attach_file: \"向此对话附加文件\",\n    text_size: \"更改文字大小。\",\n    microphone: \"语音输入你的提示。\",\n    send: \"将提示消息发送到工作区\",\n    attachments_processing: \"附件正在处理，请稍候……\",\n    tts_speak_message: \"TTS 播报消息\",\n    copy: \"复制\",\n    regenerate: \"重新\",\n    regenerate_response: \"重新回应\",\n    good_response: \"反应良好\",\n    more_actions: \"更多操作\",\n    fork: \"分叉\",\n    delete: \"删除\",\n    cancel: \"取消\",\n    edit_prompt: \"编辑问题\",\n    edit_response: \"编辑回应\",\n    preset_reset_description: \"清除聊天纪录并开始新的聊天\",\n    add_new_preset: \"新增预设\",\n    command: \"指令\",\n    your_command: \"你的指令\",\n    placeholder_prompt: \"提示范例\",\n    description: \"描述\",\n    placeholder_description: \"描述范例\",\n    save: \"保存\",\n    small: \"小\",\n    normal: \"一般\",\n    large: \"大\",\n    workspace_llm_manager: {\n      search: \"搜索\",\n      loading_workspace_settings: \"正在载入工作区设置\",\n      available_models: \"可用模型\",\n      available_models_description: \"可用模型说明\",\n      save: \"保存\",\n      saving: \"正在保存\",\n      missing_credentials: \"缺少凭证\",\n      missing_credentials_description: \"缺少凭证说明\",\n    },\n    submit: \"提交\",\n    edit_info_user: \"“提交”会重新生成 AI 的回复。 “保存”只会更新您的消息。\",\n    edit_info_assistant: \"您所做的修改将直接保存到此处。\",\n    see_less: \"查看更多\",\n    see_more: \"查看更多\",\n    tools: \"工具\",\n    browse: \"浏览\",\n    text_size_label: \"字体大小\",\n    select_model: \"选择型号\",\n    sources: \"来源\",\n    document: \"文件\",\n    similarity_match: \"比赛\",\n    source_count_one: \"{{count}} 参考\",\n    source_count_other: \"{{count}} 相关资料\",\n    preset_exit_description: \"停止当前的代理会话\",\n    add_new: \"添加新\",\n    edit: \"编辑\",\n    publish: \"出版\",\n    stop_generating: \"停止生成回复\",\n    pause_tts_speech_message: \"暂停消息的语音合成（TTS）功能\",\n    slash_commands: \"快捷命令\",\n    agent_skills: \"代理人技能\",\n    manage_agent_skills: \"管理代理人技能\",\n    agent_skills_disabled_in_session:\n      \"在活动会话期间，无法修改技能。首先使用 /exit 命令结束会话。\",\n    start_agent_session: \"开始代理会\",\n    use_agent_session_to_use_tools:\n      \"您可以通过在提示词的开头使用'@agent'来启动与代理的聊天，从而使用聊天工具。\",\n  },\n  profile_settings: {\n    edit_account: \"编辑帐户\",\n    profile_picture: \"头像\",\n    remove_profile_picture: \"移除头像\",\n    username: \"用户名\",\n    new_password: \"新密码\",\n    password_description: \"密码长度必须至少为 8 个字符\",\n    cancel: \"取消\",\n    update_account: \"更新帐号\",\n    theme: \"主题偏好\",\n    language: \"语言偏好\",\n    failed_upload: \"上传个人资料图片失败：{{error}}\",\n    upload_success: \"个人资料图片已上传。\",\n    failed_remove: \"移除个人资料图片失败：{{error}}\",\n    profile_updated: \"个人资料已更新。\",\n    failed_update_user: \"更新使用者失败：{{error}}\",\n    account: \"帐户\",\n    support: \"支援\",\n    signout: \"登出\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"键盘快捷键\",\n    shortcuts: {\n      settings: \"打开设置\",\n      workspaceSettings: \"打开目前工作区设置\",\n      home: \"前往首页\",\n      workspaces: \"管理工作区\",\n      apiKeys: \"API 密钥设定\",\n      llmPreferences: \"LLM 偏好设置\",\n      chatSettings: \"聊天设置\",\n      help: \"显示键盘快捷键说明\",\n      showLLMSelector: \"显示工作区 LLM 选择器\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"成功！\",\n        success_description: \"您的系统提示已发布到社区中心！\",\n        success_thank_you: \"感谢您分享到社群！\",\n        view_on_hub: \"在社区中心查看\",\n        modal_title: \"发布系统提示\",\n        name_label: \"名称\",\n        name_description: \"这是您系统提示的显示名称。\",\n        name_placeholder: \"我的系统提示\",\n        description_label: \"描述\",\n        description_description:\n          \"这是您系统提示的描述。用它来描述您系统提示的目的。\",\n        tags_label: \"标签\",\n        tags_description:\n          \"标签用于标记您的系统提示，以便于搜索。您可以添加多个标签。最多 5 个标签。每个标签最多 20 个字符。\",\n        tags_placeholder: \"输入并按 Enter 键添加标签\",\n        visibility_label: \"可见性\",\n        public_description: \"公共系统提示对所有人可见。\",\n        private_description: \"私人系统提示仅对您可见。\",\n        publish_button: \"发布到社区中心\",\n        submitting: \"发布中...\",\n        prompt_label: \"提示\",\n        prompt_description: \"这是将用于引导 LLM 的实际系统提示。\",\n        prompt_placeholder: \"在此输入您的系统提示...\",\n      },\n      agent_flow: {\n        success_title: \"成功！\",\n        success_description: \"您的代理流程已发布到社区中心！\",\n        success_thank_you: \"感谢您分享到社群！\",\n        view_on_hub: \"在社区中心查看\",\n        modal_title: \"发布代理流程\",\n        name_label: \"名称\",\n        name_description: \"这是您代理流程的显示名称。\",\n        name_placeholder: \"我的代理流程\",\n        description_label: \"描述\",\n        description_description:\n          \"这是您代理流程的描述。用它来描述您代理流程的目的。\",\n        tags_label: \"标签\",\n        tags_description:\n          \"标签用于标记您的代理流程，以便于搜索。您可以添加多个标签。最多 5 个标签。每个标签最多 20 个字符。\",\n        tags_placeholder: \"输入并按 Enter 键添加标签\",\n        visibility_label: \"可见性\",\n        submitting: \"发布中...\",\n        submit: \"发布到社区中心\",\n        privacy_note:\n          \"代理流程始终以上传为私有，以保护任何敏感资料。您可以在发布后在社区中心更改可见性。请在发布前验证您的流程不包含任何敏感或私人信息。\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"需要验证\",\n          description:\n            \"在发布项目之前，您需要通过 AnythingLLM 社区中心进行验证。\",\n          button: \"连接到社区中心\",\n        },\n      },\n      slash_command: {\n        success_title: \"成功！\",\n        success_description: \"您的斜线指令已发布到社区中心！\",\n        success_thank_you: \"感谢您分享到社群！\",\n        view_on_hub: \"在社区中心查看\",\n        modal_title: \"发布斜线指令\",\n        name_label: \"名称\",\n        name_description: \"这是您斜线指令的显示名称。\",\n        name_placeholder: \"我的斜线指令\",\n        description_label: \"描述\",\n        description_description:\n          \"这是您斜线指令的描述。用它来描述您斜线指令的目的。\",\n        tags_label: \"标签\",\n        tags_description:\n          \"标签用于标记您的斜线指令，以便于搜索。您可以添加多个标签。最多 5 个标签。每个标签最多 20 个字符。\",\n        tags_placeholder: \"输入并按 Enter 键添加标签\",\n        visibility_label: \"可见性\",\n        public_description: \"公共斜线指令对所有人可见。\",\n        private_description: \"私人斜线指令仅对您可见。\",\n        publish_button: \"发布到社区中心\",\n        submitting: \"发布中...\",\n        prompt_label: \"提示\",\n        prompt_description: \"这是触发斜线指令时将使用的提示。\",\n        prompt_placeholder: \"在此输入您的提示...\",\n      },\n    },\n  },\n  security: {\n    title: \"用户与安全\",\n    multiuser: {\n      title: \"多用户模式\",\n      description: \"通过激活多用户模式来设置你的实例以支持你的团队。\",\n      enable: {\n        \"is-enable\": \"多用户模式已启用\",\n        enable: \"启用多用户模式\",\n        description:\n          \"默认情况下，你将是唯一的管理员。作为管理员，你需要为所有新用户或管理员创建账户。不要丢失你的密码，因为只有管理员用户可以重置密码。\",\n        username: \"管理员账户用户名\",\n        password: \"管理员账户密码\",\n      },\n    },\n    password: {\n      title: \"密码保护\",\n      description:\n        \"用密码保护你的AnythingLLM实例。如果你忘记了密码，那么没有恢复方法，所以请确保保存这个密码。\",\n      \"password-label\": \"实例密码\",\n    },\n  },\n  home: {\n    welcome: \"欢迎\",\n    chooseWorkspace: \"选择一个工作区开始聊天！\",\n    notAssigned:\n      \"你目前还没有分配到任何工作区。\\n请联系你的管理员请求访问一个工作区。\",\n    goToWorkspace: '前往 \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/locales/zh_TW/common.js",
    "content": "// Anything with \"null\" requires a translation. Contribute to translation via a PR!\nconst TRANSLATIONS = {\n  onboarding: {\n    home: {\n      title: \"歡迎使用\",\n      getStarted: \"開始使用\",\n    },\n    llm: {\n      title: \"LLM 偏好\",\n      description:\n        \"AnythingLLM 可搭配多種 LLM 提供者使用。這項服務會負責處理對話。\",\n    },\n    userSetup: {\n      title: \"使用者設定\",\n      description: \"設定使用者偏好。\",\n      howManyUsers: \"這套系統會有多少位使用者？\",\n      justMe: \"只有我\",\n      myTeam: \"我的團隊\",\n      instancePassword: \"系統密碼\",\n      setPassword: \"要設定密碼嗎？\",\n      passwordReq: \"密碼必須至少包含 8 個字元。\",\n      passwordWarn: \"請務必妥善保存此密碼，因為目前沒有復原方式。\",\n      adminUsername: \"管理員帳號使用者名稱\",\n      adminPassword: \"管理員帳號密碼\",\n      adminPasswordReq: \"密碼必須至少包含 8 個字元。\",\n      teamHint:\n        \"預設只有您擁有管理員權限。完成初始設定後，即可建立帳號並邀請其他人成為使用者或管理員。請勿遺失密碼，因為只有管理員能重設密碼。\",\n    },\n    data: {\n      title: \"資料處理與隱私\",\n      description: \"對於個人資料的處理方式，我們致力於提供透明度與控制權。\",\n      settingsHint: \"這些設定之後都可以在設定頁面重新調整。\",\n    },\n    survey: {\n      title: \"歡迎使用 AnythingLLM\",\n      description: \"協助我們打造更符合需求的 AnythingLLM。此問卷為選填。\",\n      email: \"您的電子郵件是什麼？\",\n      useCase: \"您將如何使用 AnythingLLM？\",\n      useCaseWork: \"用於工作\",\n      useCasePersonal: \"用於個人使用\",\n      useCaseOther: \"其他\",\n      comment: \"您是從哪裡知道 AnythingLLM 的？\",\n      commentPlaceholder:\n        \"例如 Reddit、Twitter、GitHub、YouTube 等，告訴我們您是怎麼找到 AnythingLLM 的！\",\n      skip: \"略過問卷\",\n      thankYou: \"感謝您的回饋！\",\n    },\n  },\n  common: {\n    \"workspaces-name\": \"工作區名稱\",\n    user: \"使用者\",\n    selection: \"模型選擇\",\n    saving: \"儲存中...\",\n    save: \"儲存變更\",\n    previous: \"上一頁\",\n    next: \"下一頁\",\n    optional: \"選填\",\n    yes: \"是\",\n    no: \"否\",\n    search: \"搜尋\",\n    username_requirements:\n      \"使用者名稱必須為 2-32 個字元，以小寫字母開頭，且只能包含小寫字母、數字、底線、連字號和句點。\",\n    on: \"關於\",\n    none: \"沒有\",\n    stopped: \"停止\",\n    loading: \"載入\",\n    refresh: \"重新整理\",\n  },\n  settings: {\n    title: \"系統設定\",\n    invites: \"邀請\",\n    users: \"使用者\",\n    workspaces: \"工作區\",\n    \"workspace-chats\": \"工作區對話紀錄\",\n    customization: \"自訂\",\n    \"api-keys\": \"開發者 API\",\n    llm: \"大型語言模型 (LLM)\",\n    transcription: \"語音轉錄\",\n    embedder: \"向量嵌入器\",\n    \"text-splitting\": \"文字分割與切塊\",\n    \"voice-speech\": \"語音與發音\",\n    \"vector-database\": \"向量資料庫\",\n    embeds: \"對話嵌入\",\n    security: \"安全性\",\n    \"event-logs\": \"事件記錄\",\n    privacy: \"隱私與資料\",\n    \"ai-providers\": \"AI 服務提供者\",\n    \"agent-skills\": \"智慧代理人技能\",\n    admin: \"管理\",\n    tools: \"工具\",\n    \"experimental-features\": \"實驗性功能\",\n    contact: \"聯絡支援\",\n    \"browser-extension\": \"瀏覽器擴充功能\",\n    \"system-prompt-variables\": \"系統提示變數\",\n    interface: \"介面偏好\",\n    branding: \"品牌與白標設定\",\n    chat: \"對話\",\n    \"mobile-app\": \"AnythingLLM 行動版\",\n    \"community-hub\": {\n      title: \"社群中心\",\n      trending: \"探索熱門\",\n      \"your-account\": \"您的帳戶\",\n      \"import-item\": \"匯入項目\",\n    },\n  },\n  login: {\n    \"multi-user\": {\n      welcome: \"歡迎使用\",\n      \"placeholder-username\": \"使用者名稱\",\n      \"placeholder-password\": \"密碼\",\n      login: \"登入\",\n      validating: \"驗證中...\",\n      \"forgot-pass\": \"忘記密碼\",\n      reset: \"重設\",\n    },\n    \"sign-in\": \"輸入使用者名稱與密碼，以存取您的 {{appName}} 系統。\",\n    \"password-reset\": {\n      title: \"重設密碼\",\n      description: \"請在下方提供必要資訊以重設您的密碼。\",\n      \"recovery-codes\": \"復原碼\",\n      \"back-to-login\": \"返回登入\",\n    },\n  },\n  \"new-workspace\": {\n    title: \"新增工作區\",\n    placeholder: \"我的工作區\",\n  },\n  \"workspaces—settings\": {\n    general: \"一般設定\",\n    chat: \"對話設定\",\n    vector: \"向量資料庫\",\n    members: \"成員\",\n    agent: \"智慧代理人設定\",\n  },\n  general: {\n    vector: {\n      title: \"向量計數\",\n      description: \"向量資料庫中的向量總數。\",\n    },\n    names: {\n      description: \"只會變更工作區的顯示名稱。\",\n    },\n    message: {\n      title: \"建議對話訊息\",\n      description: \"自訂要推薦給工作區使用者的訊息。\",\n      add: \"新增訊息\",\n      save: \"儲存訊息\",\n      heading: \"請向我說明\",\n      body: \"AnythingLLM 的優點\",\n    },\n    delete: {\n      title: \"刪除工作區\",\n      description: \"刪除此工作區及其所有資料。所有使用者都會失去這個工作區。\",\n      delete: \"刪除工作區\",\n      deleting: \"正在刪除工作區...\",\n      \"confirm-start\": \"您即將刪除整個\",\n      \"confirm-end\":\n        \"工作區。這會移除向量資料庫中的所有向量嵌入內容。\\n\\n原始來源檔案不會受影響。此動作無法復原。\",\n    },\n  },\n  chat: {\n    llm: {\n      title: \"工作區 LLM 提供者\",\n      description:\n        \"這個工作區要使用的 LLM 提供者與模型。預設會沿用系統層級的 LLM 提供者與設定。\",\n      search: \"搜尋所有 LLM 提供者\",\n    },\n    model: {\n      title: \"工作區對話模型\",\n      description:\n        \"這個工作區要使用的對話模型。若留空，會沿用系統層級的 LLM 偏好設定。\",\n    },\n    mode: {\n      title: \"對話模式\",\n      chat: {\n        title: \"對話\",\n        description:\n          \"將提供答案，利用 LLM 的一般知識和相關文件內容。您需要使用 `@agent` 命令來使用工具。\",\n      },\n      query: {\n        title: \"查詢\",\n        description:\n          \"將提供答案，僅在找到文件上下文時 <b>。您需要使用 @agent 指令來使用工具。\",\n      },\n      automatic: {\n        title: \"自動\",\n        description:\n          \"如果模型和供應商支援原生工具調用，則系統會自動使用這些工具。<br />如果原生工具調用不受支援，您需要使用 `@agent` 命令來使用工具。\",\n      },\n    },\n    history: {\n      title: \"對話紀錄\",\n      \"desc-start\": \"會納入回應短期記憶的過往對話訊息數量。\",\n      recommend: \"建議值為 20。\",\n      \"desc-end\": \"若超過 45，依訊息大小不同，很可能持續發生對話失敗。\",\n    },\n    prompt: {\n      title: \"系統提示詞\",\n      description:\n        \"這是此工作區會使用的提示詞，用來定義 AI 產生回應時的脈絡與指示。請提供經過仔細設計的提示詞，讓 AI 能產生相關且準確的回應。\",\n      history: {\n        title: \"系統提示詞歷史記錄\",\n        clearAll: \"清除全部\",\n        noHistory: \"目前沒有系統提示詞歷史記錄\",\n        restore: \"復原\",\n        delete: \"刪除\",\n        deleteConfirm: \"您確定要刪除此歷史記錄項目嗎？\",\n        clearAllConfirm: \"您確定要刪除所有歷史記錄嗎？此操作無法復原。\",\n        expand: \"展開\",\n        publish: \"發布到社群中心\",\n      },\n    },\n    refusal: {\n      title: \"查詢模式拒絕訊息\",\n      \"desc-start\": \"在\",\n      query: \"查詢\",\n      \"desc-end\": \"模式下，若找不到內容，您可以設定自訂的拒絕回應。\",\n      \"tooltip-title\": \"我為什麼會看到這個？\",\n      \"tooltip-description\":\n        \"目前處於查詢模式，這個模式只會使用文件中的資訊。若想進行更彈性的對話，請切換到對話模式；或點選這裡前往文件，進一步了解對話模式。\",\n    },\n    temperature: {\n      title: \"LLM 溫度\",\n      \"desc-start\": \"這項設定會控制 LLM 回應的「創意程度」。\",\n      \"desc-end\":\n        \"數值越高，創意度越高。對於某些模型，設定過高可能會導致不連貫的回應。\",\n      hint: \"大多數 LLM 都有各自可接受的有效值範圍，請向 LLM 提供者查詢。\",\n    },\n  },\n  \"vector-workspace\": {\n    identifier: \"向量資料庫識別碼\",\n    snippets: {\n      title: \"最大內容片段數\",\n      description: \"這項設定會控制每次對話或查詢送給 LLM 的內容片段上限。\",\n      recommend: \"建議值：4\",\n    },\n    doc: {\n      title: \"文件相似度門檻\",\n      description:\n        \"來源至少要達到這個相似度分數，才會被視為與對話相關。數值越高，代表來源必須越接近對話內容。\",\n      zero: \"不限制\",\n      low: \"低（相似度分數 ≥ .25）\",\n      medium: \"中（相似度分數 ≥ .50）\",\n      high: \"高（相似度分數 ≥ .75）\",\n    },\n    reset: {\n      reset: \"重設向量資料庫\",\n      resetting: \"正在清除向量...\",\n      confirm:\n        \"您即將重設這個工作區的向量資料庫。這會移除目前所有已嵌入的向量。\\n\\n原始來源檔案不會受影響。此動作無法復原。\",\n      error: \"無法重設工作區向量資料庫！\",\n      success: \"工作區向量資料庫已重設！\",\n    },\n  },\n  agent: {\n    \"performance-warning\":\n      \"對於未明確支援工具呼叫的 LLM，其效能高度仰賴模型本身的能力與準確度。部分功能可能受限，甚至無法使用。\",\n    provider: {\n      title: \"工作區智慧代理人 LLM 提供者\",\n      description: \"這個工作區的 @agent 會使用的 LLM 提供者與模型。\",\n    },\n    mode: {\n      chat: {\n        title: \"工作區 @agent 對話模型\",\n        description: \"這個工作區的 @agent 會使用的對話模型。\",\n      },\n      title: \"工作區 @agent 模型\",\n      description: \"這個工作區的 @agent 會使用的 LLM 模型。\",\n      wait: \"-- 正在等待模型 --\",\n    },\n    skill: {\n      rag: {\n        title: \"RAG 與長期記憶體\",\n        description:\n          \"讓智慧代理人可運用本機文件回答問題，也能要求智慧代理人「記住」特定內容片段，以供長期記憶擷取。\",\n      },\n      view: {\n        title: \"檢視並摘要文件\",\n        description: \"允許智慧代理人列出並摘要目前已嵌入的工作區檔案內容。\",\n      },\n      scrape: {\n        title: \"擷取網站\",\n        description: \"允許智慧代理人瀏覽並擷取網站內容。\",\n      },\n      generate: {\n        title: \"產生圖表\",\n        description:\n          \"讓預設智慧代理人能夠根據提供的資料或對話中給定的資料來產生各種圖表。\",\n      },\n      save: {\n        title: \"產生並儲存檔案\",\n        description: \"讓預設智慧代理人產生並寫入檔案，之後可儲存到電腦。\",\n      },\n      web: {\n        title: \"網頁搜尋\",\n        description:\n          \"透過連接網頁搜尋 (SERP) 提供者，讓智慧代理人能搜尋網路並回答問題。\",\n      },\n      sql: {\n        title: \"SQL 連接器\",\n        description:\n          \"讓您的智慧代理人能夠利用 SQL 查詢來回答您的問題，只需連接到不同的 SQL 資料庫提供者即可。\",\n      },\n      default_skill: \"這項技能預設為啟用；若不希望智慧代理人使用，也可以停用。\",\n    },\n    mcp: {\n      title: \"MCP 伺服器\",\n      \"loading-from-config\": \"從設定檔中載入 MCP 伺服器\",\n      \"learn-more\": \"了解更多關於 MCP 伺服器的資訊。\",\n      \"no-servers-found\": \"未找到任何 MCP 伺服器\",\n      \"tool-warning\": \"為了獲得最佳效能，建議關閉不必要的工具，以節省資源。\",\n      \"stop-server\": \"停止 MCP 伺服器\",\n      \"start-server\": \"啟動 MCP 伺服器\",\n      \"delete-server\": \"刪除 MCP 伺服器\",\n      \"tool-count-warning\":\n        \"這個 MCP 伺服器已啟用 <b> 工具，這些工具會消耗聊天中的語境 </b>。建議停用不需要的工具，以節省語境。\",\n      \"startup-command\": \"啟動指令\",\n      command: \"指令\",\n      arguments: \"辯論\",\n      \"not-running-warning\":\n        \"這個 MCP 伺服器目前處於停止狀態，可能是因為已停止運作，或是啟動時出現錯誤。\",\n      \"tool-call-arguments\": \"工具呼叫的參數\",\n      \"tools-enabled\": \"已啟用工具\",\n    },\n    settings: {\n      title: \"代理人技能設定\",\n      \"max-tool-calls\": {\n        title: \"每次回應的最大工具呼叫次數\",\n        description:\n          \"這設定了代理可以串聯使用的最大工具數量，以確保每次回應只會呼叫有限的工具，並避免無限循環。\",\n      },\n      \"intelligent-skill-selection\": {\n        title: \"智能技能選擇\",\n        \"beta-badge\": \"β 版本\",\n        description:\n          \"啟用無限多個工具，並將每個查詢的 token 使用量最多降低 80% — AnythingLLM 能夠自動選擇最適合的技能，以處理每一個提示。\",\n        \"max-tools\": {\n          title: \"馬克斯工具\",\n          description:\n            \"可選取的工具的最大數量，適用於每個查詢。我們建議將此值設定為較高的值，以適用於較大的模型。\",\n        },\n      },\n    },\n  },\n  recorded: {\n    title: \"工作區對話紀錄\",\n    description: \"這裡列出所有已記錄的對話與訊息，依建立時間排序。\",\n    export: \"匯出\",\n    table: {\n      id: \"編號\",\n      by: \"傳送者\",\n      workspace: \"工作區\",\n      prompt: \"提示詞\",\n      response: \"回應\",\n      at: \"傳送時間\",\n    },\n  },\n  api: {\n    title: \"API 金鑰\",\n    description:\n      \"API 金鑰可讓持有人透過程式方式存取並管理這個 AnythingLLM 系統。\",\n    link: \"閱讀 API 文件\",\n    generate: \"產生新的 API 金鑰\",\n    table: {\n      key: \"API 金鑰\",\n      by: \"建立者\",\n      created: \"建立時間\",\n    },\n  },\n  llm: {\n    title: \"LLM 偏好設定\",\n    description:\n      \"這裡設定偏好的 LLM 對話與嵌入提供者之認證資訊與參數。請確認金鑰保持最新且正確，否則 AnythingLLM 可能無法正常運作。\",\n    provider: \"LLM 提供者\",\n    providers: {\n      azure_openai: {\n        azure_service_endpoint: \"Azure 服務端點\",\n        api_key: \"API 金鑰\",\n        chat_deployment_name: \"對話部署名稱\",\n        chat_model_token_limit: \"對話模型 Token 上限\",\n        model_type: \"模型類型\",\n        default: \"預設\",\n        reasoning: \"推理\",\n        model_type_tooltip:\n          \"如果您的部署使用推理模型（例如 o1、o1-mini、o3-mini 等），請將此設定設為「推理」。否則，您的對話請求可能會失敗。\",\n      },\n    },\n  },\n  transcription: {\n    title: \"語音轉錄模型偏好設定\",\n    description:\n      \"這裡設定偏好的語音轉錄模型提供者之認證資訊與參數。請確認金鑰保持最新且正確，否則媒體檔與音訊可能無法完成轉錄。\",\n    provider: \"語音轉錄提供者\",\n    \"warn-start\":\n      \"在記憶體或 CPU 資源有限的電腦上使用本機 Whisper 模型時，處理媒體檔案可能會讓 AnythingLLM 卡住。\",\n    \"warn-recommend\": \"建議至少保留 2 GB 記憶體，且上傳檔案小於 10 MB。\",\n    \"warn-end\": \"內建模型將會在第一次使用時自動下載。\",\n  },\n  embedding: {\n    title: \"嵌入模型偏好設定\",\n    \"desc-start\":\n      \"使用原生不支援嵌入引擎的 LLM 時，可能需要另外提供文字嵌入的認證資訊。\",\n    \"desc-end\":\n      \"嵌入是把文字轉成向量的過程。這些認證資訊用來把檔案與提示詞轉成 AnythingLLM 可處理的格式。\",\n    provider: {\n      title: \"向量嵌入提供者\",\n    },\n  },\n  text: {\n    title: \"文字分割與切塊偏好設定\",\n    \"desc-start\":\n      \"有時您可能想調整新文件在寫入向量資料庫前的預設分割與切塊方式。\",\n    \"desc-end\":\n      \"只有在清楚了解文字分割的運作方式及其副作用時，才建議調整此設定。\",\n    size: {\n      title: \"文字區塊大小\",\n      description: \"單一向量可包含的最大字元長度。\",\n      recommend: \"嵌入模型的最大長度為\",\n    },\n    overlap: {\n      title: \"文字切塊重疊\",\n      description: \"切塊時兩個相鄰文字區塊之間允許的最大重疊字元數。\",\n    },\n  },\n  vector: {\n    title: \"向量資料庫\",\n    description:\n      \"這裡設定 AnythingLLM 系統運作所需的認證資訊與參數。請務必確認金鑰保持最新且正確。\",\n    provider: {\n      title: \"向量資料庫提供者\",\n      description: \"使用 LanceDB 不需要任何設定。\",\n    },\n  },\n  embeddable: {\n    title: \"可嵌入對話元件\",\n    description:\n      \"可嵌入對話元件是綁定單一工作區、可對外公開的對話介面。您可以建立工作區，再將它發布給外部使用。\",\n    create: \"建立嵌入元件\",\n    table: {\n      workspace: \"工作區\",\n      chats: \"已傳送對話\",\n      active: \"啟用中的網域\",\n      created: \"建立\",\n    },\n  },\n  \"embed-chats\": {\n    title: \"嵌入對話記錄\",\n    export: \"匯出\",\n    description: \"這裡列出所有來自已發布嵌入元件的對話與訊息紀錄。\",\n    table: {\n      embed: \"嵌入\",\n      sender: \"傳送者\",\n      message: \"訊息\",\n      response: \"回應\",\n      at: \"傳送時間\",\n    },\n  },\n  event: {\n    title: \"事件記錄\",\n    description: \"檢視這套系統上發生的所有動作與事件，以便監控。\",\n    clear: \"清除事件記錄\",\n    table: {\n      type: \"事件類型\",\n      user: \"使用者\",\n      occurred: \"發生時間\",\n    },\n  },\n  privacy: {\n    title: \"隱私與資料處理\",\n    description: \"這裡設定已連線的第三方提供者與 AnythingLLM 會如何處理資料。\",\n    anonymous: \"已啟用匿名遙測\",\n  },\n  connectors: {\n    \"search-placeholder\": \"搜尋資料連接器\",\n    \"no-connectors\": \"未找到資料連接器。\",\n    github: {\n      name: \"GitHub 儲存庫\",\n      description: \"一鍵匯入整個公開或私有的 GitHub 儲存庫。\",\n      URL: \"GitHub 儲存庫網址\",\n      URL_explained: \"您希望收集的 GitHub 儲存庫網址。\",\n      token: \"GitHub 存取權杖\",\n      optional: \"可選\",\n      token_explained: \"存取權杖以防止速率限制。\",\n      token_explained_start: \"若沒有 \",\n      token_explained_link1: \"個人存取權杖\",\n      token_explained_middle:\n        \"，GitHub API 可能會因為速率限制而限制可收集的檔案數量。您可以 \",\n      token_explained_link2: \"建立一個臨時的存取權杖\",\n      token_explained_end: \" 來避免此問題。\",\n      ignores: \"忽略檔案\",\n      git_ignore:\n        \"以 .gitignore 格式列出以忽略特定檔案。每輸入一個條目後按 Enter 鍵儲存。\",\n      task_explained: \"完成後，所有檔案將可供嵌入到工作區中的檔案選擇器。\",\n      branch: \"您希望收集檔案的分支。\",\n      branch_loading: \"-- 載入可用分支 --\",\n      branch_explained: \"您希望收集檔案的分支。\",\n      token_information:\n        \"若未填寫 <b>GitHub 存取權杖</b>，此資料連接器僅能收集儲存庫的 <b>頂層</b> 檔案，因 GitHub 的公開 API 速率限制。\",\n      token_personal: \"在此獲取免費的 GitHub 個人存取權杖。\",\n    },\n    gitlab: {\n      name: \"GitLab 儲存庫\",\n      description: \"一鍵匯入整個公開或私有的 GitLab 儲存庫。\",\n      URL: \"GitLab 儲存庫網址\",\n      URL_explained: \"您希望收集的 GitLab 儲存庫網址。\",\n      token: \"GitLab 存取權杖\",\n      optional: \"可選\",\n      token_description: \"選擇要從 GitLab API 中擷取的其他實體。\",\n      token_explained_start: \"若沒有 \",\n      token_explained_link1: \"個人存取權杖\",\n      token_explained_middle:\n        \"，GitLab API 可能會因為速率限制而限制可收集的檔案數量。您可以 \",\n      token_explained_link2: \"建立一個臨時的存取權杖\",\n      token_explained_end: \" 來避免此問題。\",\n      fetch_issues: \"擷取問題作為文件\",\n      ignores: \"忽略檔案\",\n      git_ignore:\n        \"以 .gitignore 格式列出以忽略特定檔案。每輸入一個條目後按 Enter 鍵儲存。\",\n      task_explained: \"完成後，所有檔案將可供嵌入到工作區中的檔案選擇器。\",\n      branch: \"您希望收集檔案的分支\",\n      branch_loading: \"-- 載入可用分支 --\",\n      branch_explained: \"您希望收集檔案的分支。\",\n      token_information:\n        \"若未填寫 <b>GitLab 存取權杖</b>，此資料連接器僅能收集儲存庫的 <b>頂層</b> 檔案，因 GitLab 的公開 API 速率限制。\",\n      token_personal: \"在此獲取免費的 GitLab 個人存取權杖。\",\n    },\n    youtube: {\n      name: \"YouTube 文字稿\",\n      description: \"從連結匯入整個 YouTube 影片的文字稿。\",\n      URL: \"YouTube 影片網址\",\n      URL_explained_start:\n        \"輸入任何 YouTube 影片的網址以擷取其文字稿。該影片必須擁有 \",\n      URL_explained_link: \"字幕\",\n      URL_explained_end: \" 來提供文字稿。\",\n      task_explained: \"完成後，文字稿將可供嵌入到工作區中的檔案選擇器。\",\n    },\n    \"website-depth\": {\n      name: \"批次連結擷取器\",\n      description: \"擷取網站及其子連結，直到指定深度。\",\n      URL: \"網站網址\",\n      URL_explained: \"您希望擷取的網站網址。\",\n      depth: \"擷取深度\",\n      depth_explained: \"系統會從起始網址往下追蹤的子連結層數。\",\n      max_pages: \"最大頁數\",\n      max_pages_explained: \"最大擷取連結數量。\",\n      task_explained:\n        \"完成後，所有擷取的內容將可供嵌入到工作區中的檔案選擇器。\",\n    },\n    confluence: {\n      name: \"Confluence\",\n      description: \"一鍵匯入整個 Confluence 頁面。\",\n      deployment_type: \"Confluence 部署類型\",\n      deployment_type_explained:\n        \"確認 Confluence 環境是託管於 Atlassian 雲端，還是自行託管。\",\n      base_url: \"Confluence 基本網址\",\n      base_url_explained: \"這是您的 Confluence 空間的基本網址。\",\n      space_key: \"Confluence 空間金鑰\",\n      space_key_explained:\n        \"這是 Confluence 環境要使用的空間金鑰，通常會以 ~ 開頭。\",\n      username: \"Confluence 使用者名稱\",\n      username_explained: \"請輸入 Confluence 使用者名稱。\",\n      auth_type: \"Confluence 認證類型\",\n      auth_type_explained: \"選擇您希望用來存取 Confluence 頁面的認證類型。\",\n      auth_type_username: \"使用者名稱和存取權杖\",\n      auth_type_personal: \"個人存取權杖\",\n      token: \"Confluence 存取權杖\",\n      token_explained_start: \"需要提供存取權杖才能完成驗證。您可以在 \",\n      token_explained_link: \"這裡\",\n      token_desc: \"用於認證的存取權杖\",\n      pat_token: \"Confluence 個人存取權杖\",\n      pat_token_explained: \"您的 Confluence 個人存取權杖。\",\n      task_explained: \"完成後，頁面內容將可供嵌入到工作區中的檔案選擇器。\",\n      bypass_ssl: \"跳過 SSL 憑證驗證\",\n      bypass_ssl_explained:\n        \"若是使用自簽憑證的自行託管 Confluence 環境，可啟用此選項略過 SSL 憑證驗證。\",\n    },\n    manage: {\n      documents: \"文件\",\n      \"data-connectors\": \"資料連接器\",\n      \"desktop-only\":\n        \"編輯這些設定僅在桌面裝置上可用。請在桌面上開啟此頁面以繼續。\",\n      dismiss: \"忽略\",\n      editing: \"編輯中\",\n    },\n    directory: {\n      \"my-documents\": \"我的文件\",\n      \"new-folder\": \"新資料夾\",\n      \"search-document\": \"搜尋文件\",\n      \"no-documents\": \"無文件\",\n      \"move-workspace\": \"移動到工作區\",\n      \"delete-confirmation\":\n        \"您確定要刪除這些檔案和資料夾嗎？\\n這將從系統中刪除這些檔案並自動從任何現有工作區中移除它們。\\n此操作無法還原。\",\n      \"removing-message\":\n        \"正在刪除 {{count}} 份文件和 {{folderCount}} 個資料夾，請稍候。\",\n      \"move-success\": \"已成功移動 {{count}} 份文件。\",\n      no_docs: \"無文件\",\n      select_all: \"全選\",\n      deselect_all: \"取消全選\",\n      remove_selected: \"移除選擇的項目\",\n      costs: \"*嵌入僅會計費一次\",\n      save_embed: \"儲存並嵌入\",\n      \"total-documents_one\": \"{{count}} 文件\",\n      \"total-documents_other\": \"{{count}} 文件\",\n    },\n    upload: {\n      \"processor-offline\": \"文件處理器無法使用\",\n      \"processor-offline-desc\":\n        \"目前無法上傳檔案，因為文件處理器已離線。請稍後再試。\",\n      \"click-upload\": \"點選以上傳，或直接拖放檔案\",\n      \"file-types\": \"支援文字檔、CSV、試算表、音訊檔等格式！\",\n      \"or-submit-link\": \"或貼上連結\",\n      \"placeholder-link\": \"https://example.com\",\n      fetching: \"正在擷取...\",\n      \"fetch-website\": \"擷取網站\",\n      \"privacy-notice\":\n        \"這些檔案會上傳到此 AnythingLLM 系統上的文件處理器，不會傳送給或分享給第三方。\",\n    },\n    pinning: {\n      what_pinning: \"什麼是文件釘選？\",\n      pin_explained_block1:\n        \"當您在 AnythingLLM 中<b>釘選</b>文件時，系統會把整份文件的內容注入提示詞輸入區，讓 LLM 能完整理解。\",\n      pin_explained_block2:\n        \"這最適合搭配<b>大上下文模型</b>，或對知識庫很重要的小型文件。\",\n      pin_explained_block3:\n        \"如果 AnythingLLM 在預設情況下給不出想要的答案，釘選文件是快速提升回答品質的好方法。\",\n      accept: \"好的，明白了\",\n    },\n    watching: {\n      what_watching: \"追蹤文件有何作用？\",\n      watch_explained_block1:\n        \"當您在 AnythingLLM 中<b>追蹤</b>文件時，系統會<i>自動</i>定期從原始來源同步內容，並更新所有管理這份文件的工作區。\",\n      watch_explained_block2:\n        \"目前這項功能只支援線上來源內容，手動上傳的文件無法使用。\",\n      watch_explained_block3_start: \"您可以從 \",\n      watch_explained_block3_link: \"檔案管理器\",\n      watch_explained_block3_end: \" 管理頁面查看及管理追蹤中的文件。\",\n      accept: \"好的，明白了\",\n    },\n    obsidian: {\n      vault_location: \"Vault 位置\",\n      vault_description:\n        \"選擇您的 Obsidian Vault 資料夾以匯入所有筆記及其連結。\",\n      selected_files: \"找到 {{count}} 個 Markdown 檔案\",\n      importing: \"正在匯入 Vault...\",\n      import_vault: \"匯入 Vault\",\n      processing_time: \"這可能需要一段時間，具體取決於您的 Vault 大小。\",\n      vault_warning: \"為避免任何衝突，請確保您的 Obsidian Vault 目前未開啟。\",\n    },\n  },\n  chat_window: {\n    send_message: \"傳送訊息\",\n    attach_file: \"將檔案附加到這段對話\",\n    text_size: \"調整文字大小。\",\n    microphone: \"以語音輸入提示詞。\",\n    send: \"將提示詞送到工作區\",\n    attachments_processing: \"附件正在處理中，請稍後...\",\n    tts_speak_message: \"朗讀訊息\",\n    copy: \"複製\",\n    regenerate: \"重新產生\",\n    regenerate_response: \"重新產生回應\",\n    good_response: \"標記為優質回應\",\n    more_actions: \"更多操作\",\n    fork: \"分支對話\",\n    delete: \"刪除\",\n    cancel: \"取消\",\n    edit_prompt: \"編輯提示詞\",\n    edit_response: \"編輯回應\",\n    preset_reset_description: \"清除聊天紀錄並開始新的聊天\",\n    add_new_preset: \"新增預設\",\n    command: \"指令\",\n    your_command: \"your-command\",\n    placeholder_prompt: \"這段內容會插入在提示詞前方。\",\n    description: \"描述\",\n    placeholder_description: \"回應一首關於 LLM 的詩。\",\n    save: \"儲存\",\n    small: \"小\",\n    normal: \"一般\",\n    large: \"大\",\n    workspace_llm_manager: {\n      search: \"搜尋 LLM 提供者\",\n      loading_workspace_settings: \"正在載入工作區設定...\",\n      available_models: \"{{provider}} 可用模型\",\n      available_models_description: \"選擇要在此工作區使用的模型。\",\n      save: \"使用此模型\",\n      saving: \"正在將模型設為工作區預設值...\",\n      missing_credentials: \"此提供者缺少憑證！\",\n      missing_credentials_description: \"點選以設定認證資訊\",\n    },\n    submit: \"送出\",\n    edit_info_user: \"「送出」會重新產生 AI 回應。「儲存」只會更新訊息內容。\",\n    edit_info_assistant: \"您的修改將直接儲存到此處。\",\n    see_less: \"顯示較少\",\n    see_more: \"查看更多\",\n    tools: \"工具\",\n    browse: \"瀏覽\",\n    text_size_label: \"文字大小\",\n    select_model: \"選擇模型\",\n    sources: \"來源\",\n    document: \"文件\",\n    similarity_match: \"相符度\",\n    source_count_one: \"{{count}} 筆參考資料\",\n    source_count_other: \"{{count}} 筆參考資料\",\n    preset_exit_description: \"暫停目前的智慧代理人工作階段\",\n    add_new: \"新增\",\n    edit: \"編輯\",\n    publish: \"發佈\",\n    stop_generating: \"停止產生回應\",\n    pause_tts_speech_message: \"暫停語音合成的訊息\",\n    slash_commands: \"斜線指令\",\n    agent_skills: \"智慧代理人技能\",\n    manage_agent_skills: \"管理智慧代理人技能\",\n    agent_skills_disabled_in_session:\n      \"啟用智慧代理人工作階段時無法修改技能。請先使用 /exit 指令結束目前工作階段。\",\n    start_agent_session: \"開始智慧代理人工作階段\",\n    use_agent_session_to_use_tools:\n      \"若要在對話中使用工具，請在提示詞開頭加上 '@agent'，即可開始智慧代理人工作階段。\",\n  },\n  profile_settings: {\n    edit_account: \"編輯帳戶\",\n    profile_picture: \"個人資料圖片\",\n    remove_profile_picture: \"移除個人資料圖片\",\n    username: \"使用者名稱\",\n    new_password: \"新密碼\",\n    password_description: \"密碼長度必須至少為 8 個字元\",\n    cancel: \"取消\",\n    update_account: \"更新帳戶\",\n    theme: \"主題偏好\",\n    language: \"慣用語言\",\n    failed_upload: \"上傳個人資料圖片失敗：{{error}}\",\n    upload_success: \"個人資料圖片已上傳。\",\n    failed_remove: \"移除個人資料圖片失敗：{{error}}\",\n    profile_updated: \"個人資料已更新。\",\n    failed_update_user: \"更新使用者資料失敗：{{error}}\",\n    account: \"帳戶\",\n    support: \"支援\",\n    signout: \"登出\",\n  },\n  customization: {\n    interface: {\n      title: \"介面偏好設定\",\n      description: \"設定 AnythingLLM 的介面偏好。\",\n    },\n    branding: {\n      title: \"品牌與白標設定\",\n      description: \"透過自訂品牌元素，將 AnythingLLM 白標化。\",\n    },\n    chat: {\n      title: \"對話\",\n      description: \"設定 AnythingLLM 的對話偏好。\",\n      auto_submit: {\n        title: \"語音輸入自動送出\",\n        description: \"在一段靜默後自動送出語音輸入\",\n      },\n      auto_speak: {\n        title: \"自動語音回應\",\n        description: \"自動朗讀 AI 的回應內容\",\n      },\n      spellcheck: {\n        title: \"啟用拼字檢查\",\n        description: \"在對話輸入框中啟用或停用拼字檢查\",\n      },\n    },\n    items: {\n      theme: {\n        title: \"主題\",\n        description: \"選擇偏好的應用程式色彩主題。\",\n      },\n      \"show-scrollbar\": {\n        title: \"顯示捲軸\",\n        description: \"在對話視窗中啟用或停用捲軸。\",\n      },\n      \"support-email\": {\n        title: \"支援信箱\",\n        description: \"設定當使用者需要協助時可聯絡的支援電子郵件地址。\",\n      },\n      \"app-name\": {\n        title: \"名稱\",\n        description: \"設定顯示在登入頁面、讓所有使用者都看得到的名稱。\",\n      },\n      \"display-language\": {\n        title: \"顯示語言\",\n        description: \"選擇 AnythingLLM 介面的顯示語言；若已有翻譯就會套用。\",\n      },\n      logo: {\n        title: \"品牌標誌\",\n        description: \"上傳自訂標誌，顯示於所有頁面。\",\n        add: \"新增自訂標誌\",\n        recommended: \"建議尺寸：800 x 200\",\n        remove: \"移除\",\n        replace: \"更換\",\n      },\n      \"browser-appearance\": {\n        title: \"瀏覽器外觀\",\n        description: \"自訂應用程式在瀏覽器分頁中的外觀與標題。\",\n        tab: {\n          title: \"分頁標題\",\n          description: \"當應用程式在瀏覽器中開啟時設定自訂的分頁標題。\",\n        },\n        favicon: {\n          title: \"網站圖示 (Favicon)\",\n          description: \"為瀏覽器分頁設定自訂網站圖示。\",\n        },\n      },\n      \"sidebar-footer\": {\n        title: \"側邊欄頁尾項目\",\n        description: \"自訂側邊欄底部顯示的項目。\",\n        icon: \"圖示\",\n        link: \"連結\",\n      },\n      \"render-html\": {\n        title: \"在對話中渲染 HTML\",\n        description:\n          \"在助理回應中渲染 HTML 內容。\\n這能顯著提升呈現精細度，但也可能帶來潛在安全風險。\",\n      },\n    },\n  },\n  \"main-page\": {\n    quickActions: {\n      createAgent: \"建立智慧代理人\",\n      editWorkspace: \"編輯工作區\",\n      uploadDocument: \"上傳文件\",\n    },\n    greeting: \"今天想做什麼？\",\n  },\n  \"keyboard-shortcuts\": {\n    title: \"鍵盤快速鍵\",\n    shortcuts: {\n      settings: \"開啟設定\",\n      workspaceSettings: \"開啟目前工作區設定\",\n      home: \"前往首頁\",\n      workspaces: \"管理工作區\",\n      apiKeys: \"API 金鑰設定\",\n      llmPreferences: \"LLM 偏好設定\",\n      chatSettings: \"對話設定\",\n      help: \"顯示快速鍵說明\",\n      showLLMSelector: \"顯示工作區 LLM 選擇器\",\n    },\n  },\n  community_hub: {\n    publish: {\n      system_prompt: {\n        success_title: \"成功！\",\n        success_description: \"您的系統提示詞已發布到社群中心！\",\n        success_thank_you: \"感謝您分享到社群！\",\n        view_on_hub: \"在社群中心查看\",\n        modal_title: \"發布系統提示詞\",\n        name_label: \"名稱\",\n        name_description: \"這是系統提示詞的顯示名稱。\",\n        name_placeholder: \"我的系統提示詞\",\n        description_label: \"描述\",\n        description_description: \"這是系統提示詞的描述，可用來說明用途。\",\n        tags_label: \"標籤\",\n        tags_description:\n          \"標籤用來標示系統提示詞，方便搜尋。可新增多個標籤，最多 5 個，每個標籤最多 20 個字元。\",\n        tags_placeholder: \"輸入並按 Enter 鍵添加標籤\",\n        visibility_label: \"可見範圍\",\n        public_description: \"公開的系統提示詞對所有人都可見。\",\n        private_description: \"私人系統提示詞只有您看得到。\",\n        publish_button: \"發布到社群中心\",\n        submitting: \"發布中...\",\n        prompt_label: \"提示詞\",\n        prompt_description: \"這是實際會用來引導 LLM 的系統提示詞。\",\n        prompt_placeholder: \"在此輸入系統提示詞...\",\n      },\n      agent_flow: {\n        success_title: \"成功！\",\n        success_description: \"您的代理流程已發布到社群中心！\",\n        success_thank_you: \"感謝您分享到社群！\",\n        view_on_hub: \"在社群中心查看\",\n        modal_title: \"發布代理流程\",\n        name_label: \"名稱\",\n        name_description: \"這是代理流程的顯示名稱。\",\n        name_placeholder: \"我的代理流程\",\n        description_label: \"描述\",\n        description_description: \"這是代理流程的描述，可用來說明用途。\",\n        tags_label: \"標籤\",\n        tags_description:\n          \"標籤用來標示代理流程，方便搜尋。可新增多個標籤，最多 5 個，每個標籤最多 20 個字元。\",\n        tags_placeholder: \"輸入並按 Enter 鍵添加標籤\",\n        visibility_label: \"可見範圍\",\n        submitting: \"發布中...\",\n        submit: \"發布到社群中心\",\n        privacy_note:\n          \"代理流程一律會先以私人方式上傳，以保護敏感資料。發布後可再到社群中心調整可見範圍。發布前請先確認流程中不含任何敏感或私人資訊。\",\n      },\n      generic: {\n        unauthenticated: {\n          title: \"需要驗證\",\n          description: \"發布項目前，需先完成 AnythingLLM 社群中心驗證。\",\n          button: \"連接到社群中心\",\n        },\n      },\n      slash_command: {\n        success_title: \"成功！\",\n        success_description: \"您的斜線指令已發布到社群中心！\",\n        success_thank_you: \"感謝您分享到社群！\",\n        view_on_hub: \"在社群中心查看\",\n        modal_title: \"發布斜線指令\",\n        name_label: \"名稱\",\n        name_description: \"這是斜線指令的顯示名稱。\",\n        name_placeholder: \"我的斜線指令\",\n        description_label: \"描述\",\n        description_description: \"這是斜線指令的描述，可用來說明用途。\",\n        tags_label: \"標籤\",\n        tags_description:\n          \"標籤用來標示斜線指令，方便搜尋。可新增多個標籤，最多 5 個，每個標籤最多 20 個字元。\",\n        tags_placeholder: \"輸入並按 Enter 鍵添加標籤\",\n        visibility_label: \"可見範圍\",\n        public_description: \"公開的斜線指令對所有人都可見。\",\n        private_description: \"私人斜線指令只有您看得到。\",\n        publish_button: \"發布到社群中心\",\n        submitting: \"發布中...\",\n        prompt_label: \"提示詞\",\n        prompt_description: \"這是觸發斜線指令時會使用的提示詞。\",\n        prompt_placeholder: \"在此輸入提示詞...\",\n      },\n    },\n  },\n  security: {\n    title: \"安全性設定\",\n    multiuser: {\n      title: \"多使用者模式\",\n      description: \"啟用多使用者模式，讓這套系統支援團隊使用。\",\n      enable: {\n        \"is-enable\": \"多使用者模式已啟用\",\n        enable: \"啟用多使用者模式\",\n        description:\n          \"預設只有您具備管理員權限。身為管理員，您需要為所有新使用者或管理員建立帳號。請勿遺失密碼，因為只有管理員可以重設密碼。\",\n        username: \"管理員帳號使用者名稱\",\n        password: \"管理員帳號密碼\",\n      },\n    },\n    password: {\n      title: \"密碼保護\",\n      description:\n        \"使用密碼保護 AnythingLLM 系統。若忘記此密碼，將無法復原，請務必妥善保存。\",\n      \"password-label\": \"系統密碼\",\n    },\n  },\n  home: {\n    welcome: \"歡迎\",\n    chooseWorkspace: \"選擇一個工作區開始對話！\",\n    notAssigned:\n      \"您目前尚未被分配到任何工作區。\\n請聯絡您的管理員以申請工作區的存取權限。\",\n    goToWorkspace: '前往 \"{{workspace}}\"',\n  },\n};\n\nexport default TRANSLATIONS;\n"
  },
  {
    "path": "frontend/src/main.jsx",
    "content": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport { createBrowserRouter, RouterProvider } from \"react-router-dom\";\nimport App from \"@/App.jsx\";\nimport PrivateRoute, {\n  AdminRoute,\n  ManagerRoute,\n} from \"@/components/PrivateRoute\";\nimport Login from \"@/pages/Login\";\nimport SimpleSSOPassthrough from \"@/pages/Login/SSO/simple\";\nimport OnboardingFlow from \"@/pages/OnboardingFlow\";\nimport \"@/index.css\";\n\nconst isDev = import.meta.env.DEV;\nconst REACTWRAP = isDev ? React.Fragment : React.StrictMode;\n\nconst router = createBrowserRouter([\n  {\n    path: \"/\",\n    element: <App />,\n    children: [\n      {\n        path: \"/\",\n        lazy: async () => {\n          const { default: Main } = await import(\"@/pages/Main\");\n          return { element: <PrivateRoute Component={Main} /> };\n        },\n      },\n      {\n        path: \"/login\",\n        element: <Login />,\n      },\n      {\n        path: \"/sso/simple\",\n        element: <SimpleSSOPassthrough />,\n      },\n      {\n        path: \"/workspace/:slug/settings/:tab\",\n        lazy: async () => {\n          const { default: WorkspaceSettings } = await import(\n            \"@/pages/WorkspaceSettings\"\n          );\n          return { element: <ManagerRoute Component={WorkspaceSettings} /> };\n        },\n      },\n      {\n        path: \"/workspace/:slug\",\n        lazy: async () => {\n          const { default: WorkspaceChat } = await import(\n            \"@/pages/WorkspaceChat\"\n          );\n          return { element: <PrivateRoute Component={WorkspaceChat} /> };\n        },\n      },\n      {\n        path: \"/workspace/:slug/t/:threadSlug\",\n        lazy: async () => {\n          const { default: WorkspaceChat } = await import(\n            \"@/pages/WorkspaceChat\"\n          );\n          return { element: <PrivateRoute Component={WorkspaceChat} /> };\n        },\n      },\n      {\n        path: \"/accept-invite/:code\",\n        lazy: async () => {\n          const { default: InvitePage } = await import(\"@/pages/Invite\");\n          return { element: <InvitePage /> };\n        },\n      },\n      // Admin routes\n      {\n        path: \"/settings/llm-preference\",\n        lazy: async () => {\n          const { default: GeneralLLMPreference } = await import(\n            \"@/pages/GeneralSettings/LLMPreference\"\n          );\n          return { element: <AdminRoute Component={GeneralLLMPreference} /> };\n        },\n      },\n      {\n        path: \"/settings/transcription-preference\",\n        lazy: async () => {\n          const { default: GeneralTranscriptionPreference } = await import(\n            \"@/pages/GeneralSettings/TranscriptionPreference\"\n          );\n          return {\n            element: <AdminRoute Component={GeneralTranscriptionPreference} />,\n          };\n        },\n      },\n      {\n        path: \"/settings/audio-preference\",\n        lazy: async () => {\n          const { default: GeneralAudioPreference } = await import(\n            \"@/pages/GeneralSettings/AudioPreference\"\n          );\n          return {\n            element: <AdminRoute Component={GeneralAudioPreference} />,\n          };\n        },\n      },\n      {\n        path: \"/settings/embedding-preference\",\n        lazy: async () => {\n          const { default: GeneralEmbeddingPreference } = await import(\n            \"@/pages/GeneralSettings/EmbeddingPreference\"\n          );\n          return {\n            element: <AdminRoute Component={GeneralEmbeddingPreference} />,\n          };\n        },\n      },\n      {\n        path: \"/settings/text-splitter-preference\",\n        lazy: async () => {\n          const { default: EmbeddingTextSplitterPreference } = await import(\n            \"@/pages/GeneralSettings/EmbeddingTextSplitterPreference\"\n          );\n          return {\n            element: <AdminRoute Component={EmbeddingTextSplitterPreference} />,\n          };\n        },\n      },\n      {\n        path: \"/settings/vector-database\",\n        lazy: async () => {\n          const { default: GeneralVectorDatabase } = await import(\n            \"@/pages/GeneralSettings/VectorDatabase\"\n          );\n          return {\n            element: <AdminRoute Component={GeneralVectorDatabase} />,\n          };\n        },\n      },\n      {\n        path: \"/settings/agents\",\n        lazy: async () => {\n          const { default: AdminAgents } = await import(\"@/pages/Admin/Agents\");\n          return { element: <AdminRoute Component={AdminAgents} /> };\n        },\n      },\n      {\n        path: \"/settings/agents/builder\",\n        lazy: async () => {\n          const { default: AgentBuilder } = await import(\n            \"@/pages/Admin/AgentBuilder\"\n          );\n          return {\n            element: (\n              <AdminRoute Component={AgentBuilder} hideUserMenu={true} />\n            ),\n          };\n        },\n      },\n      {\n        path: \"/settings/agents/builder/:flowId\",\n        lazy: async () => {\n          const { default: AgentBuilder } = await import(\n            \"@/pages/Admin/AgentBuilder\"\n          );\n          return {\n            element: (\n              <AdminRoute Component={AgentBuilder} hideUserMenu={true} />\n            ),\n          };\n        },\n      },\n      {\n        path: \"/settings/event-logs\",\n        lazy: async () => {\n          const { default: AdminLogs } = await import(\"@/pages/Admin/Logging\");\n          return { element: <AdminRoute Component={AdminLogs} /> };\n        },\n      },\n      {\n        path: \"/settings/embed-chat-widgets\",\n        lazy: async () => {\n          const { default: ChatEmbedWidgets } = await import(\n            \"@/pages/GeneralSettings/ChatEmbedWidgets\"\n          );\n          return { element: <AdminRoute Component={ChatEmbedWidgets} /> };\n        },\n      },\n      // Manager routes\n      {\n        path: \"/settings/security\",\n        lazy: async () => {\n          const { default: GeneralSecurity } = await import(\n            \"@/pages/GeneralSettings/Security\"\n          );\n          return { element: <ManagerRoute Component={GeneralSecurity} /> };\n        },\n      },\n      {\n        path: \"/settings/privacy\",\n        lazy: async () => {\n          const { default: PrivacyAndData } = await import(\n            \"@/pages/GeneralSettings/PrivacyAndData\"\n          );\n          return { element: <AdminRoute Component={PrivacyAndData} /> };\n        },\n      },\n      {\n        path: \"/settings/interface\",\n        lazy: async () => {\n          const { default: InterfaceSettings } = await import(\n            \"@/pages/GeneralSettings/Settings/Interface\"\n          );\n          return { element: <ManagerRoute Component={InterfaceSettings} /> };\n        },\n      },\n      {\n        path: \"/settings/branding\",\n        lazy: async () => {\n          const { default: BrandingSettings } = await import(\n            \"@/pages/GeneralSettings/Settings/Branding\"\n          );\n          return { element: <ManagerRoute Component={BrandingSettings} /> };\n        },\n      },\n      {\n        path: \"/settings/default-system-prompt\",\n        lazy: async () => {\n          const { default: DefaultSystemPrompt } = await import(\n            \"@/pages/Admin/DefaultSystemPrompt\"\n          );\n          return { element: <AdminRoute Component={DefaultSystemPrompt} /> };\n        },\n      },\n      {\n        path: \"/settings/chat\",\n        lazy: async () => {\n          const { default: ChatSettings } = await import(\n            \"@/pages/GeneralSettings/Settings/Chat\"\n          );\n          return { element: <ManagerRoute Component={ChatSettings} /> };\n        },\n      },\n      {\n        path: \"/settings/beta-features\",\n        lazy: async () => {\n          const { default: ExperimentalFeatures } = await import(\n            \"@/pages/Admin/ExperimentalFeatures\"\n          );\n          return { element: <AdminRoute Component={ExperimentalFeatures} /> };\n        },\n      },\n      {\n        path: \"/settings/api-keys\",\n        lazy: async () => {\n          const { default: GeneralApiKeys } = await import(\n            \"@/pages/GeneralSettings/ApiKeys\"\n          );\n          return { element: <AdminRoute Component={GeneralApiKeys} /> };\n        },\n      },\n      {\n        path: \"/settings/system-prompt-variables\",\n        lazy: async () => {\n          const { default: SystemPromptVariables } = await import(\n            \"@/pages/Admin/SystemPromptVariables\"\n          );\n          return {\n            element: <AdminRoute Component={SystemPromptVariables} />,\n          };\n        },\n      },\n      {\n        path: \"/settings/browser-extension\",\n        lazy: async () => {\n          const { default: GeneralBrowserExtension } = await import(\n            \"@/pages/GeneralSettings/BrowserExtensionApiKey\"\n          );\n          return {\n            element: <ManagerRoute Component={GeneralBrowserExtension} />,\n          };\n        },\n      },\n      {\n        path: \"/settings/workspace-chats\",\n        lazy: async () => {\n          const { default: GeneralChats } = await import(\n            \"@/pages/GeneralSettings/Chats\"\n          );\n          return { element: <ManagerRoute Component={GeneralChats} /> };\n        },\n      },\n      {\n        path: \"/settings/invites\",\n        lazy: async () => {\n          const { default: AdminInvites } = await import(\n            \"@/pages/Admin/Invitations\"\n          );\n          return { element: <ManagerRoute Component={AdminInvites} /> };\n        },\n      },\n      {\n        path: \"/settings/users\",\n        lazy: async () => {\n          const { default: AdminUsers } = await import(\"@/pages/Admin/Users\");\n          return { element: <ManagerRoute Component={AdminUsers} /> };\n        },\n      },\n      {\n        path: \"/settings/workspaces\",\n        lazy: async () => {\n          const { default: AdminWorkspaces } = await import(\n            \"@/pages/Admin/Workspaces\"\n          );\n          return { element: <ManagerRoute Component={AdminWorkspaces} /> };\n        },\n      },\n      // Onboarding Flow\n      {\n        path: \"/onboarding\",\n        element: <OnboardingFlow />,\n      },\n      {\n        path: \"/onboarding/:step\",\n        element: <OnboardingFlow />,\n      },\n      // Experimental feature pages\n      {\n        path: \"/settings/beta-features/live-document-sync/manage\",\n        lazy: async () => {\n          const { default: LiveDocumentSyncManage } = await import(\n            \"@/pages/Admin/ExperimentalFeatures/Features/LiveSync/manage\"\n          );\n          return {\n            element: <AdminRoute Component={LiveDocumentSyncManage} />,\n          };\n        },\n      },\n      {\n        path: \"/settings/community-hub/trending\",\n        lazy: async () => {\n          const { default: CommunityHubTrending } = await import(\n            \"@/pages/GeneralSettings/CommunityHub/Trending\"\n          );\n          return { element: <AdminRoute Component={CommunityHubTrending} /> };\n        },\n      },\n      {\n        path: \"/settings/community-hub/authentication\",\n        lazy: async () => {\n          const { default: CommunityHubAuthentication } = await import(\n            \"@/pages/GeneralSettings/CommunityHub/Authentication\"\n          );\n          return {\n            element: <AdminRoute Component={CommunityHubAuthentication} />,\n          };\n        },\n      },\n      {\n        path: \"/settings/community-hub/import-item\",\n        lazy: async () => {\n          const { default: CommunityHubImportItem } = await import(\n            \"@/pages/GeneralSettings/CommunityHub/ImportItem\"\n          );\n          return {\n            element: <AdminRoute Component={CommunityHubImportItem} />,\n          };\n        },\n      },\n      {\n        path: \"/settings/mobile-connections\",\n        lazy: async () => {\n          const { default: MobileConnections } = await import(\n            \"@/pages/GeneralSettings/MobileConnections\"\n          );\n          return { element: <ManagerRoute Component={MobileConnections} /> };\n        },\n      },\n      // Catch-all route for 404s\n      {\n        path: \"*\",\n        lazy: async () => {\n          const { default: NotFound } = await import(\"@/pages/404\");\n          return { element: <NotFound /> };\n        },\n      },\n    ],\n  },\n]);\n\nReactDOM.createRoot(document.getElementById(\"root\")).render(\n  <REACTWRAP>\n    <RouterProvider router={router} />\n  </REACTWRAP>\n);\n"
  },
  {
    "path": "frontend/src/models/admin.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\n\nconst Admin = {\n  // User Management\n  users: async () => {\n    return await fetch(`${API_BASE}/admin/users`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .then((res) => res?.users || [])\n      .catch((e) => {\n        console.error(e);\n        return [];\n      });\n  },\n  newUser: async (data) => {\n    return await fetch(`${API_BASE}/admin/users/new`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(data),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { user: null, error: e.message };\n      });\n  },\n  updateUser: async (userId, data) => {\n    return await fetch(`${API_BASE}/admin/user/${userId}`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(data),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n  deleteUser: async (userId) => {\n    return await fetch(`${API_BASE}/admin/user/${userId}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n\n  // Invitations\n  invites: async () => {\n    return await fetch(`${API_BASE}/admin/invites`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .then((res) => res?.invites || [])\n      .catch((e) => {\n        console.error(e);\n        return [];\n      });\n  },\n  newInvite: async ({ role = null, workspaceIds = null }) => {\n    return await fetch(`${API_BASE}/admin/invite/new`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({\n        role,\n        workspaceIds,\n      }),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { invite: null, error: e.message };\n      });\n  },\n  disableInvite: async (inviteId) => {\n    return await fetch(`${API_BASE}/admin/invite/${inviteId}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n\n  // Workspaces Mgmt\n  workspaces: async () => {\n    return await fetch(`${API_BASE}/admin/workspaces`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .then((res) => res?.workspaces || [])\n      .catch((e) => {\n        console.error(e);\n        return [];\n      });\n  },\n  workspaceUsers: async (workspaceId) => {\n    return await fetch(`${API_BASE}/admin/workspaces/${workspaceId}/users`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .then((res) => res?.users || [])\n      .catch((e) => {\n        console.error(e);\n        return [];\n      });\n  },\n  newWorkspace: async (name) => {\n    return await fetch(`${API_BASE}/admin/workspaces/new`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ name }),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { workspace: null, error: e.message };\n      });\n  },\n  updateUsersInWorkspace: async (workspaceId, userIds = []) => {\n    return await fetch(\n      `${API_BASE}/admin/workspaces/${workspaceId}/update-users`,\n      {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({ userIds }),\n      }\n    )\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n  deleteWorkspace: async (workspaceId) => {\n    return await fetch(`${API_BASE}/admin/workspaces/${workspaceId}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n\n  // System Preferences\n  /**\n   * Fetches system preferences by fields\n   * @param {string[]} labels - Array of labels for settings\n   * @returns {Promise<{settings: Object, error: string}>} - System preferences object\n   */\n  systemPreferencesByFields: async (labels = []) => {\n    return await fetch(\n      `${API_BASE}/admin/system-preferences-for?labels=${labels.join(\",\")}`,\n      {\n        method: \"GET\",\n        headers: baseHeaders(),\n      }\n    )\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return null;\n      });\n  },\n  updateSystemPreferences: async (updates = {}) => {\n    return await fetch(`${API_BASE}/admin/system-preferences`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(updates),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n\n  // API Keys\n  getApiKeys: async function () {\n    return fetch(`${API_BASE}/admin/api-keys`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) {\n          throw new Error(res.statusText || \"Error fetching api keys.\");\n        }\n        return res.json();\n      })\n      .catch((e) => {\n        console.error(e);\n        return { apiKeys: [], error: e.message };\n      });\n  },\n  generateApiKey: async function () {\n    return fetch(`${API_BASE}/admin/generate-api-key`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) {\n          throw new Error(res.statusText || \"Error generating api key.\");\n        }\n        return res.json();\n      })\n      .catch((e) => {\n        console.error(e);\n        return { apiKey: null, error: e.message };\n      });\n  },\n  deleteApiKey: async function (apiKeyId = \"\") {\n    return fetch(`${API_BASE}/admin/delete-api-key/${apiKeyId}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.ok)\n      .catch((e) => {\n        console.error(e);\n        return false;\n      });\n  },\n};\n\nexport default Admin;\n"
  },
  {
    "path": "frontend/src/models/agentFlows.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\n\nconst AgentFlows = {\n  /**\n   * Save a flow configuration\n   * @param {string} name - Display name of the flow\n   * @param {object} config - The configuration object for the flow\n   * @param {string} [uuid] - Optional UUID for updating existing flow\n   * @returns {Promise<{success: boolean, error: string | null, flow: {name: string, config: object, uuid: string} | null}>}\n   */\n  saveFlow: async (name, config, uuid = null) => {\n    return await fetch(`${API_BASE}/agent-flows/save`, {\n      method: \"POST\",\n      headers: {\n        ...baseHeaders(),\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({ name, config, uuid }),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(res.error || \"Failed to save flow\");\n        return res;\n      })\n      .then((res) => res.json())\n      .catch((e) => ({\n        success: false,\n        error: e.message,\n        flow: null,\n      }));\n  },\n\n  /**\n   * List all available flows in the system\n   * @returns {Promise<{success: boolean, error: string | null, flows: Array<{name: string, uuid: string, description: string, steps: Array}>}>}\n   */\n  listFlows: async () => {\n    return await fetch(`${API_BASE}/agent-flows/list`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => ({\n        success: false,\n        error: e.message,\n        flows: [],\n      }));\n  },\n\n  /**\n   * Get a specific flow by UUID\n   * @param {string} uuid - The UUID of the flow to retrieve\n   * @returns {Promise<{success: boolean, error: string | null, flow: {name: string, config: object, uuid: string} | null}>}\n   */\n  getFlow: async (uuid) => {\n    return await fetch(`${API_BASE}/agent-flows/${uuid}`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(res.error || \"Failed to get flow\");\n        return res;\n      })\n      .then((res) => res.json())\n      .catch((e) => ({\n        success: false,\n        error: e.message,\n        flow: null,\n      }));\n  },\n\n  /**\n   * Execute a specific flow\n   * @param {string} uuid - The UUID of the flow to run\n   * @param {object} variables - Optional variables to pass to the flow\n   * @returns {Promise<{success: boolean, error: string | null, results: object | null}>}\n   */\n  // runFlow: async (uuid, variables = {}) => {\n  //   return await fetch(`${API_BASE}/agent-flows/${uuid}/run`, {\n  //     method: \"POST\",\n  //     headers: {\n  //       ...baseHeaders(),\n  //       \"Content-Type\": \"application/json\",\n  //     },\n  //     body: JSON.stringify({ variables }),\n  //   })\n  //     .then((res) => {\n  //       if (!res.ok) throw new Error(response.error || \"Failed to run flow\");\n  //       return res;\n  //     })\n  //     .then((res) => res.json())\n  //     .catch((e) => ({\n  //       success: false,\n  //       error: e.message,\n  //       results: null,\n  //     }));\n  // },\n\n  /**\n   * Delete a specific flow\n   * @param {string} uuid - The UUID of the flow to delete\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  deleteFlow: async (uuid) => {\n    return await fetch(`${API_BASE}/agent-flows/${uuid}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(res.error || \"Failed to delete flow\");\n        return res;\n      })\n      .then((res) => res.json())\n      .catch((e) => ({\n        success: false,\n        error: e.message,\n      }));\n  },\n\n  /**\n   * Toggle a flow's active status\n   * @param {string} uuid - The UUID of the flow to toggle\n   * @param {boolean} active - The new active status\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  toggleFlow: async (uuid, active) => {\n    try {\n      const result = await fetch(`${API_BASE}/agent-flows/${uuid}/toggle`, {\n        method: \"POST\",\n        headers: {\n          ...baseHeaders(),\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify({ active }),\n      })\n        .then((res) => {\n          if (!res.ok) throw new Error(res.error || \"Failed to toggle flow\");\n          return res;\n        })\n        .then((res) => res.json());\n      return { success: true, flow: result.flow };\n    } catch (error) {\n      console.error(\"Failed to toggle flow:\", error);\n      return { success: false, error: error.message };\n    }\n  },\n};\n\nexport default AgentFlows;\n"
  },
  {
    "path": "frontend/src/models/appearance.js",
    "content": "import { APPEARANCE_SETTINGS } from \"@/utils/constants\";\nimport { safeJsonParse } from \"@/utils/request\";\n\n/**\n * @typedef { 'showScrollbar' |\n * 'autoSubmitSttInput' |\n * 'autoPlayAssistantTtsResponse' |\n * 'enableSpellCheck' |\n * 'renderHTML'\n * } AvailableSettings - The supported settings for the appearance model.\n */\n\nconst Appearance = {\n  defaultSettings: {\n    showScrollbar: false,\n    autoSubmitSttInput: true,\n    autoPlayAssistantTtsResponse: false,\n    enableSpellCheck: true,\n    renderHTML: false,\n  },\n\n  /**\n   * Fetches any locally storage settings for the user\n   * @returns {{showScrollbar: boolean, autoSubmitSttInput: boolean, autoPlayAssistantTtsResponse: boolean, enableSpellCheck: boolean, renderHTML: boolean}}\n   */\n  getSettings: () => {\n    const settings = localStorage.getItem(APPEARANCE_SETTINGS);\n    return safeJsonParse(settings, Appearance.defaultSettings);\n  },\n\n  /**\n   * Fetches a specific setting from the user's settings\n   * @param {AvailableSettings} key - The key of the setting to fetch\n   * @returns {boolean} - a default value if the setting is not found or the current value\n   */\n  get: (key) => {\n    const settings = Appearance.getSettings();\n    return settings.hasOwnProperty(key)\n      ? settings[key]\n      : Appearance.defaultSettings[key];\n  },\n\n  /**\n   * Updates a specific setting from the user's settings\n   * @param {AvailableSettings} key - The key of the setting to update\n   * @param {any} value - The value to update the setting to\n   * @returns {object}\n   */\n  set: (key, value) => {\n    const settings = Appearance.getSettings();\n    settings[key] = value;\n    Appearance.updateSettings(settings);\n    return settings;\n  },\n\n  /**\n   * Updates locally stored user settings\n   * @param {object} newSettings - new settings to update.\n   * @returns {object}\n   */\n  updateSettings: (newSettings) => {\n    const updatedSettings = { ...Appearance.getSettings(), ...newSettings };\n    localStorage.setItem(APPEARANCE_SETTINGS, JSON.stringify(updatedSettings));\n    return updatedSettings;\n  },\n};\n\nexport default Appearance;\n"
  },
  {
    "path": "frontend/src/models/browserExtensionApiKey.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\n\nconst BrowserExtensionApiKey = {\n  getAll: async () => {\n    return await fetch(`${API_BASE}/browser-extension/api-keys`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message, apiKeys: [] };\n      });\n  },\n\n  generateKey: async () => {\n    return await fetch(`${API_BASE}/browser-extension/api-keys/new`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n\n  revoke: async (id) => {\n    return await fetch(`${API_BASE}/browser-extension/api-keys/${id}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n};\n\nexport default BrowserExtensionApiKey;\n"
  },
  {
    "path": "frontend/src/models/communityHub.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\n\nconst CommunityHub = {\n  /**\n   * Get an item from the community hub by its import ID.\n   * @param {string} importId - The import ID of the item.\n   * @returns {Promise<{error: string | null, item: object | null}>}\n   */\n  getItemFromImportId: async (importId) => {\n    return await fetch(`${API_BASE}/community-hub/item`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ importId }),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return {\n          error: e.message,\n          item: null,\n        };\n      });\n  },\n\n  /**\n   * Apply an item to the AnythingLLM instance. Used for simple items like slash commands and system prompts.\n   * @param {string} importId - The import ID of the item.\n   * @param {object} options - Additional options for applying the item for whatever the item type requires.\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  applyItem: async (importId, options = {}) => {\n    return await fetch(`${API_BASE}/community-hub/apply`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ importId, options }),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return {\n          success: false,\n          error: e.message,\n        };\n      });\n  },\n\n  /**\n   * Import a bundle item from the community hub.\n   * @param {string} importId - The import ID of the item.\n   * @returns {Promise<{error: string | null, item: object | null}>}\n   */\n  importBundleItem: async (importId) => {\n    return await fetch(`${API_BASE}/community-hub/import`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ importId }),\n    })\n      .then(async (res) => {\n        const response = await res.json();\n        if (!res.ok) throw new Error(response?.error ?? res.statusText);\n        return response;\n      })\n      .catch((e) => {\n        return {\n          error: e.message,\n          item: null,\n        };\n      });\n  },\n\n  /**\n   * Update the hub settings (API key, etc.)\n   * @param {Object} data - The data to update.\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  updateSettings: async (data) => {\n    return await fetch(`${API_BASE}/community-hub/settings`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(data),\n    })\n      .then(async (res) => {\n        const response = await res.json();\n        if (!res.ok)\n          throw new Error(response.error || \"Failed to update settings\");\n        return { success: true, error: null };\n      })\n      .catch((e) => ({\n        success: false,\n        error: e.message,\n      }));\n  },\n\n  /**\n   * Get the hub settings (API key, etc.)\n   * @returns {Promise<{connectionKey: string | null, error: string | null}>}\n   */\n  getSettings: async () => {\n    return await fetch(`${API_BASE}/community-hub/settings`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then(async (res) => {\n        const response = await res.json();\n        if (!res.ok)\n          throw new Error(response.error || \"Failed to fetch settings\");\n        return { connectionKey: response.connectionKey, error: null };\n      })\n      .catch((e) => ({\n        connectionKey: null,\n        error: e.message,\n      }));\n  },\n\n  /**\n   * Fetch the explore items from the community hub that are publicly available.\n   * @returns {Promise<{agentSkills: {items: [], hasMore: boolean, totalCount: number}, systemPrompts: {items: [], hasMore: boolean, totalCount: number}, slashCommands: {items: [], hasMore: boolean, totalCount: number}}>}\n   */\n  fetchExploreItems: async () => {\n    return await fetch(`${API_BASE}/community-hub/explore`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return {\n          success: false,\n          error: e.message,\n          result: null,\n        };\n      });\n  },\n\n  /**\n   * Fetch the user items from the community hub.\n   * @returns {Promise<{success: boolean, error: string | null, createdByMe: object, teamItems: object[]}>}\n   */\n  fetchUserItems: async () => {\n    return await fetch(`${API_BASE}/community-hub/items`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return {\n          success: false,\n          error: e.message,\n          createdByMe: {},\n          teamItems: [],\n        };\n      });\n  },\n\n  /**\n   * Create a new system prompt in the community hub\n   * @param {Object} data - The system prompt data\n   * @param {string} data.name - The name of the prompt\n   * @param {string} data.description - The description of the prompt\n   * @param {string} data.prompt - The actual system prompt text\n   * @param {string[]} data.tags - Array of tags\n   * @param {string} data.visibility - Either 'public' or 'private'\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  createSystemPrompt: async (data) => {\n    return await fetch(`${API_BASE}/community-hub/system-prompt/create`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(data),\n    })\n      .then(async (res) => {\n        const response = await res.json();\n        if (!res.ok)\n          throw new Error(response.error || \"Failed to create system prompt\");\n        return { success: true, error: null, itemId: response.item?.id };\n      })\n      .catch((e) => ({\n        success: false,\n        error: e.message,\n      }));\n  },\n\n  /**\n   * Create a new agent flow in the community hub\n   * @param {Object} data - The agent flow data\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  createAgentFlow: async (data) => {\n    return await fetch(`${API_BASE}/community-hub/agent-flow/create`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(data),\n    }).then(async (res) => {\n      const response = await res.json();\n      if (!res.ok)\n        throw new Error(response.error || \"Failed to create agent flow\");\n      return { success: true, error: null, itemId: response.item?.id };\n    });\n  },\n\n  /**\n   * Create a new slash command in the community hub\n   * @param {Object} data - The slash command data\n   * @param {string} data.name - The name of the command\n   * @param {string} data.description - The description of the command\n   * @param {string} data.command - The actual command text\n   * @param {string} data.prompt - The prompt for the command\n   * @param {string[]} data.tags - Array of tags\n   * @param {string} data.visibility - Either 'public' or 'private'\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  createSlashCommand: async (data) => {\n    return await fetch(`${API_BASE}/community-hub/slash-command/create`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(data),\n    })\n      .then(async (res) => {\n        const response = await res.json();\n        if (!res.ok)\n          throw new Error(response.error || \"Failed to create slash command\");\n        return { success: true, error: null, itemId: response.item?.id };\n      })\n      .catch((e) => ({\n        success: false,\n        error: e.message,\n      }));\n  },\n};\n\nexport default CommunityHub;\n"
  },
  {
    "path": "frontend/src/models/dataConnector.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\nimport showToast from \"@/utils/toast\";\n\nconst DataConnector = {\n  github: {\n    branches: async ({ repo, accessToken }) => {\n      return await fetch(`${API_BASE}/ext/github/branches`, {\n        method: \"POST\",\n        headers: baseHeaders(),\n        cache: \"force-cache\",\n        body: JSON.stringify({ repo, accessToken }),\n      })\n        .then((res) => res.json())\n        .then((res) => {\n          if (!res.success) throw new Error(res.reason);\n          return res.data;\n        })\n        .then((data) => {\n          return { branches: data?.branches || [], error: null };\n        })\n        .catch((e) => {\n          console.error(e);\n          showToast(e.message, \"error\");\n          return { branches: [], error: e.message };\n        });\n    },\n    collect: async function ({ repo, accessToken, branch, ignorePaths = [] }) {\n      return await fetch(`${API_BASE}/ext/github/repo`, {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({ repo, accessToken, branch, ignorePaths }),\n      })\n        .then((res) => res.json())\n        .then((res) => {\n          if (!res.success) throw new Error(res.reason);\n          return { data: res.data, error: null };\n        })\n        .catch((e) => {\n          console.error(e);\n          return { data: null, error: e.message };\n        });\n    },\n  },\n  gitlab: {\n    branches: async ({ repo, accessToken }) => {\n      return await fetch(`${API_BASE}/ext/gitlab/branches`, {\n        method: \"POST\",\n        headers: baseHeaders(),\n        cache: \"force-cache\",\n        body: JSON.stringify({ repo, accessToken }),\n      })\n        .then((res) => res.json())\n        .then((res) => {\n          if (!res.success) throw new Error(res.reason);\n          return res.data;\n        })\n        .then((data) => {\n          return { branches: data?.branches || [], error: null };\n        })\n        .catch((e) => {\n          console.error(e);\n          showToast(e.message, \"error\");\n          return { branches: [], error: e.message };\n        });\n    },\n    collect: async function ({\n      repo,\n      accessToken,\n      branch,\n      ignorePaths = [],\n      fetchIssues = false,\n      fetchWikis = false,\n    }) {\n      return await fetch(`${API_BASE}/ext/gitlab/repo`, {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({\n          repo,\n          accessToken,\n          branch,\n          ignorePaths,\n          fetchIssues,\n          fetchWikis,\n        }),\n      })\n        .then((res) => res.json())\n        .then((res) => {\n          if (!res.success) throw new Error(res.reason);\n          return { data: res.data, error: null };\n        })\n        .catch((e) => {\n          console.error(e);\n          return { data: null, error: e.message };\n        });\n    },\n  },\n  youtube: {\n    transcribe: async ({ url }) => {\n      return await fetch(`${API_BASE}/ext/youtube/transcript`, {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({ url }),\n      })\n        .then((res) => res.json())\n        .then((res) => {\n          if (!res.success) throw new Error(res.reason);\n          return { data: res.data, error: null };\n        })\n        .catch((e) => {\n          console.error(e);\n          return { data: null, error: e.message };\n        });\n    },\n  },\n  websiteDepth: {\n    scrape: async ({ url, depth, maxLinks }) => {\n      return await fetch(`${API_BASE}/ext/website-depth`, {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({ url, depth, maxLinks }),\n      })\n        .then((res) => res.json())\n        .then((res) => {\n          if (!res.success) throw new Error(res.reason);\n          return { data: res.data, error: null };\n        })\n        .catch((e) => {\n          console.error(e);\n          return { data: null, error: e.message };\n        });\n    },\n  },\n\n  confluence: {\n    collect: async function ({\n      baseUrl,\n      spaceKey,\n      username,\n      accessToken,\n      cloud,\n      personalAccessToken,\n      bypassSSL,\n    }) {\n      return await fetch(`${API_BASE}/ext/confluence`, {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({\n          baseUrl,\n          spaceKey,\n          username,\n          accessToken,\n          cloud,\n          personalAccessToken,\n          bypassSSL,\n        }),\n      })\n        .then((res) => res.json())\n        .then((res) => {\n          if (!res.success) throw new Error(res.reason);\n          return { data: res.data, error: null };\n        })\n        .catch((e) => {\n          console.error(e);\n          return { data: null, error: e.message };\n        });\n    },\n  },\n\n  drupalwiki: {\n    collect: async function ({ baseUrl, spaceIds, accessToken }) {\n      return await fetch(`${API_BASE}/ext/drupalwiki`, {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({\n          baseUrl,\n          spaceIds,\n          accessToken,\n        }),\n      })\n        .then((res) => res.json())\n        .then((res) => {\n          if (!res.success) throw new Error(res.reason);\n          return { data: res.data, error: null };\n        })\n        .catch((e) => {\n          console.error(e);\n          return { data: null, error: e.message };\n        });\n    },\n  },\n  obsidian: {\n    collect: async function ({ files }) {\n      return await fetch(`${API_BASE}/ext/obsidian/vault`, {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({\n          files,\n        }),\n      })\n        .then((res) => res.json())\n        .then((res) => {\n          if (!res.success) throw new Error(res.reason);\n          return { data: res.data, error: null };\n        })\n        .catch((e) => {\n          console.error(e);\n          return { data: null, error: e.message };\n        });\n    },\n  },\n\n  paperlessNgx: {\n    collect: async function ({ baseUrl, apiToken }) {\n      return await fetch(`${API_BASE}/ext/paperless-ngx`, {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({ baseUrl, apiToken }),\n      })\n        .then((res) => res.json())\n        .then((res) => {\n          if (!res.success) throw new Error(res.reason);\n          return { data: res.data, error: null };\n        })\n        .catch((e) => {\n          console.error(e);\n          return { data: null, error: e.message };\n        });\n    },\n  },\n};\n\nexport default DataConnector;\n"
  },
  {
    "path": "frontend/src/models/document.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\n\nconst Document = {\n  createFolder: async (name) => {\n    return await fetch(`${API_BASE}/document/create-folder`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ name }),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n  moveToFolder: async (files, folderName) => {\n    const data = {\n      files: files.map((file) => ({\n        from: file.folderName ? `${file.folderName}/${file.name}` : file.name,\n        to: `${folderName}/${file.name}`,\n      })),\n    };\n\n    return await fetch(`${API_BASE}/document/move-files`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(data),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n};\n\nexport default Document;\n"
  },
  {
    "path": "frontend/src/models/embed.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\n\nconst Embed = {\n  embeds: async () => {\n    return await fetch(`${API_BASE}/embeds`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .then((res) => res?.embeds || [])\n      .catch((e) => {\n        console.error(e);\n        return [];\n      });\n  },\n  newEmbed: async (data) => {\n    return await fetch(`${API_BASE}/embeds/new`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(data),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { embed: null, error: e.message };\n      });\n  },\n  updateEmbed: async (embedId, data) => {\n    return await fetch(`${API_BASE}/embed/update/${embedId}`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(data),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n  deleteEmbed: async (embedId) => {\n    return await fetch(`${API_BASE}/embed/${embedId}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (res.ok) return { success: true, error: null };\n        throw new Error(res.statusText);\n      })\n      .catch((e) => {\n        console.error(e);\n        return { success: true, error: e.message };\n      });\n  },\n  chats: async (offset = 0) => {\n    return await fetch(`${API_BASE}/embed/chats`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ offset }),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return [];\n      });\n  },\n  deleteChat: async (chatId) => {\n    return await fetch(`${API_BASE}/embed/chats/${chatId}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n};\n\nexport default Embed;\n"
  },
  {
    "path": "frontend/src/models/experimental/agentPlugins.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\n\nconst AgentPlugins = {\n  toggleFeature: async function (hubId, active = false) {\n    return await fetch(\n      `${API_BASE}/experimental/agent-plugins/${hubId}/toggle`,\n      {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({ active }),\n      }\n    )\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not update agent plugin status.\");\n        return true;\n      })\n      .catch((e) => {\n        console.error(e);\n        return false;\n      });\n  },\n  updatePluginConfig: async function (hubId, updates = {}) {\n    return await fetch(\n      `${API_BASE}/experimental/agent-plugins/${hubId}/config`,\n      {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({ updates }),\n      }\n    )\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not update agent plugin config.\");\n        return true;\n      })\n      .catch((e) => {\n        console.error(e);\n        return false;\n      });\n  },\n  deletePlugin: async function (hubId) {\n    return await fetch(`${API_BASE}/experimental/agent-plugins/${hubId}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not delete agent plugin config.\");\n        return true;\n      })\n      .catch((e) => {\n        console.error(e);\n        return false;\n      });\n  },\n};\n\nexport default AgentPlugins;\n"
  },
  {
    "path": "frontend/src/models/experimental/liveSync.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\n\nconst LiveDocumentSync = {\n  featureFlag: \"experimental_live_file_sync\",\n  toggleFeature: async function (updatedStatus = false) {\n    return await fetch(`${API_BASE}/experimental/toggle-live-sync`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ updatedStatus }),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not update status.\");\n        return true;\n      })\n      .then((res) => res)\n      .catch((e) => {\n        console.error(e);\n        return false;\n      });\n  },\n  queues: async function () {\n    return await fetch(`${API_BASE}/experimental/live-sync/queues`, {\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not update status.\");\n        return res.json();\n      })\n      .then((res) => res?.queues || [])\n      .catch((e) => {\n        console.error(e);\n        return [];\n      });\n  },\n\n  // Should be in Workspaces but is here for now while in preview\n  setWatchStatusForDocument: async function (slug, docPath, watchStatus) {\n    return fetch(`${API_BASE}/workspace/${slug}/update-watch-status`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ docPath, watchStatus }),\n    })\n      .then((res) => {\n        if (!res.ok) {\n          throw new Error(\n            res.statusText || \"Error setting watch status for document.\"\n          );\n        }\n        return true;\n      })\n      .catch((e) => {\n        console.error(e);\n        return false;\n      });\n  },\n};\n\nexport default LiveDocumentSync;\n"
  },
  {
    "path": "frontend/src/models/invite.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\n\nconst Invite = {\n  checkInvite: async (inviteCode) => {\n    return await fetch(`${API_BASE}/invite/${inviteCode}`, {\n      method: \"GET\",\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { invite: null, error: e.message };\n      });\n  },\n  acceptInvite: async (inviteCode, newUserInfo = {}) => {\n    return await fetch(`${API_BASE}/invite/${inviteCode}`, {\n      method: \"POST\",\n      body: JSON.stringify(newUserInfo),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n};\n\nexport default Invite;\n"
  },
  {
    "path": "frontend/src/models/mcpServers.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\n\nconst MCPServers = {\n  /**\n   * Forces a reload of the MCP Hypervisor and its servers\n   * @returns {Promise<{success: boolean, error: string | null, servers: Array<{name: string, running: boolean, tools: Array<{name: string, description: string, inputSchema: Object}>, error: string | null, process: {pid: number, cmd: string} | null}>}>}\n   */\n  forceReload: async () => {\n    return await fetch(`${API_BASE}/mcp-servers/force-reload`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => ({\n        servers: [],\n        success: false,\n        error: e.message,\n      }));\n  },\n\n  /**\n   * List all available MCP servers in the system\n   * @returns {Promise<{success: boolean, error: string | null, servers: Array<{name: string, running: boolean, tools: Array<{name: string, description: string, inputSchema: Object}>, error: string | null, process: {pid: number, cmd: string} | null}>}>}\n   */\n  listServers: async () => {\n    return await fetch(`${API_BASE}/mcp-servers/list`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => ({\n        success: false,\n        error: e.message,\n        servers: [],\n      }));\n  },\n\n  /**\n   * Toggle the MCP server (start or stop)\n   * @param {string} name - The name of the MCP server to toggle\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  toggleServer: async (name) => {\n    return await fetch(`${API_BASE}/mcp-servers/toggle`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ name }),\n    })\n      .then((res) => res.json())\n      .catch((e) => ({\n        success: false,\n        error: e.message,\n      }));\n  },\n\n  /**\n   * Delete the MCP server - will also remove it from the config file\n   * @param {string} name - The name of the MCP server to delete\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  deleteServer: async (name) => {\n    return await fetch(`${API_BASE}/mcp-servers/delete`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ name }),\n    })\n      .then((res) => res.json())\n      .catch((e) => ({\n        success: false,\n        error: e.message,\n      }));\n  },\n\n  /**\n   * Toggle a tool's suppression status for an MCP server\n   * @param {string} serverName - The name of the MCP server\n   * @param {string} toolName - The name of the tool to toggle\n   * @param {boolean} enabled - Whether the tool should be enabled (true) or suppressed (false)\n   * @returns {Promise<{success: boolean, error: string | null, suppressedTools: string[]}>}\n   */\n  toggleTool: async (serverName, toolName, enabled) => {\n    return await fetch(`${API_BASE}/mcp-servers/toggle-tool`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ serverName, toolName, enabled }),\n    })\n      .then((res) => res.json())\n      .catch((e) => ({\n        success: false,\n        error: e.message,\n        suppressedTools: [],\n      }));\n  },\n};\n\nexport default MCPServers;\n"
  },
  {
    "path": "frontend/src/models/mobile.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\n\n/**\n * @typedef {Object} MobileConnection\n * @property {string} id - The database ID of the device.\n * @property {string} deviceId - The device ID of the device.\n * @property {string} deviceOs - The operating system of the device.\n * @property {boolean} approved - Whether the device is approved.\n * @property {string} createdAt - The date and time the device was created.\n */\n\nconst MobileConnection = {\n  /**\n   * Get the connection info for the mobile app.\n   * @returns {Promise<{connectionUrl: string|null}>} The connection info.\n   */\n  getConnectionInfo: async function () {\n    return await fetch(`${API_BASE}/mobile/connect-info`, {\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch(() => false);\n  },\n\n  /**\n   * Get all the devices from the database.\n   * @returns {Promise<MobileDevice[]>} The devices.\n   */\n  getDevices: async function () {\n    return await fetch(`${API_BASE}/mobile/devices`, {\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .then((res) => res.devices || [])\n      .catch(() => []);\n  },\n\n  /**\n   * Delete a device from the database.\n   * @param {string} deviceId - The database ID of the device to delete.\n   * @returns {Promise<{message: string}>} The deleted device.\n   */\n  deleteDevice: async function (id) {\n    return await fetch(`${API_BASE}/mobile/${id}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch(() => false);\n  },\n\n  /**\n   * Update a device in the database.\n   * @param {string} id - The database ID of the device to update.\n   * @param {Object} updates - The updates to apply to the device.\n   * @returns {Promise<{updates: MobileDevice}>} The updated device.\n   */\n  updateDevice: async function (id, updates = {}) {\n    return await fetch(`${API_BASE}/mobile/update/${id}`, {\n      method: \"POST\",\n      body: JSON.stringify(updates),\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch(() => false);\n  },\n};\n\nexport default MobileConnection;\n"
  },
  {
    "path": "frontend/src/models/promptHistory.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\n\n/**\n * @typedef {Object} PromptHistory\n * @property {number} id - The ID of the prompt history entry\n * @property {number} workspaceId - The ID of the workspace\n * @property {string} prompt - The prompt text\n * @property {number|null} modifiedBy - The ID of the user who modified the prompt\n * @property {Date} modifiedAt - The date when the prompt was modified\n * @property {Object|null} user - The user who modified the prompt\n */\n\nconst PromptHistory = {\n  /**\n   * Get all prompt history for a workspace\n   * @param {number} workspaceId - The ID of the workspace\n   * @returns {Promise<PromptHistory[]>} - An array of prompt history entries\n   */\n  forWorkspace: async function (workspaceId) {\n    try {\n      return await fetch(\n        `${API_BASE}/workspace/${workspaceId}/prompt-history`,\n        {\n          method: \"GET\",\n          headers: baseHeaders(),\n        }\n      )\n        .then((res) => res.json())\n        .then((res) => res.history || [])\n        .catch((error) => {\n          console.error(\"Error fetching prompt history:\", error);\n          return [];\n        });\n    } catch (error) {\n      console.error(\"Error fetching prompt history:\", error);\n      return [];\n    }\n  },\n\n  /**\n   * Delete all prompt history for a workspace\n   * @param {number} workspaceId - The ID of the workspace\n   * @returns {Promise<{success: boolean, error: string}>} - A promise that resolves to an object containing a success flag and an error message\n   */\n  clearAll: async function (workspaceId) {\n    try {\n      return await fetch(\n        `${API_BASE}/workspace/${workspaceId}/prompt-history`,\n        {\n          method: \"DELETE\",\n          headers: baseHeaders(),\n        }\n      )\n        .then((res) => res.json())\n        .catch((error) => {\n          console.error(\"Error clearing prompt history:\", error);\n          return { success: false, error };\n        });\n    } catch (error) {\n      console.error(\"Error clearing prompt history:\", error);\n      return { success: false, error };\n    }\n  },\n\n  delete: async function (id) {\n    try {\n      return await fetch(`${API_BASE}/workspace/prompt-history/${id}`, {\n        method: \"DELETE\",\n        headers: baseHeaders(),\n      })\n        .then((res) => res.json())\n        .catch((error) => {\n          console.error(\"Error deleting prompt history:\", error);\n          return { success: false, error };\n        });\n    } catch (error) {\n      console.error(\"Error deleting prompt history:\", error);\n      return { success: false, error };\n    }\n  },\n};\n\nexport default PromptHistory;\n"
  },
  {
    "path": "frontend/src/models/system.js",
    "content": "import { API_BASE, AUTH_TIMESTAMP, fullApiUrl } from \"@/utils/constants\";\nimport { baseHeaders, safeJsonParse } from \"@/utils/request\";\nimport DataConnector from \"./dataConnector\";\nimport LiveDocumentSync from \"./experimental/liveSync\";\nimport AgentPlugins from \"./experimental/agentPlugins\";\nimport SystemPromptVariable from \"./systemPromptVariable\";\n\nconst System = {\n  cacheKeys: {\n    footerIcons: \"anythingllm_footer_links\",\n    supportEmail: \"anythingllm_support_email\",\n    customAppName: \"anythingllm_custom_app_name\",\n    canViewChatHistory: \"anythingllm_can_view_chat_history\",\n    deploymentVersion: \"anythingllm_deployment_version\",\n  },\n  ping: async function () {\n    return await fetch(`${API_BASE}/ping`)\n      .then((res) => res.json())\n      .then((res) => res?.online || false)\n      .catch(() => false);\n  },\n  totalIndexes: async function (slug = null) {\n    const url = new URL(`${fullApiUrl()}/system/system-vectors`);\n    if (!!slug) url.searchParams.append(\"slug\", encodeURIComponent(slug));\n    return await fetch(url.toString(), {\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not find indexes.\");\n        return res.json();\n      })\n      .then((res) => res.vectorCount)\n      .catch(() => 0);\n  },\n\n  /**\n   * Checks if the onboarding is complete.\n   * @returns {Promise<boolean>}\n   */\n  isOnboardingComplete: async function () {\n    return await fetch(`${API_BASE}/onboarding`)\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not find onboarding information.\");\n        return res.json();\n      })\n      .then((res) => res.onboardingComplete)\n      .catch(() => false);\n  },\n  /**\n   * Marks the onboarding as complete.\n   * @returns {Promise<boolean>}\n   */\n  markOnboardingComplete: async function () {\n    return await fetch(`${API_BASE}/onboarding`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.ok)\n      .catch(() => false);\n  },\n  keys: async function () {\n    return await fetch(`${API_BASE}/setup-complete`)\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not find setup information.\");\n        return res.json();\n      })\n      .then((res) => res.results)\n      .catch(() => null);\n  },\n  localFiles: async function () {\n    return await fetch(`${API_BASE}/system/local-files`, {\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not find setup information.\");\n        return res.json();\n      })\n      .then((res) => res.localFiles)\n      .catch(() => null);\n  },\n  needsAuthCheck: function () {\n    const lastAuthCheck = window.localStorage.getItem(AUTH_TIMESTAMP);\n    if (!lastAuthCheck) return true;\n    const expiresAtMs = Number(lastAuthCheck) + 60 * 5 * 1000; // expires in 5 minutes in ms\n    return Number(new Date()) > expiresAtMs;\n  },\n\n  checkAuth: async function (currentToken = null) {\n    const valid = await fetch(`${API_BASE}/system/check-token`, {\n      headers: baseHeaders(currentToken),\n    })\n      .then((res) => res.ok)\n      .catch(() => false);\n\n    window.localStorage.setItem(AUTH_TIMESTAMP, Number(new Date()));\n    return valid;\n  },\n  requestToken: async function (body) {\n    return await fetch(`${API_BASE}/request-token`, {\n      method: \"POST\",\n      body: JSON.stringify({ ...body }),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not validate login.\");\n        return res.json();\n      })\n      .then((res) => res)\n      .catch((e) => {\n        return { valid: false, message: e.message };\n      });\n  },\n  /**\n   * Refreshes the user object from the session.\n   * @returns {Promise<{success: boolean, user: Object | null, message: string | null}>}\n   */\n  refreshUser: () => {\n    return fetch(`${API_BASE}/system/refresh-user`, {\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not refresh user.\");\n        return res.json();\n      })\n      .catch((e) => {\n        return { success: false, user: null, message: e.message };\n      });\n  },\n  recoverAccount: async function (username, recoveryCodes) {\n    return await fetch(`${API_BASE}/system/recover-account`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ username, recoveryCodes }),\n    })\n      .then(async (res) => {\n        const data = await res.json();\n        if (!res.ok) {\n          throw new Error(data.message || \"Error recovering account.\");\n        }\n        return data;\n      })\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n  resetPassword: async function (token, newPassword, confirmPassword) {\n    return await fetch(`${API_BASE}/system/reset-password`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ token, newPassword, confirmPassword }),\n    })\n      .then(async (res) => {\n        const data = await res.json();\n        if (!res.ok) {\n          throw new Error(data.message || \"Error resetting password.\");\n        }\n        return data;\n      })\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n\n  checkDocumentProcessorOnline: async () => {\n    return await fetch(`${API_BASE}/system/document-processing-status`, {\n      headers: baseHeaders(),\n    })\n      .then((res) => res.ok)\n      .catch(() => false);\n  },\n  acceptedDocumentTypes: async () => {\n    return await fetch(`${API_BASE}/system/accepted-document-types`, {\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .then((res) => res?.types)\n      .catch(() => null);\n  },\n  updateSystem: async (data) => {\n    return await fetch(`${API_BASE}/system/update-env`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(data),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { newValues: null, error: e.message };\n      });\n  },\n  updateSystemPassword: async (data) => {\n    return await fetch(`${API_BASE}/system/update-password`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(data),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n  setupMultiUser: async (data) => {\n    return await fetch(`${API_BASE}/system/enable-multi-user`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(data),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n  isMultiUserMode: async () => {\n    return await fetch(`${API_BASE}/system/multi-user-mode`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .then((res) => res?.multiUserMode)\n      .catch((e) => {\n        console.error(e);\n        return false;\n      });\n  },\n  deleteDocument: async (name) => {\n    return await fetch(`${API_BASE}/system/remove-document`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ name }),\n    })\n      .then((res) => res.ok)\n      .catch((e) => {\n        console.error(e);\n        return false;\n      });\n  },\n  deleteDocuments: async (names = []) => {\n    return await fetch(`${API_BASE}/system/remove-documents`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ names }),\n    })\n      .then((res) => res.ok)\n      .catch((e) => {\n        console.error(e);\n        return false;\n      });\n  },\n  deleteFolder: async (name) => {\n    return await fetch(`${API_BASE}/system/remove-folder`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ name }),\n    })\n      .then((res) => res.ok)\n      .catch((e) => {\n        console.error(e);\n        return false;\n      });\n  },\n  uploadPfp: async function (formData) {\n    return await fetch(`${API_BASE}/system/upload-pfp`, {\n      method: \"POST\",\n      body: formData,\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Error uploading pfp.\");\n        return { success: true, error: null };\n      })\n      .catch((e) => {\n        console.log(e);\n        return { success: false, error: e.message };\n      });\n  },\n  uploadLogo: async function (formData) {\n    return await fetch(`${API_BASE}/system/upload-logo`, {\n      method: \"POST\",\n      body: formData,\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Error uploading logo.\");\n        return { success: true, error: null };\n      })\n      .catch((e) => {\n        console.log(e);\n        return { success: false, error: e.message };\n      });\n  },\n  fetchCustomFooterIcons: async function () {\n    const cache = window.localStorage.getItem(this.cacheKeys.footerIcons);\n    const { data, lastFetched } = cache\n      ? safeJsonParse(cache, { data: [], lastFetched: 0 })\n      : { data: [], lastFetched: 0 };\n\n    if (!!data && Date.now() - lastFetched < 3_600_000)\n      return { footerData: data, error: null };\n\n    const { footerData, error } = await fetch(\n      `${API_BASE}/system/footer-data`,\n      {\n        method: \"GET\",\n        cache: \"no-cache\",\n        headers: baseHeaders(),\n      }\n    )\n      .then((res) => res.json())\n      .catch((e) => {\n        console.log(e);\n        return { footerData: [], error: e.message };\n      });\n\n    if (!footerData || !!error) return { footerData: [], error: null };\n\n    const newData = safeJsonParse(footerData, []);\n    window.localStorage.setItem(\n      this.cacheKeys.footerIcons,\n      JSON.stringify({ data: newData, lastFetched: Date.now() })\n    );\n    return { footerData: newData, error: null };\n  },\n  fetchSupportEmail: async function () {\n    const cache = window.localStorage.getItem(this.cacheKeys.supportEmail);\n    const { email, lastFetched } = cache\n      ? safeJsonParse(cache, { email: \"\", lastFetched: 0 })\n      : { email: \"\", lastFetched: 0 };\n\n    if (!!email && Date.now() - lastFetched < 3_600_000)\n      return { email: email, error: null };\n\n    const { supportEmail, error } = await fetch(\n      `${API_BASE}/system/support-email`,\n      {\n        method: \"GET\",\n        cache: \"no-cache\",\n        headers: baseHeaders(),\n      }\n    )\n      .then((res) => res.json())\n      .catch((e) => {\n        console.log(e);\n        return { email: \"\", error: e.message };\n      });\n\n    if (!supportEmail || !!error) return { email: \"\", error: null };\n    window.localStorage.setItem(\n      this.cacheKeys.supportEmail,\n      JSON.stringify({ email: supportEmail, lastFetched: Date.now() })\n    );\n    return { email: supportEmail, error: null };\n  },\n\n  fetchCustomAppName: async function () {\n    const cache = window.localStorage.getItem(this.cacheKeys.customAppName);\n    const { appName, lastFetched } = cache\n      ? safeJsonParse(cache, { appName: \"\", lastFetched: 0 })\n      : { appName: \"\", lastFetched: 0 };\n\n    if (!!appName && Date.now() - lastFetched < 3_600_000)\n      return { appName: appName, error: null };\n\n    const { customAppName, error } = await fetch(\n      `${API_BASE}/system/custom-app-name`,\n      {\n        method: \"GET\",\n        cache: \"no-cache\",\n        headers: baseHeaders(),\n      }\n    )\n      .then((res) => res.json())\n      .catch((e) => {\n        console.log(e);\n        return { customAppName: \"\", error: e.message };\n      });\n\n    if (!customAppName || !!error) {\n      window.localStorage.removeItem(this.cacheKeys.customAppName);\n      return { appName: \"\", error: null };\n    }\n\n    window.localStorage.setItem(\n      this.cacheKeys.customAppName,\n      JSON.stringify({ appName: customAppName, lastFetched: Date.now() })\n    );\n    return { appName: customAppName, error: null };\n  },\n  /**\n   * Fetches the default system prompt from the server.\n   * @returns {Promise<{defaultSystemPrompt: string, saneDefaultSystemPrompt: string}>}\n   */\n  fetchDefaultSystemPrompt: async function () {\n    return await fetch(`${API_BASE}/system/default-system-prompt`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .then((res) => ({\n        defaultSystemPrompt: res.defaultSystemPrompt,\n        saneDefaultSystemPrompt: res.saneDefaultSystemPrompt,\n      }))\n      .catch((e) => {\n        console.error(e);\n        return { defaultSystemPrompt: \"\", saneDefaultSystemPrompt: \"\" };\n      });\n  },\n  updateDefaultSystemPrompt: async function (defaultSystemPrompt) {\n    try {\n      const res = await fetch(`${API_BASE}/system/default-system-prompt`, {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({ defaultSystemPrompt }),\n      });\n      const data = await res.json();\n      return data;\n    } catch (e) {\n      console.error(e);\n      return { success: false, message: e.message };\n    }\n  },\n  fetchLogo: async function () {\n    const url = new URL(`${fullApiUrl()}/system/logo`);\n    url.searchParams.append(\n      \"theme\",\n      localStorage.getItem(\"theme\") || \"default\"\n    );\n\n    return await fetch(url, {\n      method: \"GET\",\n      cache: \"no-cache\",\n    })\n      .then(async (res) => {\n        if (res.ok && res.status !== 204) {\n          const isCustomLogo = res.headers.get(\"X-Is-Custom-Logo\") === \"true\";\n          const blob = await res.blob();\n          const logoURL = URL.createObjectURL(blob);\n          return { isCustomLogo, logoURL };\n        }\n        throw new Error(\"Failed to fetch logo!\");\n      })\n      .catch((e) => {\n        console.log(e);\n        return { isCustomLogo: false, logoURL: null };\n      });\n  },\n  fetchPfp: async function (id) {\n    return await fetch(`${API_BASE}/system/pfp/${id}`, {\n      method: \"GET\",\n      cache: \"no-cache\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (res.ok && res.status !== 204) return res.blob();\n        throw new Error(\"Failed to fetch pfp.\");\n      })\n      .then((blob) => (blob ? URL.createObjectURL(blob) : null))\n      .catch(() => {\n        return null;\n      });\n  },\n  removePfp: async function () {\n    return await fetch(`${API_BASE}/system/remove-pfp`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (res.ok) return { success: true, error: null };\n        throw new Error(\"Failed to remove pfp.\");\n      })\n      .catch((e) => {\n        console.log(e);\n        return { success: false, error: e.message };\n      });\n  },\n\n  isDefaultLogo: async function () {\n    return await fetch(`${API_BASE}/system/is-default-logo`, {\n      method: \"GET\",\n      cache: \"no-cache\",\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Failed to get is default logo!\");\n        return res.json();\n      })\n      .then((res) => res?.isDefaultLogo)\n      .catch((e) => {\n        console.log(e);\n        return null;\n      });\n  },\n  removeCustomLogo: async function () {\n    return await fetch(`${API_BASE}/system/remove-logo`, {\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (res.ok) return { success: true, error: null };\n        throw new Error(\"Error removing logo!\");\n      })\n      .catch((e) => {\n        console.log(e);\n        return { success: false, error: e.message };\n      });\n  },\n  getApiKeys: async function () {\n    return fetch(`${API_BASE}/system/api-keys`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) {\n          throw new Error(res.statusText || \"Error fetching api key.\");\n        }\n        return res.json();\n      })\n      .catch((e) => {\n        console.error(e);\n        return { apiKey: null, error: e.message };\n      });\n  },\n  generateApiKey: async function () {\n    return fetch(`${API_BASE}/system/generate-api-key`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) {\n          throw new Error(res.statusText || \"Error generating api key.\");\n        }\n        return res.json();\n      })\n      .catch((e) => {\n        console.error(e);\n        return { apiKey: null, error: e.message };\n      });\n  },\n  deleteApiKey: async function (apiKeyId = \"\") {\n    return fetch(`${API_BASE}/system/api-key/${apiKeyId}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.ok)\n      .catch((e) => {\n        console.error(e);\n        return false;\n      });\n  },\n  customModels: async function (\n    provider,\n    apiKey = null,\n    basePath = null,\n    timeout = null\n  ) {\n    const controller = new AbortController();\n    if (!!timeout) {\n      setTimeout(() => {\n        controller.abort(\"Request timed out.\");\n      }, timeout);\n    }\n\n    return fetch(`${API_BASE}/system/custom-models`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      signal: controller.signal,\n      body: JSON.stringify({\n        provider,\n        apiKey,\n        basePath,\n      }),\n    })\n      .then((res) => {\n        if (!res.ok) {\n          throw new Error(res.statusText || \"Error finding custom models.\");\n        }\n        return res.json();\n      })\n      .catch((e) => {\n        console.error(e);\n        return { models: [], error: e.message };\n      });\n  },\n  chats: async (offset = 0) => {\n    return await fetch(`${API_BASE}/system/workspace-chats`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ offset }),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return [];\n      });\n  },\n  eventLogs: async (offset = 0) => {\n    return await fetch(`${API_BASE}/system/event-logs`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ offset }),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return [];\n      });\n  },\n  clearEventLogs: async () => {\n    return await fetch(`${API_BASE}/system/event-logs`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n  deleteChat: async (chatId) => {\n    return await fetch(`${API_BASE}/system/workspace-chats/${chatId}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n  exportChats: async (type = \"csv\", chatType = \"workspace\") => {\n    const url = new URL(`${fullApiUrl()}/system/export-chats`);\n    url.searchParams.append(\"type\", encodeURIComponent(type));\n    url.searchParams.append(\"chatType\", encodeURIComponent(chatType));\n    return await fetch(url, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (res.ok) return res.text();\n        throw new Error(res.statusText);\n      })\n      .catch((e) => {\n        console.error(e);\n        return null;\n      });\n  },\n  updateUser: async (data) => {\n    return await fetch(`${API_BASE}/system/user`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(data),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n  dataConnectors: DataConnector,\n\n  getSlashCommandPresets: async function () {\n    return await fetch(`${API_BASE}/system/slash-command-presets`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not fetch slash command presets.\");\n        return res.json();\n      })\n      .then((res) => res.presets)\n      .catch((e) => {\n        console.error(e);\n        return [];\n      });\n  },\n\n  createSlashCommandPreset: async function (presetData) {\n    return await fetch(`${API_BASE}/system/slash-command-presets`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(presetData),\n    })\n      .then(async (res) => {\n        const data = await res.json();\n        if (!res.ok)\n          throw new Error(\n            data.message || \"Error creating slash command preset.\"\n          );\n        return data;\n      })\n      .then((res) => ({ preset: res.preset, error: null }))\n      .catch((e) => {\n        console.error(e);\n        return { preset: null, error: e.message };\n      });\n  },\n\n  updateSlashCommandPreset: async function (presetId, presetData) {\n    return await fetch(`${API_BASE}/system/slash-command-presets/${presetId}`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify(presetData),\n    })\n      .then(async (res) => {\n        const data = await res.json();\n        if (!res.ok)\n          throw new Error(\n            data.message || \"Could not update slash command preset.\"\n          );\n        return data;\n      })\n      .then((res) => ({ preset: res.preset, error: null }))\n      .catch((e) => {\n        console.error(e);\n        return { preset: null, error: e.message };\n      });\n  },\n\n  deleteSlashCommandPreset: async function (presetId) {\n    return await fetch(`${API_BASE}/system/slash-command-presets/${presetId}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not delete slash command preset.\");\n        return true;\n      })\n      .catch((e) => {\n        console.error(e);\n        return false;\n      });\n  },\n\n  /**\n   * Fetches the can view chat history state from local storage or the system settings.\n   * Notice: This is an instance setting that cannot be changed via the UI and it is cached\n   * in local storage for 24 hours.\n   * @returns {Promise<{viewable: boolean, error: string | null}>}\n   */\n  fetchCanViewChatHistory: async function () {\n    const cache = window.localStorage.getItem(\n      this.cacheKeys.canViewChatHistory\n    );\n    const { viewable, lastFetched } = cache\n      ? safeJsonParse(cache, { viewable: false, lastFetched: 0 })\n      : { viewable: false, lastFetched: 0 };\n\n    // Since this is an instance setting that cannot be changed via the UI,\n    // we can cache it in local storage for a day and if the admin changes it,\n    // they should instruct the users to clear local storage.\n    if (typeof viewable === \"boolean\" && Date.now() - lastFetched < 8.64e7)\n      return { viewable, error: null };\n\n    const res = await System.keys();\n    const isViewable = res?.DisableViewChatHistory === false;\n\n    window.localStorage.setItem(\n      this.cacheKeys.canViewChatHistory,\n      JSON.stringify({ viewable: isViewable, lastFetched: Date.now() })\n    );\n    return { viewable: isViewable, error: null };\n  },\n\n  /**\n   * Validates a temporary auth token and logs in the user if the token is valid.\n   * @param {string} publicToken - the token to validate against\n   * @returns {Promise<{valid: boolean, user: import(\"@prisma/client\").users | null, token: string | null, message: string | null}>}\n   */\n  simpleSSOLogin: async function (publicToken) {\n    return fetch(`${API_BASE}/request-token/sso/simple?token=${publicToken}`, {\n      method: \"GET\",\n    })\n      .then(async (res) => {\n        if (!res.ok) {\n          const text = await res.text();\n          if (!text.startsWith(\"{\")) throw new Error(text);\n          return JSON.parse(text);\n        }\n        return await res.json();\n      })\n      .catch((e) => {\n        console.error(e);\n        return { valid: false, user: null, token: null, message: e.message };\n      });\n  },\n\n  /**\n   * Fetches the app version from the server.\n   * @returns {Promise<string | null>} The app version.\n   */\n  fetchAppVersion: async function () {\n    const cache = window.localStorage.getItem(this.cacheKeys.deploymentVersion);\n    const { version, lastFetched } = cache\n      ? safeJsonParse(cache, { version: null, lastFetched: 0 })\n      : { version: null, lastFetched: 0 };\n\n    if (!!version && Date.now() - lastFetched < 3_600_000) return version;\n    const newVersion = await fetch(`${API_BASE}/utils/metrics`, {\n      method: \"GET\",\n      cache: \"no-cache\",\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not fetch app version.\");\n        return res.json();\n      })\n      .then((res) => res?.appVersion)\n      .catch(() => null);\n\n    if (!newVersion) return null;\n    window.localStorage.setItem(\n      this.cacheKeys.deploymentVersion,\n      JSON.stringify({ version: newVersion, lastFetched: Date.now() })\n    );\n    return newVersion;\n  },\n\n  /**\n   * Validates a SQL connection string.\n   * @param {'postgresql'|'mysql'|'sql-server'} engine - the database engine identifier\n   * @param {string} connectionString - the connection string to validate\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  validateSQLConnection: async function (engine, connectionString) {\n    return fetch(`${API_BASE}/system/validate-sql-connection`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ engine, connectionString }),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(\"Failed to validate SQL connection:\", e);\n        return { success: false, error: e.message };\n      });\n  },\n\n  experimentalFeatures: {\n    liveSync: LiveDocumentSync,\n    agentPlugins: AgentPlugins,\n  },\n  promptVariables: SystemPromptVariable,\n};\n\nexport default System;\n"
  },
  {
    "path": "frontend/src/models/systemPromptVariable.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\n\n/**\n * @typedef {Object} SystemPromptVariable\n * @property {number|null} id - The ID of the system prompt variable\n * @property {string} key - The key of the system prompt variable\n * @property {string} value - The value of the system prompt variable\n * @property {string} description - The description of the system prompt variable\n * @property {string} type - The type of the system prompt variable\n */\n\nconst SystemPromptVariable = {\n  /**\n   * Get all system prompt variables\n   * @returns {Promise<{variables: SystemPromptVariable[]}>} - An array of system prompt variables\n   */\n  getAll: async function () {\n    try {\n      return await fetch(`${API_BASE}/system/prompt-variables`, {\n        method: \"GET\",\n        headers: baseHeaders(),\n      })\n        .then((res) => res.json())\n        .catch((error) => {\n          console.error(\"Error fetching system prompt variables:\", error);\n          return { variables: [] };\n        });\n    } catch (error) {\n      console.error(\"Error fetching system prompt variables:\", error);\n      return { variables: [] };\n    }\n  },\n\n  /**\n   * Create a new system prompt variable\n   * @param {SystemPromptVariable} variable - The system prompt variable to create\n   * @returns {Promise<{success: boolean, error: string}>} - A promise that resolves to an object containing a success flag and an error message\n   */\n  create: async function (variable = {}) {\n    try {\n      return await fetch(`${API_BASE}/system/prompt-variables`, {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify(variable),\n      })\n        .then((res) => res.json())\n        .catch((error) => {\n          console.error(\"Error creating system prompt variable:\", error);\n          return { success: false, error };\n        });\n    } catch (error) {\n      console.error(\"Error creating system prompt variable:\", error);\n      return { success: false, error };\n    }\n  },\n\n  /**\n   * Update a system prompt variable\n   * @param {string} id - The ID of the system prompt variable to update\n   * @param {SystemPromptVariable} variable - The system prompt variable to update\n   * @returns {Promise<{success: boolean, error: string}>} - A promise that resolves to an object containing a success flag and an error message\n   */\n  update: async function (id, variable = {}) {\n    try {\n      return await fetch(`${API_BASE}/system/prompt-variables/${id}`, {\n        method: \"PUT\",\n        headers: baseHeaders(),\n        body: JSON.stringify(variable),\n      })\n        .then((res) => res.json())\n        .catch((error) => {\n          console.error(\"Error updating system prompt variable:\", error);\n          return { success: false, error };\n        });\n    } catch (error) {\n      console.error(\"Error updating system prompt variable:\", error);\n      return { success: false, error };\n    }\n  },\n\n  /**\n   * Delete a system prompt variable\n   * @param {string} id - The ID of the system prompt variable to delete\n   * @returns {Promise<{success: boolean, error: string}>} - A promise that resolves to an object containing a success flag and an error message\n   */\n  delete: async function (id = null) {\n    try {\n      if (id === null) return { success: false, error: \"ID is required\" };\n      return await fetch(`${API_BASE}/system/prompt-variables/${id}`, {\n        method: \"DELETE\",\n        headers: baseHeaders(),\n      })\n        .then((res) => res.json())\n        .catch((error) => {\n          console.error(\"Error deleting system prompt variable:\", error);\n          return { success: false, error };\n        });\n    } catch (error) {\n      console.error(\"Error deleting system prompt variable:\", error);\n      return { success: false, error };\n    }\n  },\n};\n\nexport default SystemPromptVariable;\n"
  },
  {
    "path": "frontend/src/models/utils/dmrUtils.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\nimport { safeJsonParse } from \"@/utils/request\";\n\nconst DMRUtils = {\n  /**\n   * Download a DMR model.\n   * @param {string} modelId - The ID of the model to download.\n   * @param {(percentage: number) => void} progressCallback - The callback to receive the progress percentage. If the model is already downloaded, it will be called once with 100.\n   * @returns {Promise<{success: boolean, error: string|null}>}\n   */\n  downloadModel: async function (\n    modelId,\n    basePath = \"\",\n    progressCallback = () => {}\n  ) {\n    // eslint-disable-next-line no-async-promise-executor\n    return new Promise(async (resolve) => {\n      try {\n        const response = await fetch(`${API_BASE}/utils/dmr/download-model`, {\n          method: \"POST\",\n          headers: baseHeaders(),\n          body: JSON.stringify({ modelId, basePath }),\n        });\n\n        if (!response.ok)\n          throw new Error(\"Error downloading model: \" + response.statusText);\n        const reader = response.body.getReader();\n        let done = false;\n\n        while (!done) {\n          const { value, done: readerDone } = await reader.read();\n          if (readerDone) {\n            done = true;\n            resolve({ success: true });\n          } else {\n            const chunk = new TextDecoder(\"utf-8\").decode(value);\n            const lines = chunk.split(\"\\n\");\n            for (const line of lines) {\n              if (line.startsWith(\"data:\")) {\n                const data = safeJsonParse(line.slice(5));\n                switch (data?.type) {\n                  case \"success\":\n                    done = true;\n                    resolve({ success: true });\n                    break;\n                  case \"error\":\n                    done = true;\n                    resolve({\n                      success: false,\n                      error: data?.error || data?.message,\n                    });\n                    break;\n                  case \"progress\":\n                    progressCallback(data?.percentage);\n                    break;\n                  default:\n                    break;\n                }\n              }\n            }\n          }\n        }\n      } catch (error) {\n        console.error(\"Error downloading model:\", error);\n        resolve({\n          success: false,\n          error:\n            error?.message || \"An error occurred while downloading the model\",\n        });\n      }\n    });\n  },\n  // Uninstall a DMR model is not supported via the API\n};\n\nexport default DMRUtils;\n"
  },
  {
    "path": "frontend/src/models/utils/lemonadeUtils.js",
    "content": "import { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders } from \"@/utils/request\";\nimport { safeJsonParse } from \"@/utils/request\";\n\nconst LemonadeUtils = {\n  /**\n   * Download a Lemonade model.\n   * @param {string} modelId - The ID of the model to download.\n   * @param {(percentage: number) => void} progressCallback - The callback to receive the progress percentage. If the model is already downloaded, it will be called once with 100.\n   * @returns {Promise<{success: boolean, error: string|null}>}\n   */\n  downloadModel: async function (\n    modelId,\n    basePath = \"\",\n    progressCallback = () => {}\n  ) {\n    // eslint-disable-next-line no-async-promise-executor\n    return new Promise(async (resolve) => {\n      try {\n        const response = await fetch(\n          `${API_BASE}/utils/lemonade/download-model`,\n          {\n            method: \"POST\",\n            headers: baseHeaders(),\n            body: JSON.stringify({ modelId, basePath }),\n          }\n        );\n\n        if (!response.ok)\n          throw new Error(\"Error downloading model: \" + response.statusText);\n        const reader = response.body.getReader();\n        let done = false;\n\n        while (!done) {\n          const { value, done: readerDone } = await reader.read();\n          if (readerDone) {\n            done = true;\n            resolve({ success: true });\n          } else {\n            const chunk = new TextDecoder(\"utf-8\").decode(value);\n            const lines = chunk.split(\"\\n\");\n            for (const line of lines) {\n              if (line.startsWith(\"data:\")) {\n                const data = safeJsonParse(line.slice(5));\n                switch (data?.type) {\n                  case \"success\":\n                    done = true;\n                    resolve({ success: true });\n                    break;\n                  case \"error\":\n                    done = true;\n                    resolve({\n                      success: false,\n                      error: data?.error || data?.message,\n                    });\n                    break;\n                  case \"progress\":\n                    progressCallback(data?.percentage);\n                    break;\n                  default:\n                    break;\n                }\n              }\n            }\n          }\n        }\n      } catch (error) {\n        console.error(\"Error downloading model:\", error);\n        resolve({\n          success: false,\n          error:\n            error?.message || \"An error occurred while downloading the model\",\n        });\n      }\n    });\n  },\n\n  /**\n   * Delete a Lemonade model from local storage.\n   * If the model is currently loaded, it will be unloaded first.\n   * @param {string} modelId - The ID of the model to delete.\n   * @param {string} basePath - The base path of the Lemonade server.\n   * @returns {Promise<{success: boolean, message?: string, error?: string}>}\n   */\n  deleteModel: async function (modelId, basePath = \"\") {\n    try {\n      const response = await fetch(`${API_BASE}/utils/lemonade/delete-model`, {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({ modelId, basePath }),\n      });\n\n      const data = await response.json();\n      if (!response.ok || !data.success) {\n        return {\n          success: false,\n          error: data.error || \"An error occurred while deleting the model\",\n        };\n      }\n\n      return {\n        success: true,\n        message: data.message,\n      };\n    } catch (error) {\n      console.error(\"Error deleting model:\", error);\n      return {\n        success: false,\n        error: error?.message || \"An error occurred while deleting the model\",\n      };\n    }\n  },\n};\n\nexport default LemonadeUtils;\n"
  },
  {
    "path": "frontend/src/models/workspace.js",
    "content": "import { API_BASE, fullApiUrl } from \"@/utils/constants\";\nimport { baseHeaders, safeJsonParse } from \"@/utils/request\";\nimport { fetchEventSource } from \"@microsoft/fetch-event-source\";\nimport WorkspaceThread from \"@/models/workspaceThread\";\nimport { v4 } from \"uuid\";\nimport { ABORT_STREAM_EVENT } from \"@/utils/chat\";\n\nconst Workspace = {\n  workspaceOrderStorageKey: \"anythingllm-workspace-order\",\n  /** The maximum percentage of the context window that can be used for attachments */\n  maxContextWindowLimit: 0.8,\n\n  new: async function (data = {}) {\n    const { workspace, message } = await fetch(`${API_BASE}/workspace/new`, {\n      method: \"POST\",\n      body: JSON.stringify(data),\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        return { workspace: null, message: e.message };\n      });\n\n    return { workspace, message };\n  },\n  update: async function (slug, data = {}) {\n    const { workspace, message } = await fetch(\n      `${API_BASE}/workspace/${slug}/update`,\n      {\n        method: \"POST\",\n        body: JSON.stringify(data),\n        headers: baseHeaders(),\n      }\n    )\n      .then((res) => res.json())\n      .catch((e) => {\n        return { workspace: null, message: e.message };\n      });\n\n    return { workspace, message };\n  },\n  modifyEmbeddings: async function (slug, changes = {}) {\n    const { workspace, message } = await fetch(\n      `${API_BASE}/workspace/${slug}/update-embeddings`,\n      {\n        method: \"POST\",\n        body: JSON.stringify(changes), // contains 'adds' and 'removes' keys that are arrays of filepaths\n        headers: baseHeaders(),\n      }\n    )\n      .then((res) => res.json())\n      .catch((e) => {\n        return { workspace: null, message: e.message };\n      });\n\n    return { workspace, message };\n  },\n  chatHistory: async function (slug) {\n    const history = await fetch(`${API_BASE}/workspace/${slug}/chats`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .then((res) => res.history || [])\n      .catch(() => []);\n    return history;\n  },\n  updateChatFeedback: async function (chatId, slug, feedback) {\n    const result = await fetch(\n      `${API_BASE}/workspace/${slug}/chat-feedback/${chatId}`,\n      {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({ feedback }),\n      }\n    )\n      .then((res) => res.ok)\n      .catch(() => false);\n    return result;\n  },\n\n  deleteChats: async function (slug = \"\", chatIds = []) {\n    return await fetch(`${API_BASE}/workspace/${slug}/delete-chats`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ chatIds }),\n    })\n      .then((res) => {\n        if (res.ok) return true;\n        throw new Error(\"Failed to delete chats.\");\n      })\n      .catch((e) => {\n        console.log(e);\n        return false;\n      });\n  },\n  deleteEditedChats: async function (slug = \"\", threadSlug = \"\", startingId) {\n    if (!!threadSlug)\n      return this.threads._deleteEditedChats(slug, threadSlug, startingId);\n    return this._deleteEditedChats(slug, startingId);\n  },\n  updateChat: async function (\n    slug = \"\",\n    threadSlug = \"\",\n    chatId,\n    newText,\n    role = \"assistant\"\n  ) {\n    if (!!threadSlug)\n      return this.threads._updateChat(slug, threadSlug, chatId, newText, role);\n    return this._updateChat(slug, chatId, newText, role);\n  },\n  multiplexStream: async function ({\n    workspaceSlug,\n    threadSlug = null,\n    prompt,\n    chatHandler,\n    attachments = [],\n  }) {\n    if (!!threadSlug)\n      return this.threads.streamChat(\n        { workspaceSlug, threadSlug },\n        prompt,\n        chatHandler,\n        attachments\n      );\n    return this.streamChat(\n      { slug: workspaceSlug },\n      prompt,\n      chatHandler,\n      attachments\n    );\n  },\n  streamChat: async function ({ slug }, message, handleChat, attachments = []) {\n    const ctrl = new AbortController();\n\n    // Listen for the ABORT_STREAM_EVENT key to be emitted by the client\n    // to early abort the streaming response. On abort we send a special `stopGeneration`\n    // event to be handled which resets the UI for us to be able to send another message.\n    // The backend response abort handling is done in each LLM's handleStreamResponse.\n    window.addEventListener(ABORT_STREAM_EVENT, () => {\n      ctrl.abort();\n      handleChat({ id: v4(), type: \"stopGeneration\" });\n    });\n\n    await fetchEventSource(`${API_BASE}/workspace/${slug}/stream-chat`, {\n      method: \"POST\",\n      body: JSON.stringify({ message, attachments }),\n      headers: baseHeaders(),\n      signal: ctrl.signal,\n      openWhenHidden: true,\n      async onopen(response) {\n        if (response.ok) {\n          return; // everything's good\n        } else if (\n          response.status >= 400 &&\n          response.status < 500 &&\n          response.status !== 429\n        ) {\n          handleChat({\n            id: v4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: `An error occurred while streaming response. Code ${response.status}`,\n          });\n          ctrl.abort();\n          throw new Error(\"Invalid Status code response.\");\n        } else {\n          handleChat({\n            id: v4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: `An error occurred while streaming response. Unknown Error.`,\n          });\n          ctrl.abort();\n          throw new Error(\"Unknown error\");\n        }\n      },\n      async onmessage(msg) {\n        const chatResult = safeJsonParse(msg.data, null);\n        if (chatResult) handleChat(chatResult);\n      },\n      onerror(err) {\n        handleChat({\n          id: v4(),\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: `An error occurred while streaming response. ${err.message}`,\n        });\n        ctrl.abort();\n        throw new Error();\n      },\n    });\n  },\n  all: async function () {\n    const workspaces = await fetch(`${API_BASE}/workspaces`, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .then((res) => res.workspaces || [])\n      .catch(() => []);\n\n    return workspaces;\n  },\n  bySlug: async function (slug = \"\") {\n    const workspace = await fetch(`${API_BASE}/workspace/${slug}`, {\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .then((res) => res.workspace)\n      .catch(() => null);\n    return workspace;\n  },\n  delete: async function (slug) {\n    const result = await fetch(`${API_BASE}/workspace/${slug}`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.ok)\n      .catch(() => false);\n\n    return result;\n  },\n  wipeVectorDb: async function (slug) {\n    return await fetch(`${API_BASE}/workspace/${slug}/reset-vector-db`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.ok)\n      .catch(() => false);\n  },\n  uploadFile: async function (slug, formData) {\n    const response = await fetch(`${API_BASE}/workspace/${slug}/upload`, {\n      method: \"POST\",\n      body: formData,\n      headers: baseHeaders(),\n    });\n\n    const data = await response.json();\n    return { response, data };\n  },\n  parseFile: async function (slug, formData) {\n    const response = await fetch(`${API_BASE}/workspace/${slug}/parse`, {\n      method: \"POST\",\n      body: formData,\n      headers: baseHeaders(),\n    });\n\n    const data = await response.json();\n    return { response, data };\n  },\n\n  getParsedFiles: async function (slug, threadSlug = null) {\n    const basePath = new URL(`${fullApiUrl()}/workspace/${slug}/parsed-files`);\n    if (threadSlug) basePath.searchParams.set(\"threadSlug\", threadSlug);\n    const response = await fetch(basePath, {\n      method: \"GET\",\n      headers: baseHeaders(),\n    });\n\n    const data = await response.json();\n    return data;\n  },\n  uploadLink: async function (slug, link) {\n    const response = await fetch(`${API_BASE}/workspace/${slug}/upload-link`, {\n      method: \"POST\",\n      body: JSON.stringify({ link }),\n      headers: baseHeaders(),\n    });\n\n    const data = await response.json();\n    return { response, data };\n  },\n\n  getSuggestedMessages: async function (slug) {\n    return await fetch(`${API_BASE}/workspace/${slug}/suggested-messages`, {\n      method: \"GET\",\n      cache: \"no-cache\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Could not fetch suggested messages.\");\n        return res.json();\n      })\n      .then((res) => res.suggestedMessages)\n      .catch((e) => {\n        console.error(e);\n        return null;\n      });\n  },\n  setSuggestedMessages: async function (slug, messages) {\n    return fetch(`${API_BASE}/workspace/${slug}/suggested-messages`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ messages }),\n    })\n      .then((res) => {\n        if (!res.ok) {\n          throw new Error(\n            res.statusText || \"Error setting suggested messages.\"\n          );\n        }\n        return { success: true, ...res.json() };\n      })\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n  setPinForDocument: async function (slug, docPath, pinStatus) {\n    return fetch(`${API_BASE}/workspace/${slug}/update-pin`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ docPath, pinStatus }),\n    })\n      .then((res) => {\n        if (!res.ok) {\n          throw new Error(\n            res.statusText || \"Error setting pin status for document.\"\n          );\n        }\n        return true;\n      })\n      .catch((e) => {\n        console.error(e);\n        return false;\n      });\n  },\n  ttsMessage: async function (slug, chatId) {\n    return await fetch(`${API_BASE}/workspace/${slug}/tts/${chatId}`, {\n      method: \"GET\",\n      cache: \"no-cache\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (res.ok && res.status !== 204) return res.blob();\n        throw new Error(\"Failed to fetch TTS.\");\n      })\n      .then((blob) => (blob ? URL.createObjectURL(blob) : null))\n      .catch(() => {\n        return null;\n      });\n  },\n  uploadPfp: async function (formData, slug) {\n    return await fetch(`${API_BASE}/workspace/${slug}/upload-pfp`, {\n      method: \"POST\",\n      body: formData,\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Error uploading pfp.\");\n        return { success: true, error: null };\n      })\n      .catch((e) => {\n        console.log(e);\n        return { success: false, error: e.message };\n      });\n  },\n\n  fetchPfp: async function (slug) {\n    return await fetch(`${API_BASE}/workspace/${slug}/pfp`, {\n      method: \"GET\",\n      cache: \"no-cache\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (res.ok && res.status !== 204) return res.blob();\n        throw new Error(\"Failed to fetch pfp.\");\n      })\n      .then((blob) => (blob ? URL.createObjectURL(blob) : null))\n      .catch(() => {\n        return null;\n      });\n  },\n\n  removePfp: async function (slug) {\n    return await fetch(`${API_BASE}/workspace/${slug}/remove-pfp`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n    })\n      .then((res) => {\n        if (res.ok) return { success: true, error: null };\n        throw new Error(\"Failed to remove pfp.\");\n      })\n      .catch((e) => {\n        console.log(e);\n        return { success: false, error: e.message };\n      });\n  },\n  _updateChat: async function (slug = \"\", chatId, newText, role = \"assistant\") {\n    return await fetch(`${API_BASE}/workspace/${slug}/update-chat`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ chatId, newText, role }),\n    })\n      .then((res) => {\n        if (res.ok) return true;\n        throw new Error(\"Failed to update chat.\");\n      })\n      .catch((e) => {\n        console.log(e);\n        return false;\n      });\n  },\n  _deleteEditedChats: async function (slug = \"\", startingId) {\n    return await fetch(`${API_BASE}/workspace/${slug}/delete-edited-chats`, {\n      method: \"DELETE\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ startingId }),\n    })\n      .then((res) => {\n        if (res.ok) return true;\n        throw new Error(\"Failed to delete chats.\");\n      })\n      .catch((e) => {\n        console.log(e);\n        return false;\n      });\n  },\n  deleteChat: async (chatId) => {\n    return await fetch(`${API_BASE}/workspace/workspace-chats/${chatId}`, {\n      method: \"PUT\",\n      headers: baseHeaders(),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { success: false, error: e.message };\n      });\n  },\n  forkThread: async function (slug = \"\", threadSlug = null, chatId = null) {\n    return await fetch(`${API_BASE}/workspace/${slug}/thread/fork`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ threadSlug, chatId }),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Failed to fork thread.\");\n        return res.json();\n      })\n      .then((data) => data.newThreadSlug)\n      .catch((e) => {\n        console.error(\"Error forking thread:\", e);\n        return null;\n      });\n  },\n  /**\n   * Uploads and embeds a single file in a single call into a workspace\n   * @param {string} slug - workspace slug\n   * @param {FormData} formData\n   * @returns {Promise<{response: {ok: boolean}, data: {success: boolean, error: string|null, document: {id: string, location:string}|null}}>}\n   */\n  uploadAndEmbedFile: async function (slug, formData) {\n    const response = await fetch(\n      `${API_BASE}/workspace/${slug}/upload-and-embed`,\n      {\n        method: \"POST\",\n        body: formData,\n        headers: baseHeaders(),\n      }\n    );\n\n    const data = await response.json();\n    return { response, data };\n  },\n\n  deleteParsedFiles: async function (slug, fileIds = []) {\n    const response = await fetch(\n      `${API_BASE}/workspace/${slug}/delete-parsed-files`,\n      {\n        method: \"DELETE\",\n        headers: baseHeaders(),\n        body: JSON.stringify({ fileIds }),\n      }\n    );\n    return response.ok;\n  },\n\n  embedParsedFile: async function (slug, fileId) {\n    const response = await fetch(\n      `${API_BASE}/workspace/${slug}/embed-parsed-file/${fileId}`,\n      {\n        method: \"POST\",\n        headers: baseHeaders(),\n      }\n    );\n\n    const data = await response.json();\n    return { response, data };\n  },\n\n  /**\n   * Deletes and un-embeds a single file in a single call from a workspace\n   * @param {string} slug - workspace slug\n   * @param {string} documentLocation - location of file eg: custom-documents/my-file-uuid.json\n   * @returns {Promise<boolean>}\n   */\n  deleteAndUnembedFile: async function (slug, documentLocation) {\n    const response = await fetch(\n      `${API_BASE}/workspace/${slug}/remove-and-unembed`,\n      {\n        method: \"DELETE\",\n        body: JSON.stringify({ documentLocation }),\n        headers: baseHeaders(),\n      }\n    );\n    return response.ok;\n  },\n\n  /**\n   * Reorders workspaces in the UI via localstorage on client side.\n   * @param {string[]} workspaceIds - array of workspace ids to reorder\n   * @returns {boolean}\n   */\n  storeWorkspaceOrder: function (workspaceIds = []) {\n    try {\n      localStorage.setItem(\n        this.workspaceOrderStorageKey,\n        JSON.stringify(workspaceIds)\n      );\n      return true;\n    } catch (error) {\n      console.error(\"Error reordering workspaces:\", error);\n      return false;\n    }\n  },\n\n  /**\n   * Orders workspaces based on the order preference stored in localstorage\n   * @param {Array} workspaces - array of workspace JSON objects\n   * @returns {Array} - ordered workspaces\n   */\n  orderWorkspaces: function (workspaces = []) {\n    const workspaceOrderPreference =\n      safeJsonParse(localStorage.getItem(this.workspaceOrderStorageKey)) || [];\n    if (workspaceOrderPreference.length === 0) return workspaces;\n    const orderedWorkspaces = Array.from(workspaces);\n    orderedWorkspaces.sort(\n      (a, b) =>\n        workspaceOrderPreference.indexOf(a.id) -\n        workspaceOrderPreference.indexOf(b.id)\n    );\n    return orderedWorkspaces;\n  },\n\n  /**\n   * Searches for workspaces and threads\n   * @param {string} searchTerm\n   * @returns {Promise<{workspaces: [{slug: string, name: string}], threads: [{slug: string, name: string, workspace: {slug: string, name: string}}]}}>}\n   */\n  searchWorkspaceOrThread: async function (searchTerm) {\n    const response = await fetch(`${API_BASE}/workspace/search`, {\n      method: \"POST\",\n      headers: baseHeaders(),\n      body: JSON.stringify({ searchTerm }),\n    })\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { workspaces: [], threads: [] };\n      });\n    return response;\n  },\n\n  /**\n   * Checks if the agent command is available for a workspace\n   * by checking if the workspace's agent provider supports native tool calling.\n   *\n   * This can be model specific or enabled via ENV flag.\n   * @param {string} slug - workspace slug\n   * @returns {Promise<{showAgentCommand: boolean}>}\n   */\n  agentCommandAvailable: async function (slug = null) {\n    if (!slug) return { showAgentCommand: true };\n    return await fetch(\n      `${API_BASE}/workspace/${slug}/is-agent-command-available`,\n      { headers: baseHeaders() }\n    )\n      .then((res) => res.json())\n      .catch((e) => {\n        console.error(e);\n        return { showAgentCommand: true };\n      });\n  },\n\n  threads: WorkspaceThread,\n};\n\nexport default Workspace;\n"
  },
  {
    "path": "frontend/src/models/workspaceThread.js",
    "content": "import { ABORT_STREAM_EVENT } from \"@/utils/chat\";\nimport { API_BASE } from \"@/utils/constants\";\nimport { baseHeaders, safeJsonParse } from \"@/utils/request\";\nimport { fetchEventSource } from \"@microsoft/fetch-event-source\";\nimport { v4 } from \"uuid\";\n\nconst WorkspaceThread = {\n  all: async function (workspaceSlug) {\n    const { threads } = await fetch(\n      `${API_BASE}/workspace/${workspaceSlug}/threads`,\n      {\n        method: \"GET\",\n        headers: baseHeaders(),\n      }\n    )\n      .then((res) => res.json())\n      .catch(() => {\n        return { threads: [] };\n      });\n\n    return { threads };\n  },\n  new: async function (workspaceSlug) {\n    const { thread, error } = await fetch(\n      `${API_BASE}/workspace/${workspaceSlug}/thread/new`,\n      {\n        method: \"POST\",\n        headers: baseHeaders(),\n      }\n    )\n      .then((res) => res.json())\n      .catch((e) => {\n        return { thread: null, error: e.message };\n      });\n\n    return { thread, error };\n  },\n  update: async function (workspaceSlug, threadSlug, data = {}) {\n    const { thread, message } = await fetch(\n      `${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/update`,\n      {\n        method: \"POST\",\n        body: JSON.stringify(data),\n        headers: baseHeaders(),\n      }\n    )\n      .then((res) => res.json())\n      .catch((e) => {\n        return { thread: null, message: e.message };\n      });\n\n    return { thread, message };\n  },\n  delete: async function (workspaceSlug, threadSlug) {\n    return await fetch(\n      `${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}`,\n      {\n        method: \"DELETE\",\n        headers: baseHeaders(),\n      }\n    )\n      .then((res) => res.ok)\n      .catch(() => false);\n  },\n  deleteBulk: async function (workspaceSlug, threadSlugs = []) {\n    return await fetch(\n      `${API_BASE}/workspace/${workspaceSlug}/thread-bulk-delete`,\n      {\n        method: \"DELETE\",\n        body: JSON.stringify({ slugs: threadSlugs }),\n        headers: baseHeaders(),\n      }\n    )\n      .then((res) => res.ok)\n      .catch(() => false);\n  },\n  chatHistory: async function (workspaceSlug, threadSlug) {\n    const history = await fetch(\n      `${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/chats`,\n      {\n        method: \"GET\",\n        headers: baseHeaders(),\n      }\n    )\n      .then((res) => res.json())\n      .then((res) => res.history || [])\n      .catch(() => []);\n    return history;\n  },\n  streamChat: async function (\n    { workspaceSlug, threadSlug },\n    message,\n    handleChat,\n    attachments = []\n  ) {\n    const ctrl = new AbortController();\n\n    // Listen for the ABORT_STREAM_EVENT key to be emitted by the client\n    // to early abort the streaming response. On abort we send a special `stopGeneration`\n    // event to be handled which resets the UI for us to be able to send another message.\n    // The backend response abort handling is done in each LLM's handleStreamResponse.\n    window.addEventListener(ABORT_STREAM_EVENT, () => {\n      ctrl.abort();\n      handleChat({ id: v4(), type: \"stopGeneration\" });\n    });\n\n    await fetchEventSource(\n      `${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/stream-chat`,\n      {\n        method: \"POST\",\n        body: JSON.stringify({ message, attachments }),\n        headers: baseHeaders(),\n        signal: ctrl.signal,\n        openWhenHidden: true,\n        async onopen(response) {\n          if (response.ok) {\n            return; // everything's good\n          } else if (\n            response.status >= 400 &&\n            response.status < 500 &&\n            response.status !== 429\n          ) {\n            handleChat({\n              id: v4(),\n              type: \"abort\",\n              textResponse: null,\n              sources: [],\n              close: true,\n              error: `An error occurred while streaming response. Code ${response.status}`,\n            });\n            ctrl.abort();\n            throw new Error(\"Invalid Status code response.\");\n          } else {\n            handleChat({\n              id: v4(),\n              type: \"abort\",\n              textResponse: null,\n              sources: [],\n              close: true,\n              error: `An error occurred while streaming response. Unknown Error.`,\n            });\n            ctrl.abort();\n            throw new Error(\"Unknown error\");\n          }\n        },\n        async onmessage(msg) {\n          const chatResult = safeJsonParse(msg.data, null);\n          if (chatResult) handleChat(chatResult);\n        },\n        onerror(err) {\n          handleChat({\n            id: v4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: `An error occurred while streaming response. ${err.message}`,\n          });\n          ctrl.abort();\n          throw new Error();\n        },\n      }\n    );\n  },\n  _deleteEditedChats: async function (\n    workspaceSlug = \"\",\n    threadSlug = \"\",\n    startingId\n  ) {\n    return await fetch(\n      `${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/delete-edited-chats`,\n      {\n        method: \"DELETE\",\n        headers: baseHeaders(),\n        body: JSON.stringify({ startingId }),\n      }\n    )\n      .then((res) => {\n        if (res.ok) return true;\n        throw new Error(\"Failed to delete chats.\");\n      })\n      .catch((e) => {\n        console.log(e);\n        return false;\n      });\n  },\n  _updateChat: async function (\n    workspaceSlug = \"\",\n    threadSlug = \"\",\n    chatId,\n    newText,\n    role = \"assistant\"\n  ) {\n    return await fetch(\n      `${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/update-chat`,\n      {\n        method: \"POST\",\n        headers: baseHeaders(),\n        body: JSON.stringify({ chatId, newText, role }),\n      }\n    )\n      .then((res) => {\n        if (res.ok) return true;\n        throw new Error(\"Failed to update chat.\");\n      })\n      .catch((e) => {\n        console.log(e);\n        return false;\n      });\n  },\n};\n\nexport default WorkspaceThread;\n"
  },
  {
    "path": "frontend/src/pages/404.jsx",
    "content": "import { NavLink } from \"react-router-dom\";\nimport { House, MagnifyingGlass } from \"@phosphor-icons/react\";\n\nexport default function NotFound() {\n  return (\n    <div className=\"flex flex-col items-center justify-center min-h-screen bg-theme-bg-primary text-theme-text-primary gap-4 p-4 md:p-8 w-full\">\n      <MagnifyingGlass className=\"w-16 h-16 text-theme-text-secondary\" />\n      <h1 className=\"text-xl md:text-2xl font-bold text-center\">\n        404 - Page Not Found\n      </h1>\n      <p className=\"text-theme-text-secondary text-center px-4\">\n        The page you're looking for doesn't exist or has been moved.\n      </p>\n      <div className=\"flex flex-col md:flex-row gap-3 md:gap-4 mt-4 w-full md:w-auto\">\n        <NavLink\n          to=\"/\"\n          className=\"flex items-center justify-center gap-2 px-4 py-2 bg-theme-bg-secondary text-theme-text-primary rounded-lg hover:bg-theme-sidebar-item-hover transition-all duration-300 w-full md:w-auto\"\n        >\n          <House className=\"w-4 h-4\" />\n          Go Home\n        </NavLink>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/AgentBuilder/AddBlockMenu/index.jsx",
    "content": "import React, { useRef, useEffect } from \"react\";\nimport { Plus, CaretDown } from \"@phosphor-icons/react\";\nimport { BLOCK_TYPES, BLOCK_INFO } from \"../BlockList\";\n\n/**\n * Check if the last configurable block has direct output disabled or undefined\n * If this property is true then you cannot add a new block after it.\n * @param {Array} blocks - The blocks array\n * @returns {Boolean} True if the last configurable block has direct output disabled, false otherwise\n */\nfunction checkIfCanAddBlock(blocks) {\n  const lastConfigurableBlock = blocks[blocks.length - 2];\n  if (!lastConfigurableBlock) return true;\n  return (\n    lastConfigurableBlock?.config?.directOutput === false ||\n    lastConfigurableBlock?.config?.directOutput === undefined\n  );\n}\n\nexport default function AddBlockMenu({\n  blocks,\n  showBlockMenu,\n  setShowBlockMenu,\n  addBlock,\n}) {\n  const menuRef = useRef(null);\n\n  useEffect(() => {\n    function handleClickOutside(event) {\n      if (menuRef.current && !menuRef.current.contains(event.target)) {\n        setShowBlockMenu(false);\n      }\n    }\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, [setShowBlockMenu]);\n\n  if (checkIfCanAddBlock(blocks) === false) return null;\n  return (\n    <div className=\"relative mt-4 w-[280px] mx-auto pb-4\" ref={menuRef}>\n      <button\n        onClick={() => setShowBlockMenu(!showBlockMenu)}\n        className=\"transition-all duration-300 w-full p-2.5 bg-theme-action-menu-bg hover:bg-theme-action-menu-item-hover border border-white/10 rounded-lg text-white flex items-center justify-center gap-2 text-sm font-medium\"\n      >\n        <Plus className=\"w-4 h-4\" />\n        Add Block\n        <CaretDown\n          className={`w-3.5 h-3.5 transition-transform duration-300 ${showBlockMenu ? \"rotate-180\" : \"\"}`}\n        />\n      </button>\n      {showBlockMenu && (\n        <div className=\"absolute left-0 right-0 mt-2 bg-theme-action-menu-bg border border-white/10 rounded-lg shadow-lg overflow-hidden z-10 animate-fadeUpIn\">\n          {Object.entries(BLOCK_INFO).map(\n            ([type, info]) =>\n              type !== BLOCK_TYPES.START &&\n              type !== BLOCK_TYPES.FINISH &&\n              type !== BLOCK_TYPES.FLOW_INFO && (\n                <button\n                  key={type}\n                  onClick={() => {\n                    addBlock(type);\n                    setShowBlockMenu(false);\n                  }}\n                  className=\"w-full p-2.5 flex items-center gap-3 hover:bg-theme-action-menu-item-hover text-white transition-colors duration-300 group\"\n                >\n                  <div className=\"w-7 h-7 rounded-lg bg-white/10 flex items-center justify-center\">\n                    <div className=\"w-fit h-fit text-white\">{info.icon}</div>\n                  </div>\n                  <div className=\"text-left flex-1\">\n                    <div className=\"text-sm font-medium\">{info.label}</div>\n                    <div className=\"text-xs text-white/60\">\n                      {info.description}\n                    </div>\n                  </div>\n                </button>\n              )\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/AgentBuilder/BlockList/index.jsx",
    "content": "import React from \"react\";\nimport {\n  X,\n  CaretUp,\n  CaretDown,\n  Globe,\n  Browser,\n  Brain,\n  Flag,\n  Info,\n  BracketsCurly,\n} from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\nimport Toggle from \"@/components/lib/Toggle\";\nimport StartNode from \"../nodes/StartNode\";\nimport ApiCallNode from \"../nodes/ApiCallNode\";\nimport WebsiteNode from \"../nodes/WebsiteNode\";\nimport FileNode from \"../nodes/FileNode\";\nimport CodeNode from \"../nodes/CodeNode\";\nimport LLMInstructionNode from \"../nodes/LLMInstructionNode\";\nimport FinishNode from \"../nodes/FinishNode\";\nimport WebScrapingNode from \"../nodes/WebScrapingNode\";\nimport FlowInfoNode from \"../nodes/FlowInfoNode\";\n\nconst BLOCK_TYPES = {\n  FLOW_INFO: \"flowInfo\",\n  START: \"start\",\n  API_CALL: \"apiCall\",\n  // WEBSITE: \"website\", // Temporarily disabled\n  // FILE: \"file\", // Temporarily disabled\n  // CODE: \"code\", // Temporarily disabled\n  LLM_INSTRUCTION: \"llmInstruction\",\n  WEB_SCRAPING: \"webScraping\",\n  FINISH: \"finish\",\n};\n\nconst BLOCK_INFO = {\n  [BLOCK_TYPES.FLOW_INFO]: {\n    label: \"Flow Information\",\n    icon: <Info className=\"w-5 h-5 text-theme-text-primary\" />,\n    description: \"Basic flow information\",\n    defaultConfig: {\n      name: \"\",\n      description: \"\",\n    },\n    getSummary: (config) => config.name || \"Untitled Flow\",\n  },\n  [BLOCK_TYPES.START]: {\n    label: \"Flow Variables\",\n    icon: <BracketsCurly className=\"w-5 h-5 text-theme-text-primary\" />,\n    description: \"Configure agent variables and settings\",\n    getSummary: (config) => {\n      const varCount = config.variables?.filter((v) => v.name)?.length || 0;\n      return `${varCount} variable${varCount !== 1 ? \"s\" : \"\"} defined`;\n    },\n  },\n  [BLOCK_TYPES.API_CALL]: {\n    label: \"API Call\",\n    icon: <Globe className=\"w-5 h-5 text-theme-text-primary\" />,\n    description: \"Make an HTTP request\",\n    defaultConfig: {\n      url: \"\",\n      method: \"GET\",\n      headers: [],\n      bodyType: \"json\",\n      body: \"\",\n      formData: [],\n      responseVariable: \"\",\n      directOutput: false,\n    },\n    getSummary: (config) =>\n      `${config.method || \"GET\"} ${config.url || \"(no URL)\"}`,\n  },\n  // TODO: Implement website, file, and code blocks\n  /* [BLOCK_TYPES.WEBSITE]: {\n    label: \"Open Website\",\n    icon: <Browser className=\"w-5 h-5 text-theme-text-primary\" />,\n    description: \"Navigate to a URL\",\n    defaultConfig: {\n      url: \"\",\n      selector: \"\",\n      action: \"read\",\n      value: \"\",\n      resultVariable: \"\",\n    },\n    getSummary: (config) =>\n      `${config.action || \"read\"} from ${config.url || \"(no URL)\"}`,\n  },\n  [BLOCK_TYPES.FILE]: {\n    label: \"Open File\",\n    icon: <File className=\"w-5 h-5 text-theme-text-primary\" />,\n    description: \"Read or write to a file\",\n    defaultConfig: {\n      path: \"\",\n      operation: \"read\",\n      content: \"\",\n      resultVariable: \"\",\n    },\n    getSummary: (config) =>\n      `${config.operation || \"read\"} ${config.path || \"(no path)\"}`,\n  },\n  [BLOCK_TYPES.CODE]: {\n    label: \"Code Execution\",\n    icon: <Code className=\"w-5 h-5 text-theme-text-primary\" />,\n    description: \"Execute code snippets\",\n    defaultConfig: {\n      language: \"javascript\",\n      code: \"\",\n      resultVariable: \"\",\n    },\n    getSummary: (config) => `Run ${config.language || \"javascript\"} code`,\n  },\n  */\n  [BLOCK_TYPES.LLM_INSTRUCTION]: {\n    label: \"LLM Instruction\",\n    icon: <Brain className=\"w-5 h-5 text-theme-text-primary\" />,\n    description: \"Process data using LLM instructions\",\n    defaultConfig: {\n      instruction: \"\",\n      resultVariable: \"\",\n      directOutput: false,\n    },\n    getSummary: (config) => config.instruction || \"No instruction\",\n  },\n  [BLOCK_TYPES.WEB_SCRAPING]: {\n    label: \"Web Scraping\",\n    icon: <Browser className=\"w-5 h-5 text-theme-text-primary\" />,\n    description: \"Scrape content from a webpage\",\n    defaultConfig: {\n      url: \"\",\n      captureAs: \"text\",\n      querySelector: \"\",\n      resultVariable: \"\",\n      directOutput: false,\n    },\n    getSummary: (config) => config.url || \"No URL specified\",\n  },\n  [BLOCK_TYPES.FINISH]: {\n    label: \"Flow Complete\",\n    icon: <Flag className=\"w-4 h-4\" />,\n    description: \"End of agent flow\",\n    getSummary: () => \"Flow will end here\",\n    defaultConfig: {},\n    renderConfig: () => null,\n  },\n};\n\nexport default function BlockList({\n  blocks,\n  updateBlockConfig,\n  removeBlock,\n  toggleBlockExpansion,\n  renderVariableSelect,\n  onDeleteVariable,\n  moveBlock,\n  refs,\n}) {\n  const renderBlockConfig = (block) => {\n    const isLastConfigurableBlock = blocks[blocks.length - 2]?.id === block.id;\n    const props = {\n      config: block.config,\n      onConfigChange: (config) => updateBlockConfig(block.id, config),\n      renderVariableSelect,\n      onDeleteVariable,\n    };\n\n    // Direct output switch to the last configurable block before finish\n    if (\n      isLastConfigurableBlock &&\n      block.type !== BLOCK_TYPES.START &&\n      block.type !== BLOCK_TYPES.FLOW_INFO\n    ) {\n      return (\n        <div className=\"space-y-4\">\n          {renderBlockConfigContent(block, props)}\n          <div className=\"pt-4 border-t border-white/10\">\n            <Toggle\n              size=\"md\"\n              variant=\"horizontal\"\n              label=\"Direct Output\"\n              description=\"The output of this block will be returned directly to the chat. This will prevent any further tool calls from being executed.\"\n              enabled={props.config.directOutput || false}\n              onChange={(checked) =>\n                props.onConfigChange({\n                  ...props.config,\n                  directOutput: checked,\n                })\n              }\n            />\n          </div>\n        </div>\n      );\n    }\n\n    return renderBlockConfigContent(block, props);\n  };\n\n  const renderBlockConfigContent = (block, props) => {\n    switch (block.type) {\n      case BLOCK_TYPES.FLOW_INFO:\n        return <FlowInfoNode {...props} ref={refs} />;\n      case BLOCK_TYPES.START:\n        return <StartNode {...props} />;\n      case BLOCK_TYPES.API_CALL:\n        return <ApiCallNode {...props} />;\n      case BLOCK_TYPES.WEBSITE:\n        return <WebsiteNode {...props} />;\n      case BLOCK_TYPES.FILE:\n        return <FileNode {...props} />;\n      case BLOCK_TYPES.CODE:\n        return <CodeNode {...props} />;\n      case BLOCK_TYPES.LLM_INSTRUCTION:\n        return <LLMInstructionNode {...props} />;\n      case BLOCK_TYPES.WEB_SCRAPING:\n        return <WebScrapingNode {...props} />;\n      case BLOCK_TYPES.FINISH:\n        return <FinishNode />;\n      default:\n        return <div>Configuration options coming soon...</div>;\n    }\n  };\n\n  return (\n    <div className=\"space-y-1\">\n      {blocks.map((block, index) => (\n        <div key={block.id} className=\"flex flex-col\">\n          <div\n            className={`bg-theme-action-menu-bg border border-white/10 rounded-lg overflow-hidden transition-all duration-300 ${\n              block.isExpanded ? \"w-full\" : \"w-[280px] mx-auto\"\n            }`}\n          >\n            <div\n              onClick={() => toggleBlockExpansion(block.id)}\n              className=\"w-full p-4 flex items-center justify-between hover:bg-theme-action-menu-item-hover transition-colors duration-300 group cursor-pointer\"\n            >\n              <div className=\"flex items-center gap-3\">\n                <div className=\"w-7 h-7 rounded-lg bg-white/10 light:bg-white flex items-center justify-center\">\n                  {React.cloneElement(BLOCK_INFO[block.type].icon, {\n                    className: \"w-4 h-4 text-white\",\n                  })}\n                </div>\n                <div className=\"flex-1 text-left min-w-0 max-w-[115px]\">\n                  <span className=\"text-sm font-medium text-white block\">\n                    {BLOCK_INFO[block.type].label}\n                  </span>\n                  {!block.isExpanded && (\n                    <p className=\"text-xs text-white/60 truncate\">\n                      {BLOCK_INFO[block.type].getSummary(block.config)}\n                    </p>\n                  )}\n                </div>\n              </div>\n              <div className=\"flex items-center\">\n                {block.id !== \"start\" &&\n                  block.type !== BLOCK_TYPES.FINISH &&\n                  block.type !== BLOCK_TYPES.FLOW_INFO && (\n                    <div className=\"flex items-center gap-1\">\n                      {index > 2 && (\n                        <button\n                          onClick={(e) => {\n                            e.stopPropagation();\n                            moveBlock(index, index - 1);\n                          }}\n                          className=\"w-7 h-7 flex items-center justify-center rounded-lg bg-theme-bg-primary border border-white/5 text-white hover:bg-theme-action-menu-item-hover transition-colors duration-300\"\n                          data-tooltip-id=\"block-action\"\n                          data-tooltip-content=\"Move block up\"\n                        >\n                          <CaretUp className=\"w-3.5 h-3.5\" />\n                        </button>\n                      )}\n                      {index < blocks.length - 2 && (\n                        <button\n                          onClick={(e) => {\n                            e.stopPropagation();\n                            moveBlock(index, index + 1);\n                          }}\n                          className=\"w-7 h-7 flex items-center justify-center rounded-lg bg-theme-bg-primary border border-white/5 text-white hover:bg-theme-action-menu-item-hover transition-colors duration-300\"\n                          data-tooltip-id=\"block-action\"\n                          data-tooltip-content=\"Move block down\"\n                        >\n                          <CaretDown className=\"w-3.5 h-3.5\" />\n                        </button>\n                      )}\n                      <button\n                        onClick={(e) => {\n                          e.stopPropagation();\n                          removeBlock(block.id);\n                        }}\n                        className=\"w-7 h-7 flex items-center justify-center rounded-lg bg-theme-bg-primary border border-white/5 text-red-400 hover:bg-red-500/10 hover:border-red-500/20 transition-colors duration-300\"\n                        data-tooltip-id=\"block-action\"\n                        data-tooltip-content=\"Delete block\"\n                      >\n                        <X className=\"w-3.5 h-3.5\" />\n                      </button>\n                    </div>\n                  )}\n              </div>\n            </div>\n            <div\n              className={`overflow-hidden transition-all duration-300 ease-in-out ${\n                block.isExpanded\n                  ? \"max-h-[1000px] opacity-100\"\n                  : \"max-h-0 opacity-0\"\n              }`}\n            >\n              <div className=\"border-t border-white/10 p-4 bg-theme-bg-secondary rounded-b-lg\">\n                {renderBlockConfig(block)}\n              </div>\n            </div>\n          </div>\n          {index < blocks.length - 1 && (\n            <div className=\"flex justify-center my-1\">\n              <svg\n                width=\"20\"\n                height=\"20\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n                className=\"text-white/40 light:invert\"\n              >\n                <path\n                  d=\"M12 4L12 20M12 20L6 14M12 20L18 14\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"2\"\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                />\n              </svg>\n            </div>\n          )}\n        </div>\n      ))}\n      <Tooltip\n        id=\"block-action\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip !text-xs\"\n      />\n    </div>\n  );\n}\n\nexport { BLOCK_TYPES, BLOCK_INFO };\n"
  },
  {
    "path": "frontend/src/pages/Admin/AgentBuilder/HeaderMenu/index.jsx",
    "content": "import { CaretDown, CaretUp, Plus, CaretLeft } from \"@phosphor-icons/react\";\nimport AnythingInfinityLogo from \"@/media/logo/anything-llm-infinity.png\";\nimport { useState, useRef, useEffect } from \"react\";\nimport { useNavigate, useParams } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\nimport { Link } from \"react-router-dom\";\n\nexport default function HeaderMenu({\n  agentName,\n  availableFlows = [],\n  onNewFlow,\n  onSaveFlow,\n  onPublishFlow,\n}) {\n  const { flowId = null } = useParams();\n  const [showDropdown, setShowDropdown] = useState(false);\n  const navigate = useNavigate();\n  const dropdownRef = useRef(null);\n  const hasOtherFlows =\n    availableFlows.filter((flow) => flow.uuid !== flowId).length > 0;\n\n  useEffect(() => {\n    function handleClickOutside(event) {\n      if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {\n        setShowDropdown(false);\n      }\n    }\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, []);\n\n  return (\n    <div className=\"absolute top-[calc(40px+16px)] left-4 right-4\">\n      <div className=\"flex justify-between items-start max-w-[1700px] mx-auto\">\n        <div className=\"flex items-center gap-x-2\">\n          <button\n            onClick={() => navigate(paths.settings.agentSkills())}\n            className=\"w-8 h-8 flex items-center justify-center rounded-full bg-theme-settings-input-bg border border-white/10 hover:bg-theme-action-menu-bg transition-colors duration-300\"\n          >\n            <CaretLeft\n              weight=\"bold\"\n              className=\"w-5 h-5 text-theme-text-primary\"\n            />\n          </button>\n          <div\n            className=\"flex items-center bg-theme-settings-input-bg rounded-md border border-white/10 pointer-events-auto\"\n            ref={dropdownRef}\n          >\n            <button\n              onClick={() => navigate(paths.settings.agentSkills())}\n              className=\"!border-t-transparent !border-l-transparent !border-b-transparent flex items-center gap-x-2 px-4 py-2 border-r border-white/10 hover:bg-theme-action-menu-bg transition-colors duration-300\"\n            >\n              <img\n                src={AnythingInfinityLogo}\n                alt=\"logo\"\n                className=\"w-[20px] light:invert\"\n              />\n              <span className=\"text-theme-text-primary text-sm uppercase tracking-widest\">\n                Builder\n              </span>\n            </button>\n            <div className=\"relative\">\n              <button\n                disabled={!hasOtherFlows}\n                className=\"border-none flex items-center justify-between gap-x-1 text-theme-text-primary text-sm px-4 py-2 enabled:hover:bg-theme-action-menu-bg transition-colors duration-300 min-w-[200px] max-w-[300px]\"\n                onClick={() => {\n                  if (!agentName && !hasOtherFlows) {\n                    const agentNameInput = document.getElementById(\n                      \"agent-flow-name-input\"\n                    );\n                    if (agentNameInput) agentNameInput.focus();\n                    return;\n                  }\n                  setShowDropdown(!showDropdown);\n                }}\n              >\n                <span\n                  className={`text-sm font-medium truncate ${!!agentName ? \"text-theme-text-primary \" : \"text-theme-text-secondary\"}`}\n                >\n                  {agentName || \"Untitled Flow\"}\n                </span>\n                {hasOtherFlows && (\n                  <div className=\"flex flex-col ml-2 shrink-0\">\n                    <CaretUp size={10} />\n                    <CaretDown size={10} />\n                  </div>\n                )}\n              </button>\n              {showDropdown && (\n                <div className=\"absolute top-full left-0 mt-1 w-full min-w-[200px] max-w-[350px] bg-theme-settings-input-bg border border-white/10 rounded-md shadow-lg z-50 animate-fadeUpIn\">\n                  {availableFlows\n                    .filter((flow) => flow.uuid !== flowId)\n                    .map((flow, index) => (\n                      <button\n                        key={flow?.uuid || `flow-${index}`}\n                        onClick={() => {\n                          navigate(paths.agents.editAgent(flow.uuid));\n                          setShowDropdown(false);\n                        }}\n                        className=\"border-none w-full text-left px-2 py-1 text-sm text-theme-text-primary hover:bg-theme-action-menu-bg transition-colors duration-300\"\n                      >\n                        <span className=\"block truncate\">\n                          {flow?.name || \"Untitled Flow\"}\n                        </span>\n                      </button>\n                    ))}\n                </div>\n              )}\n            </div>\n          </div>\n        </div>\n\n        <div className=\"flex flex-col gap-y-1 items-end\">\n          <div className=\"flex items-center gap-x-[15px]\">\n            <button\n              onClick={onNewFlow}\n              className=\"flex items-center gap-x-2 text-theme-text-primary text-sm font-medium px-3 py-2 rounded-lg border border-white bg-theme-settings-input-bg hover:bg-theme-action-menu-bg transition-colors duration-300\"\n            >\n              <Plus className=\"w-4 h-4\" />\n              New Flow\n            </button>\n            <button\n              onClick={onPublishFlow}\n              className=\"px-3 py-2 rounded-lg text-sm font-medium flex items-center justify-center gap-2 border border-white/10 bg-theme-bg-primary text-theme-text-primary hover:bg-theme-action-menu-bg transition-all duration-300\"\n            >\n              Publish\n            </button>\n            <button\n              onClick={onSaveFlow}\n              className=\"border-none bg-primary-button hover:opacity-80 text-black light:text-white px-3 py-2 rounded-lg text-sm font-medium transition-all duration-300 flex items-center justify-center gap-2\"\n            >\n              Save\n            </button>\n          </div>\n          <Link\n            to=\"https://docs.anythingllm.com/agent-flows/overview\"\n            className=\"text-theme-text-secondary text-sm hover:underline hover:text-cta-button flex items-center gap-x-1 w-fit float-right\"\n          >\n            view documentation &rarr;\n          </Link>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/AgentBuilder/index.jsx",
    "content": "import React, { useState, useEffect, useRef } from \"react\";\nimport { useNavigate, useParams } from \"react-router-dom\";\nimport { Tooltip } from \"react-tooltip\";\n\nimport BlockList, { BLOCK_TYPES, BLOCK_INFO } from \"./BlockList\";\nimport AddBlockMenu from \"./AddBlockMenu\";\nimport showToast from \"@/utils/toast\";\nimport AgentFlows from \"@/models/agentFlows\";\nimport { useTheme } from \"@/hooks/useTheme\";\nimport HeaderMenu from \"./HeaderMenu\";\nimport paths from \"@/utils/paths\";\nimport PublishEntityModal from \"@/components/CommunityHub/PublishEntityModal\";\n\nconst DEFAULT_BLOCKS = [\n  {\n    id: \"flow_info\",\n    type: BLOCK_TYPES.FLOW_INFO,\n    config: {\n      name: \"\",\n      description: \"\",\n    },\n    isExpanded: true,\n  },\n  {\n    id: \"start\",\n    type: BLOCK_TYPES.START,\n    config: {\n      variables: [{ name: \"\", value: \"\" }],\n    },\n    isExpanded: true,\n  },\n  {\n    id: \"finish\",\n    type: BLOCK_TYPES.FINISH,\n    config: {},\n    isExpanded: false,\n  },\n];\n\nexport default function AgentBuilder() {\n  const { flowId } = useParams();\n  const { theme } = useTheme();\n  const navigate = useNavigate();\n  const [agentName, setAgentName] = useState(\"\");\n  const [_, setAgentDescription] = useState(\"\");\n  const [currentFlowUuid, setCurrentFlowUuid] = useState(null);\n  const [active, setActive] = useState(true);\n  const [blocks, setBlocks] = useState(DEFAULT_BLOCKS);\n  const [selectedBlock, setSelectedBlock] = useState(\"start\");\n  const [showBlockMenu, setShowBlockMenu] = useState(false);\n  const [availableFlows, setAvailableFlows] = useState([]);\n  const nameRef = useRef(null);\n  const descriptionRef = useRef(null);\n  const [showPublishModal, setShowPublishModal] = useState(false);\n\n  useEffect(() => {\n    loadAvailableFlows();\n  }, []);\n\n  useEffect(() => {\n    if (flowId) {\n      loadFlow(flowId);\n    }\n  }, [flowId]);\n\n  useEffect(() => {\n    const flowInfoBlock = blocks.find(\n      (block) => block.type === BLOCK_TYPES.FLOW_INFO\n    );\n    setAgentName(flowInfoBlock?.config?.name || \"\");\n  }, [blocks]);\n\n  const loadAvailableFlows = async () => {\n    try {\n      const { success, error, flows } = await AgentFlows.listFlows();\n      if (!success) throw new Error(error);\n      setAvailableFlows(flows);\n    } catch (error) {\n      console.error(error);\n      showToast(\"Failed to load available flows\", \"error\", { clear: true });\n    }\n  };\n\n  const loadFlow = async (uuid) => {\n    try {\n      const { success, error, flow } = await AgentFlows.getFlow(uuid);\n      if (!success) throw new Error(error);\n\n      // Convert steps to blocks with IDs, ensuring finish block is at the end\n      const flowBlocks = [\n        {\n          id: \"flow_info\",\n          type: BLOCK_TYPES.FLOW_INFO,\n          config: {\n            name: flow.config.name,\n            description: flow.config.description,\n          },\n          isExpanded: true,\n        },\n        ...flow.config.steps.map((step, index) => ({\n          id: index === 0 ? \"start\" : `block_${index}`,\n          type: step.type,\n          config: step.config,\n          isExpanded: true,\n        })),\n      ];\n\n      // Add finish block if not present\n      if (flowBlocks[flowBlocks.length - 1]?.type !== BLOCK_TYPES.FINISH) {\n        flowBlocks.push({\n          id: \"finish\",\n          type: BLOCK_TYPES.FINISH,\n          config: {},\n          isExpanded: false,\n        });\n      }\n\n      setAgentName(flow.config.name);\n      setAgentDescription(flow.config.description);\n      setActive(flow.config.active ?? true);\n      setCurrentFlowUuid(flow.uuid);\n      setBlocks(flowBlocks);\n    } catch (error) {\n      console.error(error);\n      showToast(\"Failed to load flow\", \"error\", { clear: true });\n    }\n  };\n\n  const addBlock = (type) => {\n    const newBlock = {\n      id: `block_${blocks.length}`,\n      type,\n      config: { ...BLOCK_INFO[type].defaultConfig },\n      isExpanded: true,\n    };\n    // Insert the new block before the finish block\n    const newBlocks = [...blocks];\n    newBlocks.splice(newBlocks.length - 1, 0, newBlock);\n    setBlocks(newBlocks);\n    setShowBlockMenu(false);\n  };\n\n  const updateBlockConfig = (blockId, config) => {\n    setBlocks(\n      blocks.map((block) =>\n        block.id === blockId\n          ? { ...block, config: { ...block.config, ...config } }\n          : block\n      )\n    );\n  };\n\n  const removeBlock = (blockId) => {\n    if (blockId === \"start\") return;\n    setBlocks(blocks.filter((block) => block.id !== blockId));\n    if (selectedBlock === blockId) {\n      setSelectedBlock(\"start\");\n    }\n  };\n\n  const saveFlow = async () => {\n    const flowInfoBlock = blocks.find(\n      (block) => block.type === BLOCK_TYPES.FLOW_INFO\n    );\n    const name = flowInfoBlock?.config?.name;\n    const description = flowInfoBlock?.config?.description;\n\n    if (!name?.trim() || !description?.trim()) {\n      // Make sure the flow info block is expanded first\n      if (!flowInfoBlock.isExpanded) {\n        setBlocks(\n          blocks.map((block) =>\n            block.type === BLOCK_TYPES.FLOW_INFO\n              ? { ...block, isExpanded: true }\n              : block\n          )\n        );\n        // Small delay to allow expansion animation to complete\n        await new Promise((resolve) => setTimeout(resolve, 100));\n      }\n\n      if (!name?.trim()) {\n        nameRef.current?.focus();\n      } else if (!description?.trim()) {\n        descriptionRef.current?.focus();\n      }\n      showToast(\n        \"Please provide both a name and description for your flow\",\n        \"error\",\n        {\n          clear: true,\n        }\n      );\n      return;\n    }\n\n    const flowConfig = {\n      name,\n      description,\n      active,\n      steps: blocks\n        .filter(\n          (block) =>\n            block.type !== BLOCK_TYPES.FINISH &&\n            block.type !== BLOCK_TYPES.FLOW_INFO\n        )\n        .map((block) => ({\n          type: block.type,\n          config: block.config,\n        })),\n    };\n\n    try {\n      const { success, error, flow } = await AgentFlows.saveFlow(\n        name,\n        flowConfig,\n        currentFlowUuid\n      );\n      if (!success) throw new Error(error);\n\n      setCurrentFlowUuid(flow.uuid);\n      showToast(\"Agent flow saved successfully!\", \"success\", { clear: true });\n      await loadAvailableFlows();\n    } catch (error) {\n      console.error(\"Save error details:\", error);\n      showToast(`Failed to save agent flow. ${error.message}`, \"error\", {\n        clear: true,\n      });\n    }\n  };\n\n  const toggleBlockExpansion = (blockId) => {\n    setBlocks(\n      blocks.map((block) =>\n        block.id === blockId\n          ? { ...block, isExpanded: !block.isExpanded }\n          : block\n      )\n    );\n  };\n\n  // Get all available variables from the start block\n  const getAvailableVariables = () => {\n    const startBlock = blocks.find((b) => b.type === BLOCK_TYPES.START);\n    return startBlock?.config?.variables?.filter((v) => v.name) || [];\n  };\n\n  const renderVariableSelect = (\n    value,\n    onChange,\n    placeholder = \"Select variable\"\n  ) => (\n    <select\n      value={value || \"\"}\n      onChange={(e) => onChange(e.target.value)}\n      className=\"w-full border-none bg-theme-settings-input-bg text-theme-text-primary text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n    >\n      <option value=\"\" className=\"bg-theme-bg-primary\">\n        {placeholder}\n      </option>\n      {getAvailableVariables().map((v) => (\n        <option key={v.name} value={v.name} className=\"bg-theme-bg-primary\">\n          {v.name}\n        </option>\n      ))}\n    </select>\n  );\n\n  const deleteVariable = (variableName) => {\n    // Clean up references in other blocks\n    blocks.forEach((block) => {\n      if (block.type === BLOCK_TYPES.START) return;\n\n      let configUpdated = false;\n      const newConfig = { ...block.config };\n\n      // Check and clean responseVariable/resultVariable\n      if (newConfig.responseVariable === variableName) {\n        newConfig.responseVariable = \"\";\n        configUpdated = true;\n      }\n      if (newConfig.resultVariable === variableName) {\n        newConfig.resultVariable = \"\";\n        configUpdated = true;\n      }\n\n      if (configUpdated) {\n        updateBlockConfig(block.id, newConfig);\n      }\n    });\n  };\n\n  const clearFlow = () => {\n    if (!!flowId) navigate(paths.agents.builder());\n    setAgentName(\"\");\n    setAgentDescription(\"\");\n    setCurrentFlowUuid(null);\n    setActive(true);\n    setBlocks(DEFAULT_BLOCKS);\n  };\n\n  const moveBlock = (fromIndex, toIndex) => {\n    const newBlocks = [...blocks];\n    const [movedBlock] = newBlocks.splice(fromIndex, 1);\n    newBlocks.splice(toIndex, 0, movedBlock);\n    setBlocks(newBlocks);\n  };\n\n  const handlePublishFlow = () => {\n    setShowPublishModal(true);\n  };\n\n  const flowInfoBlock = blocks.find(\n    (block) => block.type === BLOCK_TYPES.FLOW_INFO\n  );\n  const flowEntity = {\n    name: flowInfoBlock?.config?.name || \"\",\n    description: flowInfoBlock?.config?.description || \"\",\n    steps: blocks\n      .filter(\n        (block) =>\n          block.type !== BLOCK_TYPES.FINISH &&\n          block.type !== BLOCK_TYPES.FLOW_INFO\n      )\n      .map((block) => ({ type: block.type, config: block.config })),\n  };\n\n  return (\n    <div\n      style={{\n        backgroundImage:\n          theme === \"light\"\n            ? \"radial-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 0)\"\n            : \"radial-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 0)\",\n        backgroundSize: \"15px 15px\",\n        backgroundPosition: \"-7.5px -7.5px\",\n      }}\n      className=\"relative w-screen h-screen flex flex-col bg-theme-bg-primary overflow-clip\"\n    >\n      <PublishEntityModal\n        show={showPublishModal}\n        onClose={() => setShowPublishModal(false)}\n        entityType=\"agent-flow\"\n        entity={flowEntity}\n      />\n      <HeaderMenu\n        agentName={agentName}\n        availableFlows={availableFlows}\n        onNewFlow={clearFlow}\n        onSaveFlow={saveFlow}\n        onPublishFlow={handlePublishFlow}\n      />\n      <div className=\"flex-1 min-h-0 p-6 overflow-y-auto\">\n        <div\n          className={`max-w-xl mx-auto mt-14 ${showBlockMenu ? \"pb-52\" : \"\"}`}\n        >\n          <BlockList\n            blocks={blocks}\n            updateBlockConfig={updateBlockConfig}\n            removeBlock={removeBlock}\n            toggleBlockExpansion={toggleBlockExpansion}\n            renderVariableSelect={renderVariableSelect}\n            onDeleteVariable={deleteVariable}\n            moveBlock={moveBlock}\n            refs={{ nameRef, descriptionRef }}\n          />\n\n          <AddBlockMenu\n            blocks={blocks}\n            showBlockMenu={showBlockMenu}\n            setShowBlockMenu={setShowBlockMenu}\n            addBlock={addBlock}\n          />\n        </div>\n      </div>\n      <Tooltip\n        id=\"content-summarization-tooltip\"\n        place=\"top\"\n        delayShow={300}\n        className=\"tooltip !text-xs z-99\"\n      >\n        <p className=\"text-sm\">\n          When enabled, long webpage content will be automatically summarized to\n          reduce token usage.\n          <br />\n          <br />\n          Note: This may affect data quality and remove specific details from\n          the original content.\n        </p>\n      </Tooltip>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/AgentBuilder/nodes/ApiCallNode/index.jsx",
    "content": "/* eslint-disable react-hooks/refs */\nimport React, { useRef, useState } from \"react\";\nimport { Plus, X, CaretDown } from \"@phosphor-icons/react\";\n\nexport default function ApiCallNode({\n  config,\n  onConfigChange,\n  renderVariableSelect,\n}) {\n  const urlInputRef = useRef(null);\n  const [showVarMenu, setShowVarMenu] = useState(false);\n  const varButtonRef = useRef(null);\n\n  const handleHeaderChange = (index, field, value) => {\n    const newHeaders = [...(config.headers || [])];\n    newHeaders[index] = { ...newHeaders[index], [field]: value };\n    onConfigChange({ headers: newHeaders });\n  };\n\n  const addHeader = () => {\n    const newHeaders = [...(config.headers || []), { key: \"\", value: \"\" }];\n    onConfigChange({ headers: newHeaders });\n  };\n\n  const removeHeader = (index) => {\n    const newHeaders = [...(config.headers || [])].filter(\n      (_, i) => i !== index\n    );\n    onConfigChange({ headers: newHeaders });\n  };\n\n  const insertVariableAtCursor = (variableName) => {\n    if (!urlInputRef.current) return;\n\n    const input = urlInputRef.current;\n    const start = input.selectionStart;\n    const end = input.selectionEnd;\n    const currentValue = config.url;\n\n    const newValue =\n      currentValue.substring(0, start) +\n      \"${\" +\n      variableName +\n      \"}\" +\n      currentValue.substring(end);\n\n    onConfigChange({ url: newValue });\n    setShowVarMenu(false);\n\n    // Set cursor position after the inserted variable\n    setTimeout(() => {\n      const newPosition = start + variableName.length + 3; // +3 for ${}\n      input.setSelectionRange(newPosition, newPosition);\n      input.focus();\n    }, 0);\n  };\n\n  return (\n    <div className=\"space-y-4\">\n      <div>\n        <label className=\"block text-sm font-medium text-theme-text-primary mb-2\">\n          URL\n        </label>\n        <div className=\"flex gap-2\">\n          <input\n            ref={urlInputRef}\n            type=\"text\"\n            placeholder=\"https://api.example.com/endpoint\"\n            value={config.url}\n            onChange={(e) => onConfigChange({ url: e.target.value })}\n            className=\"flex-1 border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n          <div className=\"relative\">\n            <button\n              ref={varButtonRef}\n              onClick={() => setShowVarMenu(!showVarMenu)}\n              className=\"h-full px-3 rounded-lg border-none bg-theme-settings-input-bg text-theme-text-primary hover:bg-theme-action-menu-item-hover transition-colors duration-300 flex items-center gap-1\"\n              title=\"Insert variable\"\n            >\n              <Plus className=\"w-4 h-4\" />\n              <CaretDown className=\"w-3 h-3\" />\n            </button>\n            {showVarMenu && (\n              <div className=\"absolute right-0 top-[calc(100%+4px)] w-48 bg-theme-settings-input-bg border-none rounded-lg shadow-lg z-10\">\n                {renderVariableSelect(\n                  \"\",\n                  insertVariableAtCursor,\n                  \"Select variable to insert\",\n                  true\n                )}\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n\n      <div>\n        <label className=\"block text-sm font-medium text-theme-text-primary mb-2\">\n          Method\n        </label>\n        <select\n          value={config.method}\n          onChange={(e) => onConfigChange({ method: e.target.value })}\n          className=\"w-full border-none bg-theme-settings-input-bg text-theme-text-primary text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n        >\n          {[\"GET\", \"POST\", \"DELETE\", \"PUT\", \"PATCH\"].map((method) => (\n            <option\n              key={method}\n              value={method}\n              className=\"bg-theme-settings-input-bg\"\n            >\n              {method}\n            </option>\n          ))}\n        </select>\n      </div>\n\n      <div>\n        <div className=\"flex items-center justify-between mb-2\">\n          <label className=\"text-sm font-medium text-theme-text-primary\">\n            Headers\n          </label>\n          <button\n            onClick={addHeader}\n            className=\"p-1.5 rounded-lg border-none bg-theme-settings-input-bg text-theme-text-primary hover:bg-theme-action-menu-item-hover transition-colors duration-300\"\n            title=\"Add header\"\n          >\n            <Plus className=\"w-3.5 h-3.5\" />\n          </button>\n        </div>\n        <div className=\"space-y-2\">\n          {(config.headers || []).map((header, index) => (\n            <div key={index} className=\"flex gap-2\">\n              <input\n                type=\"text\"\n                placeholder=\"Header name\"\n                value={header.key}\n                onChange={(e) =>\n                  handleHeaderChange(index, \"key\", e.target.value)\n                }\n                className=\"flex-1 border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n                autoComplete=\"off\"\n                spellCheck={false}\n              />\n              <input\n                type=\"text\"\n                placeholder=\"Value\"\n                value={header.value}\n                onChange={(e) =>\n                  handleHeaderChange(index, \"value\", e.target.value)\n                }\n                className=\"flex-1 border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n                autoComplete=\"off\"\n                spellCheck={false}\n              />\n              <button\n                onClick={() => removeHeader(index)}\n                className=\"p-2.5 rounded-lg border-none bg-theme-settings-input-bg text-theme-text-primary hover:text-red-500 hover:border-red-500/20 hover:bg-red-500/10 transition-colors duration-300\"\n                title=\"Remove header\"\n              >\n                <X className=\"w-4 h-4\" />\n              </button>\n            </div>\n          ))}\n        </div>\n      </div>\n\n      {[\"POST\", \"PUT\", \"PATCH\"].includes(config.method) && (\n        <div>\n          <label className=\"block text-sm font-medium text-theme-text-primary mb-2\">\n            Request Body\n          </label>\n          <div className=\"space-y-2\">\n            <select\n              value={config.bodyType || \"json\"}\n              onChange={(e) => onConfigChange({ bodyType: e.target.value })}\n              className=\"w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-theme-text-primary focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none light:bg-theme-settings-input-bg light:border-black/10\"\n            >\n              <option\n                value=\"json\"\n                className=\"bg-theme-bg-primary light:bg-theme-settings-input-bg\"\n              >\n                JSON\n              </option>\n              <option\n                value=\"text\"\n                className=\"bg-theme-bg-primary light:bg-theme-settings-input-bg\"\n              >\n                Raw Text\n              </option>\n              <option\n                value=\"form\"\n                className=\"bg-theme-bg-primary light:bg-theme-settings-input-bg\"\n              >\n                Form Data\n              </option>\n            </select>\n            {config.bodyType === \"json\" ? (\n              <textarea\n                placeholder='{\"key\": \"value\"}'\n                value={config.body}\n                onChange={(e) => onConfigChange({ body: e.target.value })}\n                className=\"w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-theme-text-primary placeholder:text-theme-text-secondary/20 focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none light:bg-theme-settings-input-bg light:border-black/10 font-mono\"\n                rows={4}\n                autoComplete=\"off\"\n                spellCheck={false}\n              />\n            ) : config.bodyType === \"form\" ? (\n              <div className=\"space-y-2\">\n                {(config.formData || []).map((item, index) => (\n                  <div key={index} className=\"flex gap-2\">\n                    <input\n                      type=\"text\"\n                      placeholder=\"Key\"\n                      value={item.key}\n                      onChange={(e) => {\n                        const newFormData = [...(config.formData || [])];\n                        newFormData[index] = { ...item, key: e.target.value };\n                        onConfigChange({ formData: newFormData });\n                      }}\n                      className=\"flex-1 p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-theme-text-primary placeholder:text-theme-text-secondary/20 focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none light:bg-theme-settings-input-bg light:border-black/10\"\n                      autoComplete=\"off\"\n                      spellCheck={false}\n                    />\n                    <input\n                      type=\"text\"\n                      placeholder=\"Value\"\n                      value={item.value}\n                      onChange={(e) => {\n                        const newFormData = [...(config.formData || [])];\n                        newFormData[index] = { ...item, value: e.target.value };\n                        onConfigChange({ formData: newFormData });\n                      }}\n                      className=\"flex-1 p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-theme-text-primary placeholder:text-theme-text-secondary/20 focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none light:bg-theme-settings-input-bg light:border-black/10\"\n                      autoComplete=\"off\"\n                      spellCheck={false}\n                    />\n                    <button\n                      onClick={() => {\n                        const newFormData = [...(config.formData || [])].filter(\n                          (_, i) => i !== index\n                        );\n                        onConfigChange({ formData: newFormData });\n                      }}\n                      className=\"p-2.5 rounded-lg bg-theme-bg-primary border border-white/5 text-theme-text-primary hover:text-red-500 hover:border-red-500/20 hover:bg-red-500/10 transition-colors duration-300 light:bg-theme-settings-input-bg light:border-black/10\"\n                      title=\"Remove field\"\n                    >\n                      <X className=\"w-4 h-4\" />\n                    </button>\n                  </div>\n                ))}\n                <button\n                  onClick={() => {\n                    const newFormData = [\n                      ...(config.formData || []),\n                      { key: \"\", value: \"\" },\n                    ];\n                    onConfigChange({ formData: newFormData });\n                  }}\n                  className=\"w-full p-2.5 rounded-lg border-none bg-theme-settings-input-bg text-theme-text-primary hover:bg-theme-action-menu-item-hover transition-colors duration-300 text-sm\"\n                >\n                  Add Form Field\n                </button>\n              </div>\n            ) : (\n              <textarea\n                placeholder=\"Raw request body...\"\n                value={config.body}\n                onChange={(e) => onConfigChange({ body: e.target.value })}\n                className=\"w-full border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n                rows={4}\n                autoComplete=\"off\"\n                spellCheck={false}\n              />\n            )}\n          </div>\n        </div>\n      )}\n\n      <div>\n        <label className=\"block text-sm font-medium text-theme-text-primary mb-2\">\n          Store Response In\n        </label>\n        {renderVariableSelect(\n          config.responseVariable,\n          (value) => onConfigChange({ responseVariable: value }),\n          \"Select or create variable\"\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/AgentBuilder/nodes/CodeNode/index.jsx",
    "content": "import React from \"react\";\n\nexport default function CodeNode({\n  config,\n  onConfigChange,\n  renderVariableSelect,\n}) {\n  return (\n    <div className=\"space-y-4\">\n      <div>\n        <label className=\"block text-sm font-medium text-white mb-2\">\n          Language\n        </label>\n        <select\n          value={config.language}\n          onChange={(e) => onConfigChange({ language: e.target.value })}\n          className=\"w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none\"\n        >\n          <option value=\"javascript\" className=\"bg-theme-bg-primary\">\n            JavaScript\n          </option>\n          <option value=\"python\" className=\"bg-theme-bg-primary\">\n            Python\n          </option>\n          <option value=\"shell\" className=\"bg-theme-bg-primary\">\n            Shell\n          </option>\n        </select>\n      </div>\n      <div>\n        <label className=\"block text-sm font-medium text-white mb-2\">\n          Code\n        </label>\n        <textarea\n          placeholder=\"Enter code...\"\n          value={config.code}\n          onChange={(e) => onConfigChange({ code: e.target.value })}\n          className=\"w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white placeholder:text-white/20 focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none font-mono\"\n          rows={5}\n          autoComplete=\"off\"\n          spellCheck={false}\n        />\n      </div>\n      <div>\n        <label className=\"block text-sm font-medium text-white mb-2\">\n          Store Result In\n        </label>\n        {renderVariableSelect(\n          config.resultVariable,\n          (value) => onConfigChange({ resultVariable: value }),\n          \"Select or create variable\"\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/AgentBuilder/nodes/FileNode/index.jsx",
    "content": "import React from \"react\";\n\nexport default function FileNode({\n  config,\n  onConfigChange,\n  renderVariableSelect,\n}) {\n  return (\n    <div className=\"space-y-4\">\n      <div>\n        <label className=\"block text-sm font-medium text-white mb-2\">\n          Operation\n        </label>\n        <select\n          value={config.operation}\n          onChange={(e) => onConfigChange({ operation: e.target.value })}\n          className=\"w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none\"\n        >\n          <option value=\"read\" className=\"bg-theme-bg-primary\">\n            Read File\n          </option>\n          <option value=\"write\" className=\"bg-theme-bg-primary\">\n            Write File\n          </option>\n          <option value=\"append\" className=\"bg-theme-bg-primary\">\n            Append to File\n          </option>\n        </select>\n      </div>\n      <div>\n        <label className=\"block text-sm font-medium text-white mb-2\">\n          File Path\n        </label>\n        <input\n          type=\"text\"\n          placeholder=\"/path/to/file\"\n          value={config.path}\n          onChange={(e) => onConfigChange({ path: e.target.value })}\n          className=\"w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white placeholder:text-white/20 focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none\"\n          autoComplete=\"off\"\n          spellCheck={false}\n        />\n      </div>\n      {config.operation !== \"read\" && (\n        <div>\n          <label className=\"block text-sm font-medium text-white mb-2\">\n            Content\n          </label>\n          <textarea\n            placeholder=\"File content...\"\n            value={config.content}\n            onChange={(e) => onConfigChange({ content: e.target.value })}\n            className=\"w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white placeholder:text-white/20 focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none\"\n            rows={3}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      )}\n      <div>\n        <label className=\"block text-sm font-medium text-white mb-2\">\n          Store Result In\n        </label>\n        {renderVariableSelect(\n          config.resultVariable,\n          (value) => onConfigChange({ resultVariable: value }),\n          \"Select or create variable\"\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/AgentBuilder/nodes/FinishNode/index.jsx",
    "content": "import React from \"react\";\n\nexport default function FinishNode() {\n  return (\n    <div className=\"text-sm text-white/60\">\n      This is the end of your agent flow. All steps above will be executed in\n      sequence.\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/AgentBuilder/nodes/FlowInfoNode/index.jsx",
    "content": "/* eslint-disable react-hooks/refs */\nimport React, { forwardRef } from \"react\";\n\nconst FlowInfoNode = forwardRef(({ config, onConfigChange }, refs) => {\n  return (\n    <div className=\"space-y-4\">\n      <div>\n        <label className=\"block text-sm font-medium text-theme-text-primary mb-2\">\n          Flow Name\n        </label>\n        <div className=\"flex flex-col text-xs text-theme-text-secondary mt-2 mb-3\">\n          <p className=\"\">\n            It is important to give your flow a name that an LLM can easily\n            understand.\n          </p>\n          <p>\"SendMessageToDiscord\", \"CheckStockPrice\", \"CheckWeather\"</p>\n        </div>\n        <input\n          id=\"agent-flow-name-input\"\n          ref={refs?.nameRef}\n          type=\"text\"\n          placeholder=\"Enter flow name\"\n          value={config?.name || \"\"}\n          onChange={(e) =>\n            onConfigChange({\n              ...config,\n              name: e.target.value,\n            })\n          }\n          className=\"w-full border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n          autoComplete=\"off\"\n          spellCheck={false}\n        />\n      </div>\n\n      <div>\n        <label className=\"block text-sm font-medium text-theme-text-primary mb-2\">\n          Description\n        </label>\n        <div className=\"flex flex-col text-xs text-theme-text-secondary mt-2 mb-3\">\n          <p className=\"\">\n            It is equally important to give your flow a description that an LLM\n            can easily understand. Be sure to include the purpose of the flow,\n            the context it will be used in, and any other relevant information.\n          </p>\n        </div>\n        <textarea\n          ref={refs?.descriptionRef}\n          value={config?.description || \"\"}\n          onChange={(e) =>\n            onConfigChange({\n              ...config,\n              description: e.target.value,\n            })\n          }\n          className=\"w-full border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n          rows={3}\n          placeholder=\"Enter flow description\"\n        />\n      </div>\n    </div>\n  );\n});\n\nFlowInfoNode.displayName = \"FlowInfoNode\";\nexport default FlowInfoNode;\n"
  },
  {
    "path": "frontend/src/pages/Admin/AgentBuilder/nodes/LLMInstructionNode/index.jsx",
    "content": "import React from \"react\";\n\nexport default function LLMInstructionNode({\n  config,\n  onConfigChange,\n  renderVariableSelect,\n}) {\n  return (\n    <div className=\"space-y-4\">\n      <div>\n        <label className=\"block text-sm font-medium text-theme-text-primary mb-2\">\n          Instruction\n        </label>\n        <textarea\n          value={config?.instruction || \"\"}\n          onChange={(e) =>\n            onConfigChange({\n              ...config,\n              instruction: e.target.value,\n            })\n          }\n          className=\"w-full border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n          rows={3}\n          placeholder=\"Enter instructions for the LLM...\"\n        />\n      </div>\n\n      <div>\n        <label className=\"block text-sm font-medium text-theme-text-primary mb-2\">\n          Result Variable\n        </label>\n        {renderVariableSelect(\n          config.resultVariable,\n          (value) => onConfigChange({ ...config, resultVariable: value }),\n          \"Select or create variable\",\n          true\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/AgentBuilder/nodes/StartNode/index.jsx",
    "content": "import React from \"react\";\nimport { Plus, X } from \"@phosphor-icons/react\";\n\nexport default function StartNode({\n  config,\n  onConfigChange,\n  onDeleteVariable,\n}) {\n  const handleDeleteVariable = (index, variableName) => {\n    // First clean up references, then delete the variable\n    onDeleteVariable(variableName);\n    const newVars = config.variables.filter((_, i) => i !== index);\n    onConfigChange({ variables: newVars });\n  };\n\n  return (\n    <div className=\"space-y-4\">\n      <h3 className=\"text-sm font-medium text-theme-text-primary\">Variables</h3>\n      {config.variables.map((variable, index) => (\n        <div key={index} className=\"flex gap-2\">\n          <input\n            type=\"text\"\n            placeholder=\"Variable name\"\n            value={variable.name}\n            onChange={(e) => {\n              const newVars = [...config.variables];\n              newVars[index].name = e.target.value;\n              onConfigChange({ variables: newVars });\n            }}\n            className=\"flex-1 border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n          <input\n            type=\"text\"\n            placeholder=\"Initial value\"\n            value={variable.value}\n            onChange={(e) => {\n              const newVars = [...config.variables];\n              newVars[index].value = e.target.value;\n              onConfigChange({ variables: newVars });\n            }}\n            className=\"flex-1 border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n          {config.variables.length > 1 && (\n            <button\n              onClick={() => handleDeleteVariable(index, variable.name)}\n              className=\"p-2.5 rounded-lg border-none bg-theme-settings-input-bg text-theme-text-primary hover:text-red-500 hover:border-red-500/20 hover:bg-red-500/10 transition-colors duration-300\"\n              title=\"Delete variable\"\n            >\n              <X className=\"w-4 h-4\" />\n            </button>\n          )}\n          {index === config.variables.length - 1 && (\n            <button\n              onClick={() => {\n                const newVars = [...config.variables, { name: \"\", value: \"\" }];\n                onConfigChange({ variables: newVars });\n              }}\n              className=\"p-2.5 rounded-lg border-none bg-theme-settings-input-bg text-theme-text-primary hover:bg-theme-action-menu-item-hover transition-colors duration-300\"\n              title=\"Add variable\"\n            >\n              <Plus className=\"w-4 h-4\" />\n            </button>\n          )}\n        </div>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/AgentBuilder/nodes/WebScrapingNode/index.jsx",
    "content": "import Toggle from \"@/components/lib/Toggle\";\n\nexport default function WebScrapingNode({\n  config,\n  onConfigChange,\n  renderVariableSelect,\n}) {\n  return (\n    <div className=\"space-y-4\">\n      <div>\n        <label className=\"block text-sm font-medium text-theme-text-primary mb-2\">\n          URL to Scrape\n        </label>\n        <input\n          type=\"url\"\n          value={config?.url || \"\"}\n          onChange={(e) =>\n            onConfigChange({\n              ...config,\n              url: e.target.value,\n            })\n          }\n          className=\"w-full border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n          placeholder=\"https://example.com\"\n        />\n      </div>\n\n      <div>\n        <label className=\"block text-sm font-medium text-theme-text-primary mb-2\">\n          Capture Page Content As\n        </label>\n        <select\n          value={config.captureAs}\n          onChange={(e) =>\n            onConfigChange({ ...config, captureAs: e.target.value })\n          }\n          className=\"w-full border-none bg-theme-settings-input-bg text-theme-text-primary text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n        >\n          {[\n            { label: \"Text content only\", value: \"text\" },\n            { label: \"Raw HTML\", value: \"html\" },\n            { label: \"CSS Query Selector\", value: \"querySelector\" },\n          ].map((captureAs) => (\n            <option\n              key={captureAs.value}\n              value={captureAs.value}\n              className=\"bg-theme-settings-input-bg\"\n            >\n              {captureAs.label}\n            </option>\n          ))}\n        </select>\n      </div>\n\n      {config.captureAs === \"querySelector\" && (\n        <div>\n          <label className=\"block text-sm font-medium text-theme-text-primary mb-2\">\n            Query Selector\n          </label>\n          <p className=\"text-xs text-theme-text-secondary mb-2\">\n            Enter a valid CSS selector to scrape the content of the page.\n          </p>\n          <input\n            value={config.querySelector}\n            onChange={(e) =>\n              onConfigChange({ ...config, querySelector: e.target.value })\n            }\n            placeholder=\".article-content, #content, .main-content, etc.\"\n            className=\"w-full border-none bg-theme-settings-input-bg text-theme-text-primary text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5\"\n          />\n        </div>\n      )}\n\n      <Toggle\n        size=\"md\"\n        variant=\"horizontal\"\n        label=\"Content Summarization\"\n        hint=\"content-summarization-tooltip\"\n        enabled={config.enableSummarization ?? true}\n        onChange={(checked) =>\n          onConfigChange({ ...config, enableSummarization: checked })\n        }\n      />\n      <div>\n        <label className=\"block text-sm font-medium text-theme-text-primary mb-2\">\n          Result Variable\n        </label>\n        {renderVariableSelect(\n          config.resultVariable,\n          (value) => onConfigChange({ ...config, resultVariable: value }),\n          \"Select or create variable\",\n          true\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/AgentBuilder/nodes/WebsiteNode/index.jsx",
    "content": "import React from \"react\";\n\nexport default function WebsiteNode({\n  config,\n  onConfigChange,\n  renderVariableSelect,\n}) {\n  return (\n    <div className=\"space-y-4\">\n      <div>\n        <label className=\"block text-sm font-medium text-white mb-2\">URL</label>\n        <input\n          type=\"text\"\n          placeholder=\"https://example.com\"\n          value={config.url}\n          onChange={(e) => onConfigChange({ url: e.target.value })}\n          className=\"w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white placeholder:text-white/20 focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none\"\n          autoComplete=\"off\"\n          spellCheck={false}\n        />\n      </div>\n      <div>\n        <label className=\"block text-sm font-medium text-white mb-2\">\n          Action\n        </label>\n        <select\n          value={config.action}\n          onChange={(e) => onConfigChange({ action: e.target.value })}\n          className=\"w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none\"\n        >\n          <option value=\"read\" className=\"bg-theme-bg-primary\">\n            Read Content\n          </option>\n          <option value=\"click\" className=\"bg-theme-bg-primary\">\n            Click Element\n          </option>\n          <option value=\"type\" className=\"bg-theme-bg-primary\">\n            Type Text\n          </option>\n        </select>\n      </div>\n      <div>\n        <label className=\"block text-sm font-medium text-white mb-2\">\n          CSS Selector\n        </label>\n        <input\n          type=\"text\"\n          placeholder=\"#element-id or .class-name\"\n          value={config.selector}\n          onChange={(e) => onConfigChange({ selector: e.target.value })}\n          className=\"w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white placeholder:text-white/20 focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none\"\n          autoComplete=\"off\"\n          spellCheck={false}\n        />\n      </div>\n      <div>\n        <label className=\"block text-sm font-medium text-white mb-2\">\n          Store Result In\n        </label>\n        {renderVariableSelect(\n          config.resultVariable,\n          (value) => onConfigChange({ resultVariable: value }),\n          \"Select or create variable\"\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/AgentFlows/FlowPanel.jsx",
    "content": "import React, { useState, useEffect, useRef } from \"react\";\nimport AgentFlows from \"@/models/agentFlows\";\nimport showToast from \"@/utils/toast\";\nimport { FlowArrow, Gear } from \"@phosphor-icons/react\";\nimport { useNavigate } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nfunction ManageFlowMenu({ flow, onDelete }) {\n  const [open, setOpen] = useState(false);\n  const menuRef = useRef(null);\n  const navigate = useNavigate();\n\n  async function deleteFlow() {\n    if (\n      !window.confirm(\n        \"Are you sure you want to delete this flow? This action cannot be undone.\"\n      )\n    )\n      return;\n    const { success, error } = await AgentFlows.deleteFlow(flow.uuid);\n    if (success) {\n      showToast(\"Flow deleted successfully.\", \"success\");\n      onDelete(flow.uuid);\n    } else {\n      showToast(error || \"Failed to delete flow.\", \"error\");\n    }\n  }\n\n  useEffect(() => {\n    const handleClickOutside = (event) => {\n      if (menuRef.current && !menuRef.current.contains(event.target)) {\n        setOpen(false);\n      }\n    };\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, []);\n\n  return (\n    <div className=\"relative\" ref={menuRef}>\n      <button\n        type=\"button\"\n        onClick={() => setOpen(!open)}\n        className=\"p-1.5 rounded-lg text-white hover:bg-theme-action-menu-item-hover transition-colors duration-300\"\n      >\n        <Gear className=\"h-5 w-5\" weight=\"bold\" />\n      </button>\n      {open && (\n        <div className=\"absolute w-[100px] -top-1 left-7 mt-1 border-[1.5px] border-white/40 rounded-lg bg-theme-action-menu-bg flex flex-col shadow-[0_4px_14px_rgba(0,0,0,0.25)] text-white z-99 md:z-10\">\n          <button\n            type=\"button\"\n            onClick={() => navigate(paths.agents.editAgent(flow.uuid))}\n            className=\"border-none flex items-center rounded-lg gap-x-2 hover:bg-theme-action-menu-item-hover py-1.5 px-2 transition-colors duration-200 w-full text-left\"\n          >\n            <span className=\"text-sm\">Edit Flow</span>\n          </button>\n          <button\n            type=\"button\"\n            onClick={deleteFlow}\n            className=\"border-none flex items-center rounded-lg gap-x-2 hover:bg-theme-action-menu-item-hover py-1.5 px-2 transition-colors duration-200 w-full text-left\"\n          >\n            <span className=\"text-sm\">Delete Flow</span>\n          </button>\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default function FlowPanel({ flow, toggleFlow, onDelete }) {\n  const [isActive, setIsActive] = useState(flow.active);\n\n  useEffect(() => {\n    setIsActive(flow.active);\n  }, [flow.uuid, flow.active]);\n\n  const handleToggle = async () => {\n    try {\n      const { success, error } = await AgentFlows.toggleFlow(\n        flow.uuid,\n        !isActive\n      );\n      if (!success) throw new Error(error);\n      setIsActive(!isActive);\n      toggleFlow(flow.uuid);\n      showToast(\"Flow status updated successfully\", \"success\", { clear: true });\n    } catch (error) {\n      console.error(\"Failed to toggle flow:\", error);\n      showToast(\"Failed to toggle flow\", \"error\", { clear: true });\n    }\n  };\n\n  return (\n    <>\n      <div className=\"p-2\">\n        <div className=\"flex flex-col gap-y-[18px] max-w-[500px]\">\n          <div className=\"flex w-full justify-between items-center\">\n            <div className=\"flex items-center gap-x-2\">\n              <FlowArrow size={24} weight=\"bold\" className=\"text-white\" />\n              <label htmlFor=\"name\" className=\"text-white text-md font-bold\">\n                {flow.name}\n              </label>\n            </div>\n            <div className=\"flex items-center gap-x-2\">\n              <Toggle size=\"lg\" enabled={isActive} onChange={handleToggle} />\n              <ManageFlowMenu flow={flow} onDelete={onDelete} />\n            </div>\n          </div>\n          <p className=\"whitespace-pre-wrap text-white text-opacity-60 text-xs font-medium py-1.5\">\n            {flow.description || \"No description provided\"}\n          </p>\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/AgentFlows/index.jsx",
    "content": "import React from \"react\";\nimport { CaretRight } from \"@phosphor-icons/react\";\n\nexport default function AgentFlowsList({\n  flows = [],\n  selectedFlow,\n  handleClick,\n}) {\n  if (flows.length === 0) {\n    return (\n      <div className=\"text-theme-text-secondary text-center text-xs flex flex-col gap-y-2\">\n        <p>No agent flows found</p>\n        <a\n          href=\"https://docs.anythingllm.com/agent-flows/getting-started\"\n          target=\"_blank\"\n          className=\"text-theme-text-secondary underline hover:text-cta-button\"\n          rel=\"noreferrer\"\n        >\n          Learn more about Agent Flows.\n        </a>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"bg-theme-bg-secondary text-white rounded-xl w-full md:min-w-[360px]\">\n      {flows.map((flow, index) => (\n        <div\n          key={flow.uuid}\n          className={`py-3 px-4 flex items-center justify-between ${\n            index === 0 ? \"rounded-t-xl\" : \"\"\n          } ${\n            index === flows.length - 1\n              ? \"rounded-b-xl\"\n              : \"border-b border-white/10\"\n          } cursor-pointer transition-all duration-300 hover:bg-theme-bg-primary ${\n            selectedFlow?.uuid === flow.uuid\n              ? \"bg-white/10 light:bg-theme-bg-sidebar\"\n              : \"\"\n          }`}\n          onClick={() => handleClick?.(flow)}\n        >\n          <div className=\"text-sm font-light\">{flow.name}</div>\n          <div className=\"flex items-center gap-x-2\">\n            <div className=\"text-sm text-theme-text-secondary font-medium\">\n              {flow.active ? \"On\" : \"Off\"}\n            </div>\n            <CaretRight\n              size={14}\n              weight=\"bold\"\n              className=\"text-theme-text-secondary\"\n            />\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/AgentSkillSettings/index.jsx",
    "content": "import { useModal } from \"@/hooks/useModal\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { CircleNotch, SlidersHorizontal, X } from \"@phosphor-icons/react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport Toggle from \"@/components/lib/Toggle\";\nimport System from \"@/models/system\";\nimport debounce from \"lodash.debounce\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function AgentSkillSettings() {\n  const { isOpen, openModal, closeModal } = useModal();\n  return (\n    <>\n      <button\n        type=\"button\"\n        onClick={openModal}\n        className={`w-10 h-10 flex items-center justify-center light:border-black/10 light:border-solid border-none light:!border rounded-lg transition-colors outline-none bg-transparent hover:bg-theme-bg-secondary`}\n      >\n        <SlidersHorizontal size={24} className={`text-theme-text-secondary`} />\n      </button>\n      <AgentSkillSettingsModal isOpen={isOpen} closeModal={closeModal} />\n    </>\n  );\n}\n\nfunction AgentSkillSettingsModal({ isOpen, closeModal }) {\n  const { t } = useTranslation();\n  if (!isOpen) return null;\n\n  return (\n    <ModalWrapper isOpen={isOpen}>\n      <div className=\"w-[500px] bg-theme-bg-sidebar px-6 py-4 rounded-lg flex flex-col items-center justify-between relative shadow-lg border border-white/10\">\n        <div className=\"w-full flex items-center justify-between\">\n          <div className=\"text-white text-left font-medium text-lg\">\n            {t(\"agent.settings.title\")}\n          </div>\n          <button\n            onClick={closeModal}\n            className=\"text-white opacity-60 hover:text-white hover:opacity-100 border-none outline-none\"\n          >\n            <X size={20} />\n          </button>\n        </div>\n\n        <div className=\"flex flex-col w-full\">\n          <div className=\"flex flex-col gap-y-5 w-full\">\n            <MaxToolCallStack />\n            <div className=\"border-b border-white/10 h-[1px] w-full\" />\n            <AgentSkillReranker />\n          </div>\n        </div>\n      </div>\n    </ModalWrapper>\n  );\n}\n\nfunction MaxToolCallStack() {\n  const { t } = useTranslation();\n  const [maxCallStack, setMaxCallStack] = useState(10);\n  const [loading, setLoading] = useState(true);\n\n  const debouncedUpdateMaxCallStack = useMemo(\n    () =>\n      debounce(async (newMaxCallStack) => {\n        await System.updateSystem({\n          AgentSkillMaxToolCalls: newMaxCallStack.toString(),\n        });\n      }, 800),\n    []\n  );\n\n  useEffect(() => {\n    System.keys()\n      .then((res) => {\n        setMaxCallStack(parseInt(res.AgentSkillMaxToolCalls));\n      })\n      .finally(() => {\n        setLoading(false);\n      });\n  }, []);\n\n  useEffect(() => {\n    return () => debouncedUpdateMaxCallStack.cancel();\n  }, [debouncedUpdateMaxCallStack]);\n\n  return (\n    <div className=\"flex flex-col gap-y-2 mt-4\">\n      <div className=\"flex items-center gap-x-4 mt-2\">\n        <div className=\"flex flex-col gap-y-1 flex-1\">\n          <label className=\"block text-md font-medium text-white\">\n            {t(\"agent.settings.max-tool-calls.title\")}\n          </label>\n          <p className=\"text-xs text-white/60\">\n            {t(\"agent.settings.max-tool-calls.description\")}\n          </p>\n        </div>\n        <input\n          type=\"number\"\n          name=\"agentSkillMaxToolCalls\"\n          min={1}\n          value={maxCallStack}\n          disabled={loading}\n          onChange={(e) => {\n            if (e.target.value < 1) return;\n            debouncedUpdateMaxCallStack(e.target.value);\n            setMaxCallStack(parseInt(e.target.value));\n          }}\n          onWheel={(e) => e.target.blur()}\n          className=\"border border-white/10 bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-[80px] p-2.5 text-center\"\n          placeholder=\"10\"\n          autoComplete=\"off\"\n        />\n      </div>\n    </div>\n  );\n}\n\nfunction AgentSkillReranker() {\n  const { t } = useTranslation();\n  const [enabled, setEnabled] = useState(false);\n  const [maxTools, setMaxTools] = useState(15);\n  const [loading, setLoading] = useState(true);\n\n  const debouncedUpdateMaxTools = useMemo(\n    () =>\n      debounce(async (newMaxToolsCount) => {\n        await System.updateSystem({\n          AgentSkillRerankerTopN: newMaxToolsCount.toString(),\n        });\n      }, 800),\n    []\n  );\n\n  useEffect(() => {\n    System.keys()\n      .then((res) => {\n        setEnabled(res.AgentSkillRerankerEnabled);\n        setMaxTools(parseInt(res.AgentSkillRerankerTopN));\n      })\n      .finally(() => {\n        setLoading(false);\n      });\n  }, []);\n\n  useEffect(() => {\n    return () => debouncedUpdateMaxTools.cancel();\n  }, [debouncedUpdateMaxTools]);\n\n  async function toggleEnabled(enabled) {\n    setEnabled(enabled);\n    await System.updateSystem({\n      AgentSkillRerankerEnabled: String(enabled),\n    });\n  }\n\n  return (\n    <div className=\"flex flex-col gap-y-4\">\n      <div className=\"flex items-center gap-x-1\">\n        <label className=\"block text-md font-medium text-white flex items-center gap-x-1\">\n          {t(\"agent.settings.intelligent-skill-selection.title\")}{\" \"}\n          <i className=\"ml-1 text-xs text-white pl-2 bg-blue-500/40 rounded-md px-2 py-0.5\">\n            {t(\"agent.settings.intelligent-skill-selection.beta-badge\")}\n          </i>\n        </label>\n      </div>\n      <div className=\"flex items-center gap-x-4\">\n        <p className=\"text-xs text-white/60\">\n          {t(\"agent.settings.intelligent-skill-selection.description\")}\n        </p>\n        {loading ? (\n          <CircleNotch\n            size={16}\n            className=\"shrink-0 animate-spin text-theme-text-primary\"\n          />\n        ) : (\n          <Toggle\n            size=\"lg\"\n            name=\"agentSkillRerankerEnabled\"\n            enabled={enabled}\n            onChange={toggleEnabled}\n          />\n        )}\n      </div>\n      {enabled && (\n        <div className=\"flex items-center gap-x-4\">\n          <div className=\"flex flex-col gap-y-1 flex-1\">\n            <label className=\"block text-md font-medium text-white\">\n              {t(\"agent.settings.intelligent-skill-selection.max-tools.title\")}\n            </label>\n            <p className=\"text-xs text-white/60\">\n              {t(\n                \"agent.settings.intelligent-skill-selection.max-tools.description\"\n              )}\n            </p>\n          </div>\n          <input\n            type=\"number\"\n            name=\"agentSkillRerankerTopN\"\n            min={10}\n            value={maxTools}\n            onChange={(e) => {\n              if (e.target.value < 10) return;\n              debouncedUpdateMaxTools(e.target.value);\n              setMaxTools(parseInt(e.target.value));\n            }}\n            onWheel={(e) => e.target.blur()}\n            className=\"border border-white/10 bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-[80px] p-2.5 text-center\"\n            placeholder=\"15\"\n            autoComplete=\"off\"\n          />\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/Badges/default.jsx",
    "content": "export function DefaultBadge({ title: _title }) {\n  return (\n    <>\n      <span\n        className=\"w-fit\"\n        data-tooltip-id=\"default-skill\"\n        data-tooltip-content=\"This skill is enabled by default and cannot be turned off.\"\n      >\n        <div className=\"flex items-center gap-x-1 w-fit rounded-full bg-[#F4FFD0]/10 light:bg-blue-100 px-2.5 py-0.5 text-sm font-medium text-sky-400 light:text-theme-text-secondary shadow-sm cursor-pointer\">\n          <div className=\"text-[#F4FFD0] light:text-blue-600 text-[12px] leading-[15px]\">\n            Default\n          </div>\n        </div>\n      </span>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/DefaultSkillPanel/index.jsx",
    "content": "import React from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { DefaultBadge } from \"../Badges/default\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nexport default function DefaultSkillPanel({\n  title,\n  description,\n  image,\n  icon,\n  enabled = true,\n  toggleSkill,\n  skill,\n}) {\n  const { t } = useTranslation();\n  return (\n    <div className=\"p-2\">\n      <div className=\"flex flex-col gap-y-[18px] max-w-[500px]\">\n        <div className=\"flex w-full justify-between items-center\">\n          <div className=\"flex items-center gap-x-2\">\n            {icon &&\n              React.createElement(icon, {\n                size: 24,\n                color: \"var(--theme-text-primary)\",\n                weight: \"bold\",\n              })}\n            <label\n              htmlFor=\"name\"\n              className=\"text-theme-text-primary text-md font-bold\"\n            >\n              {title}\n            </label>\n            <DefaultBadge title={title} />\n          </div>\n          <Toggle\n            size=\"lg\"\n            enabled={enabled}\n            onChange={() => toggleSkill(skill)}\n          />\n        </div>\n        <img src={image} alt={title} className=\"w-full rounded-md\" />\n        <p className=\"text-theme-text-secondary text-opacity-60 text-xs font-medium py-1.5\">\n          {description}\n          <br />\n          <br />\n          {t(\"agent.skill.default_skill\")}\n        </p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/GenericSkillPanel/index.jsx",
    "content": "import React from \"react\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nexport default function GenericSkillPanel({\n  title,\n  description,\n  skill,\n  toggleSkill,\n  enabled = false,\n  disabled = false,\n  image,\n  icon,\n}) {\n  return (\n    <div className=\"p-2\">\n      <div className=\"flex flex-col gap-y-[18px] max-w-[500px]\">\n        <div className=\"flex w-full justify-between items-center\">\n          <div className=\"flex items-center gap-x-2\">\n            {icon &&\n              React.createElement(icon, {\n                size: 24,\n                color: \"var(--theme-text-primary)\",\n                weight: \"bold\",\n              })}\n            <label\n              htmlFor=\"name\"\n              className=\"text-theme-text-primary text-md font-bold\"\n            >\n              {title}\n            </label>\n          </div>\n          <Toggle\n            size=\"lg\"\n            enabled={enabled}\n            disabled={disabled}\n            onChange={() => toggleSkill(skill)}\n          />\n        </div>\n        <img src={image} alt={title} className=\"w-full rounded-md\" />\n        <p className=\"text-theme-text-secondary text-opacity-60 text-xs font-medium py-1.5\">\n          {description}\n        </p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/Imported/ImportedSkillConfig/index.jsx",
    "content": "import System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { Gear, Plug } from \"@phosphor-icons/react\";\nimport { useEffect, useState, useRef } from \"react\";\nimport { sentenceCase } from \"text-case\";\nimport Toggle from \"@/components/lib/Toggle\";\n\n/**\n * Converts setup_args to inputs for the form builder\n * @param {object} setupArgs - The setup arguments object\n * @returns {object} - The inputs object\n */\nfunction inputsFromArgs(setupArgs) {\n  if (\n    !setupArgs ||\n    setupArgs.constructor?.call?.().toString() !== \"[object Object]\"\n  ) {\n    return {};\n  }\n  return Object.entries(setupArgs).reduce(\n    (acc, [key, props]) => ({\n      ...acc,\n      [key]: props.hasOwnProperty(\"value\")\n        ? props.value\n        : props?.input?.default || \"\",\n    }),\n    {}\n  );\n}\n\n/**\n * Imported skill config component for imported skills only.\n * @returns {JSX.Element}\n */\nexport default function ImportedSkillConfig({\n  selectedSkill, // imported skill config object\n  setImportedSkills, // function to set imported skills since config is file-write\n}) {\n  const [config, setConfig] = useState(selectedSkill);\n  const [hasChanges, setHasChanges] = useState(false);\n  const [inputs, setInputs] = useState(\n    inputsFromArgs(selectedSkill?.setup_args)\n  );\n\n  const hasSetupArgs =\n    selectedSkill?.setup_args &&\n    Object.keys(selectedSkill.setup_args).length > 0;\n\n  async function toggleSkill() {\n    const updatedConfig = { ...selectedSkill, active: !config.active };\n    await System.experimentalFeatures.agentPlugins.updatePluginConfig(\n      config.hubId,\n      { active: !config.active }\n    );\n    setImportedSkills((prev) =>\n      prev.map((s) => (s.hubId === config.hubId ? updatedConfig : s))\n    );\n    setConfig(updatedConfig);\n    showToast(\n      `Skill ${updatedConfig.active ? \"activated\" : \"deactivated\"}.`,\n      \"success\",\n      { clear: true }\n    );\n  }\n\n  async function handleSubmit(e) {\n    e.preventDefault();\n    const errors = [];\n    const updatedConfig = { ...config };\n\n    for (const [key, value] of Object.entries(inputs)) {\n      const settings = config.setup_args[key];\n      if (settings.required && !value) {\n        errors.push(`${key} is required to have a value.`);\n        continue;\n      }\n      if (typeof value !== settings.type) {\n        errors.push(`${key} must be of type ${settings.type}.`);\n        continue;\n      }\n      updatedConfig.setup_args[key].value = value;\n    }\n\n    if (errors.length > 0) {\n      errors.forEach((error) => showToast(error, \"error\"));\n      return;\n    }\n\n    await System.experimentalFeatures.agentPlugins.updatePluginConfig(\n      config.hubId,\n      updatedConfig\n    );\n    setConfig(updatedConfig);\n    setImportedSkills((prev) =>\n      prev.map((skill) =>\n        skill.hubId === config.hubId ? updatedConfig : skill\n      )\n    );\n    showToast(\"Skill config updated successfully.\", \"success\");\n    setHasChanges(false);\n  }\n\n  useEffect(() => {\n    setHasChanges(\n      JSON.stringify(inputs) !==\n        JSON.stringify(inputsFromArgs(selectedSkill.setup_args))\n    );\n  }, [inputs]);\n\n  return (\n    <>\n      <div className=\"p-2\">\n        <div className=\"flex flex-col gap-y-[18px] max-w-[500px]\">\n          <div className=\"flex w-full justify-between items-center\">\n            <div className=\"flex items-center gap-x-2\">\n              <Plug size={24} weight=\"bold\" className=\"text-white\" />\n              <label htmlFor=\"name\" className=\"text-white text-md font-bold\">\n                {sentenceCase(config.name)}\n              </label>\n            </div>\n            <div className=\"flex items-center gap-x-2\">\n              <Toggle\n                size=\"lg\"\n                enabled={config.active}\n                onChange={toggleSkill}\n              />\n              <ManageSkillMenu\n                config={config}\n                setImportedSkills={setImportedSkills}\n              />\n            </div>\n          </div>\n          <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n            {config.description} by{\" \"}\n            <a\n              href={config.author_url}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-white hover:underline\"\n            >\n              {config.author}\n            </a>\n          </p>\n\n          {hasSetupArgs ? (\n            <div className=\"flex flex-col gap-y-2\">\n              {Object.entries(config.setup_args).map(([key, props]) => (\n                <div key={key} className=\"flex flex-col gap-y-1\">\n                  <label htmlFor={key} className=\"text-white text-sm font-bold\">\n                    {key}\n                  </label>\n                  <input\n                    type={props?.input?.type || \"text\"}\n                    required={props?.input?.required}\n                    defaultValue={\n                      props.hasOwnProperty(\"value\")\n                        ? props.value\n                        : props?.input?.default || \"\"\n                    }\n                    onChange={(e) =>\n                      setInputs({ ...inputs, [key]: e.target.value })\n                    }\n                    placeholder={props?.input?.placeholder || \"\"}\n                    className=\"border-solid bg-transparent border border-white light:border-black rounded-md p-2 text-white text-sm\"\n                  />\n                  <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n                    {props?.input?.hint}\n                  </p>\n                </div>\n              ))}\n              {hasChanges && (\n                <button\n                  onClick={handleSubmit}\n                  type=\"button\"\n                  className=\"bg-blue-500 text-white light:text-white rounded-md p-2\"\n                >\n                  Save\n                </button>\n              )}\n            </div>\n          ) : (\n            <p className=\"text-white text-opacity-60 text-sm font-medium py-1.5\">\n              There are no options to modify for this skill.\n            </p>\n          )}\n        </div>\n      </div>\n    </>\n  );\n}\n\nfunction ManageSkillMenu({ config, setImportedSkills }) {\n  const [open, setOpen] = useState(false);\n  const menuRef = useRef(null);\n\n  async function deleteSkill() {\n    if (\n      !window.confirm(\n        \"Are you sure you want to delete this skill? This action cannot be undone.\"\n      )\n    )\n      return;\n    const success = await System.experimentalFeatures.agentPlugins.deletePlugin(\n      config.hubId\n    );\n    if (success) {\n      setImportedSkills((prev) => prev.filter((s) => s.hubId !== config.hubId));\n      showToast(\"Skill deleted successfully.\", \"success\");\n      setOpen(false);\n    } else {\n      showToast(\"Failed to delete skill.\", \"error\");\n    }\n  }\n\n  useEffect(() => {\n    const handleClickOutside = (event) => {\n      if (menuRef.current && !menuRef.current.contains(event.target)) {\n        setOpen(false);\n      }\n    };\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, []);\n\n  if (!config.hubId) return null;\n  return (\n    <div className=\"relative\" ref={menuRef}>\n      <button\n        type=\"button\"\n        onClick={() => setOpen(!open)}\n        className=\"p-1.5 rounded-lg text-white hover:bg-theme-action-menu-item-hover transition-colors duration-300\"\n      >\n        <Gear className=\"h-5 w-5\" weight=\"bold\" />\n      </button>\n      {open && (\n        <div className=\"absolute w-[100px] -top-1 left-7 mt-1 border-[1.5px] border-white/40 rounded-lg bg-theme-action-menu-bg flex flex-col shadow-[0_4px_14px_rgba(0,0,0,0.25)] text-white z-99 md:z-10\">\n          <button\n            type=\"button\"\n            onClick={deleteSkill}\n            className=\"border-none flex items-center rounded-lg gap-x-2 hover:bg-theme-action-menu-item-hover py-1.5 px-2 transition-colors duration-200 w-full text-left\"\n          >\n            <span className=\"text-sm\">Delete Skill</span>\n          </button>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/Imported/SkillList/index.jsx",
    "content": "import { CaretRight } from \"@phosphor-icons/react\";\nimport { sentenceCase } from \"text-case\";\n\nexport default function ImportedSkillList({\n  skills = [],\n  selectedSkill = null,\n  handleClick = null,\n}) {\n  if (skills.length === 0)\n    return (\n      <div className=\"text-theme-text-secondary text-center text-xs flex flex-col gap-y-2\">\n        <p>No imported skills found</p>\n        <p>\n          Learn about agent skills in the{\" \"}\n          <a\n            href=\"https://docs.anythingllm.com/agent/custom/developer-guide\"\n            target=\"_blank\"\n            className=\"text-theme-text-secondary underline hover:text-cta-button\"\n            rel=\"noreferrer\"\n          >\n            AnythingLLM Agent Docs\n          </a>\n          .\n        </p>\n      </div>\n    );\n\n  return (\n    <div\n      className={`bg-theme-bg-secondary text-white rounded-xl w-full md:min-w-[360px]`}\n    >\n      {skills.map((config, index) => (\n        <div\n          key={config.hubId}\n          className={`py-3 px-4 flex items-center justify-between ${\n            index === 0 ? \"rounded-t-xl\" : \"\"\n          } ${\n            index === Object.keys(skills).length - 1\n              ? \"rounded-b-xl\"\n              : \"border-b border-white/10\"\n          } cursor-pointer transition-all duration-300 hover:bg-theme-bg-primary ${\n            selectedSkill === config.hubId ? \"bg-theme-bg-primary\" : \"\"\n          }`}\n          onClick={() => handleClick?.({ ...config, imported: true })}\n        >\n          <div className=\"text-sm font-light\">{sentenceCase(config.name)}</div>\n          <div className=\"flex items-center gap-x-2\">\n            <div className=\"text-sm text-theme-text-secondary font-medium\">\n              {config.active ? \"On\" : \"Off\"}\n            </div>\n            <CaretRight\n              size={14}\n              weight=\"bold\"\n              className=\"text-theme-text-secondary\"\n            />\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/MCPServers/ServerPanel.jsx",
    "content": "import React, { useState, useEffect, useRef } from \"react\";\nimport showToast from \"@/utils/toast\";\nimport { CaretDown, Gear, Warning } from \"@phosphor-icons/react\";\nimport MCPLogo from \"@/media/agents/mcp-logo.svg\";\nimport { titleCase } from \"text-case\";\nimport MCPServers from \"@/models/mcpServers\";\nimport { SimpleToggleSwitch } from \"@/components/lib/Toggle\";\nimport { useTranslation, Trans } from \"react-i18next\";\n\nfunction ManageServerMenu({ server, toggleServer, onDelete }) {\n  const { t } = useTranslation();\n  const [open, setOpen] = useState(false);\n  const [running, setRunning] = useState(server.running);\n  const menuRef = useRef(null);\n\n  async function deleteServer() {\n    if (\n      !window.confirm(\n        \"Are you sure you want to delete this MCP server? It will be removed from your config file and you will need to add it back manually.\"\n      )\n    )\n      return;\n    const { success, error } = await MCPServers.deleteServer(server.name);\n    if (success) {\n      showToast(\"MCP server deleted successfully.\", \"success\");\n      onDelete(server.name);\n    } else {\n      showToast(error || \"Failed to delete MCP server.\", \"error\");\n    }\n  }\n\n  async function handleToggleServer() {\n    if (\n      !window.confirm(\n        running\n          ? \"Are you sure you want to stop this MCP server? It will be started automatically when you next start the server.\"\n          : \"Are you sure you want to start this MCP server? It will be started automatically when you next start the server.\"\n      )\n    )\n      return;\n\n    const { success, error } = await MCPServers.toggleServer(server.name);\n    if (success) {\n      const newState = !running;\n      setRunning(newState);\n      toggleServer(server.name);\n      showToast(\n        `MCP server ${server.name} ${newState ? \"started\" : \"stopped\"} successfully.`,\n        \"success\",\n        { clear: true }\n      );\n    } else {\n      showToast(error || \"Failed to toggle MCP server.\", \"error\", {\n        clear: true,\n      });\n    }\n  }\n\n  useEffect(() => {\n    const handleClickOutside = (event) => {\n      if (menuRef.current && !menuRef.current.contains(event.target)) {\n        setOpen(false);\n      }\n    };\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, []);\n\n  return (\n    <div className=\"relative\" ref={menuRef}>\n      <button\n        type=\"button\"\n        onClick={() => setOpen(!open)}\n        className=\"p-1.5 rounded-lg text-white hover:bg-theme-action-menu-item-hover transition-colors duration-300\"\n      >\n        <Gear className=\"h-5 w-5\" weight=\"bold\" />\n      </button>\n      {open && (\n        <div className=\"absolute w-[150px] top-1 left-7 mt-1 border-[1.5px] border-white/40 rounded-lg bg-theme-action-menu-bg flex flex-col shadow-[0_4px_14px_rgba(0,0,0,0.25)] text-white z-99 md:z-10\">\n          <button\n            type=\"button\"\n            onClick={handleToggleServer}\n            className=\"border-none flex items-center rounded-lg gap-x-2 hover:bg-theme-action-menu-item-hover py-1.5 px-2 transition-colors duration-200 w-full text-left\"\n          >\n            <span className=\"text-sm\">\n              {running\n                ? t(\"agent.mcp.stop-server\")\n                : t(\"agent.mcp.start-server\")}\n            </span>\n          </button>\n          <button\n            type=\"button\"\n            onClick={deleteServer}\n            className=\"border-none flex items-center rounded-lg gap-x-2 hover:bg-theme-action-menu-item-hover py-1.5 px-2 transition-colors duration-200 w-full text-left\"\n          >\n            <span className=\"text-sm\">{t(\"agent.mcp.delete-server\")}</span>\n          </button>\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default function ServerPanel({\n  server,\n  toggleServer,\n  onDelete,\n  onToggleTool,\n}) {\n  const { t } = useTranslation();\n  const suppressedTools = server.config?.anythingllm?.suppressedTools || [];\n  const enabledToolCount = server.tools.filter(\n    (tool) => !suppressedTools.includes(tool.name)\n  ).length;\n\n  return (\n    <>\n      <div className=\"p-2\">\n        <div className=\"flex flex-col gap-y-[18px] max-w-[800px]\">\n          <ToolCountWarningBanner\n            server={server}\n            enabledToolCount={enabledToolCount}\n          />\n          <div className=\"flex w-full justify-between\">\n            <div className=\"flex items-center gap-x-2\">\n              <img src={MCPLogo} className=\"w-6 h-6 light:invert\" />\n              <label htmlFor=\"name\" className=\"text-white text-md font-bold\">\n                {titleCase(server.name.replace(/[_-]/g, \" \"))}\n              </label>\n              {server.tools.length > 0 && (\n                <p className=\"text-theme-text-secondary text-sm\">\n                  {enabledToolCount}/{server.tools.length}{\" \"}\n                  {t(\"agent.mcp.tools-enabled\")}\n                </p>\n              )}\n            </div>\n            <ManageServerMenu\n              key={server.name}\n              server={server}\n              toggleServer={toggleServer}\n              onDelete={onDelete}\n            />\n          </div>\n          <RenderServerConfig config={server.config} />\n          <RenderServerStatus server={server} />\n          <RenderServerTools\n            serverName={server.name}\n            tools={server.tools}\n            suppressedTools={suppressedTools}\n            onToggleTool={onToggleTool}\n          />\n        </div>\n      </div>\n    </>\n  );\n}\n\nfunction ToolCountWarningBanner({ server, enabledToolCount }) {\n  if (server.tools.length <= 10) return null;\n  if (enabledToolCount <= 10) return null;\n\n  return (\n    <div className=\"flex items-center gap-x-2 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg\">\n      <Warning className=\"h-5 w-5 text-yellow-500 shrink-0\" weight=\"fill\" />\n      <p className=\"text-yellow-500 text-sm\">\n        <Trans\n          i18nKey={`agent.mcp.tool-count-warning`}\n          values={{ count: enabledToolCount }}\n          components={{ b: <b />, br: <br /> }}\n        />\n      </p>\n    </div>\n  );\n}\n\nfunction RenderServerConfig({ config = null }) {\n  const { t } = useTranslation();\n  if (!config) return null;\n  return (\n    <div className=\"flex flex-col gap-y-2\">\n      <p className=\"text-theme-text-primary text-sm\">\n        {t(\"agent.mcp.startup-command\")}\n      </p>\n      <div className=\"bg-theme-bg-primary rounded-lg p-4\">\n        <p className=\"text-theme-text-secondary text-sm text-left\">\n          <span className=\"font-bold\">{t(\"agent.mcp.command\")}:</span>{\" \"}\n          {config.command}\n        </p>\n        <p className=\"text-theme-text-secondary text-sm text-left\">\n          <span className=\"font-bold\">{t(\"agent.mcp.arguments\")}:</span>{\" \"}\n          {config.args ? config.args.join(\" \") : t(\"common.none\")}\n        </p>\n      </div>\n    </div>\n  );\n}\n\nfunction RenderServerStatus({ server }) {\n  const { t } = useTranslation();\n  if (server.running || !server.error) return null;\n  return (\n    <div className=\"flex flex-col gap-y-2\">\n      <p className=\"text-theme-text-primary text-sm\">\n        {t(\"agent.mcp.not-running-warning\")}\n      </p>\n      <div className=\"bg-theme-bg-primary rounded-lg p-4\">\n        <p className=\"text-red-500 text-sm font-mono\">{server.error}</p>\n      </div>\n    </div>\n  );\n}\n\nfunction RenderServerTools({\n  serverName,\n  tools = [],\n  suppressedTools = [],\n  onToggleTool,\n}) {\n  if (tools.length === 0) return null;\n  return (\n    <div className=\"flex flex-col gap-y-2\">\n      <div className=\"flex flex-col gap-y-2\">\n        {tools.map((tool) => (\n          <ServerTool\n            key={tool.name}\n            serverName={serverName}\n            tool={tool}\n            enabled={!suppressedTools.includes(tool.name)}\n            onToggle={onToggleTool}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction ServerTool({ serverName, tool, enabled, onToggle }) {\n  const { t } = useTranslation();\n  const [open, setOpen] = useState(false);\n\n  return (\n    <button\n      type=\"button\"\n      onClick={() => setOpen(!open)}\n      className={`flex flex-col gap-y-2 px-4 py-2 rounded-lg border ${\n        enabled\n          ? \"border-theme-text-secondary\"\n          : \"border-theme-text-secondary/50 opacity-60\"\n      }`}\n    >\n      <div className=\"flex items-center justify-between\">\n        <div className=\"flex items-center gap-x-2 min-w-0 flex-1\">\n          <SimpleToggleSwitch\n            size=\"md\"\n            enabled={enabled}\n            onChange={(newEnabled) =>\n              onToggle?.(serverName, tool.name, newEnabled)\n            }\n          />\n          <p className=\"text-theme-text-primary font-mono font-bold text-sm shrink-0\">\n            {tool.name}\n          </p>\n          {!open && (\n            <p className=\"text-theme-text-secondary text-sm truncate\">\n              {tool.description}\n            </p>\n          )}\n        </div>\n        <div className=\"flex items-center gap-x-3\">\n          <div\n            className={`border-none text-theme-text-secondary hover:text-cta-button transition-transform duration-200 ${\n              open ? \"rotate-180\" : \"\"\n            }`}\n          >\n            <CaretDown size={16} />\n          </div>\n        </div>\n      </div>\n      {open && (\n        <div className=\"flex flex-col gap-y-2\">\n          <div className=\"flex flex-col gap-y-2\">\n            <p className=\"text-theme-text-secondary text-sm text-left\">\n              {tool.description}\n            </p>\n          </div>\n          <div className=\"flex flex-col gap-y-2\">\n            <p className=\"text-theme-text-primary text-sm text-left\">\n              {t(\"agent.mcp.tool-call-arguments\")}\n            </p>\n            <div className=\"flex flex-col gap-y-2\">\n              {Object.entries(tool.inputSchema?.properties || {}).map(\n                ([key, value]) => (\n                  <div key={key} className=\"flex items-center gap-x-2\">\n                    <p className=\"text-theme-text-secondary text-sm text-left font-bold\">\n                      {key}\n                      {tool.inputSchema?.required?.includes(key) && (\n                        <sup className=\"text-red-500\">*</sup>\n                      )}\n                    </p>\n                    <p className=\"text-theme-text-secondary text-sm text-left\">\n                      {value.type}\n                    </p>\n                  </div>\n                )\n              )}\n            </div>\n          </div>\n        </div>\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/MCPServers/index.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { titleCase } from \"text-case\";\nimport { BookOpenText, ArrowClockwise, Warning } from \"@phosphor-icons/react\";\nimport { Tooltip } from \"react-tooltip\";\nimport MCPLogo from \"@/media/agents/mcp-logo.svg\";\nimport MCPServers from \"@/models/mcpServers\";\nimport showToast from \"@/utils/toast\";\nimport { useTranslation } from \"react-i18next\";\n\nexport function MCPServerHeader({\n  setMcpServers,\n  setSelectedMcpServer,\n  children,\n}) {\n  const { t } = useTranslation();\n  const [loadingMcpServers, setLoadingMcpServers] = useState(false);\n  useEffect(() => {\n    async function fetchMCPServers() {\n      setLoadingMcpServers(true);\n      const { servers = [] } = await MCPServers.listServers();\n      setMcpServers(servers);\n      setLoadingMcpServers(false);\n    }\n    fetchMCPServers();\n  }, []);\n\n  // Refresh the list of MCP servers\n  const refreshMCPServers = () => {\n    if (\n      window.confirm(\n        \"Are you sure you want to refresh the list of MCP servers? This will restart all MCP servers and reload their tools.\"\n      )\n    ) {\n      setLoadingMcpServers(true);\n      MCPServers.forceReload()\n        .then(({ servers = [] }) => {\n          setSelectedMcpServer(null);\n          setMcpServers(servers);\n        })\n        .catch((err) => {\n          console.error(err);\n          showToast(`Failed to refresh MCP servers.`, \"error\", { clear: true });\n        })\n        .finally(() => {\n          setLoadingMcpServers(false);\n        });\n    }\n  };\n\n  return (\n    <>\n      <div className=\"text-theme-text-primary flex items-center justify-between gap-x-2 mt-4\">\n        <div className=\"flex items-center gap-x-2\">\n          <img src={MCPLogo} className=\"w-6 h-6 light:invert\" alt=\"MCP Logo\" />\n          <p className=\"text-lg font-medium\">{t(\"agent.mcp.title\")}</p>\n        </div>\n        <div className=\"flex items-center gap-x-3\">\n          <a\n            href=\"https://docs.anythingllm.com/mcp-compatibility/overview\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"border-none text-theme-text-secondary hover:text-cta-button\"\n          >\n            <BookOpenText size={16} />\n          </a>\n          <button\n            type=\"button\"\n            onClick={refreshMCPServers}\n            disabled={loadingMcpServers}\n            className=\"border-none text-theme-text-secondary hover:text-cta-button flex items-center gap-x-1\"\n          >\n            <ArrowClockwise\n              size={16}\n              className={loadingMcpServers ? \"animate-spin\" : \"\"}\n            />\n            <p className=\"text-sm\">\n              {loadingMcpServers\n                ? `${t(\"common.loading\")}...`\n                : t(\"common.refresh\")}\n            </p>\n          </button>\n        </div>\n      </div>\n      {children({ loadingMcpServers })}\n    </>\n  );\n}\n\nexport function MCPServersList({\n  isLoading = false,\n  servers = [],\n  selectedServer,\n  handleClick,\n}) {\n  const { t } = useTranslation();\n  if (isLoading) {\n    return (\n      <div className=\"text-theme-text-secondary text-center text-xs flex flex-col gap-y-2\">\n        <p>{t(\"agent.mcp.loading-from-config\")}...</p>\n        <a\n          href=\"https://docs.anythingllm.com/mcp-compatibility/overview\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"text-theme-text-secondary underline hover:text-cta-button\"\n        >\n          {t(\"agent.mcp.learn-more\")}\n        </a>\n      </div>\n    );\n  }\n\n  if (servers.length === 0) {\n    return (\n      <div className=\"text-theme-text-secondary text-center text-xs flex flex-col gap-y-2\">\n        <p>{t(\"agent.mcp.no-servers-found\")}</p>\n        <a\n          href=\"https://docs.anythingllm.com/mcp-compatibility/overview\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"text-theme-text-secondary underline hover:text-cta-button\"\n        >\n          {t(\"agent.mcp.learn-more\")}\n        </a>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"bg-theme-bg-secondary text-white rounded-xl w-full md:min-w-[360px]\">\n      {servers.map((server, index) => (\n        <MCPServerItem\n          key={server.name}\n          server={server}\n          isFirst={index === 0}\n          isLast={index === servers.length - 1}\n          isSelected={selectedServer?.name === server.name}\n          handleClick={() => handleClick?.(server)}\n        />\n      ))}\n      <Tooltip\n        id=\"mcp-server-warning\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"tooltip !text-xs\"\n        content={t(\"agent.mcp.tool-warning\")}\n      />\n    </div>\n  );\n}\n\nfunction MCPServerItem({ server, isFirst, isLast, isSelected, handleClick }) {\n  const { t } = useTranslation();\n  const suppressedTools = server.config?.anythingllm?.suppressedTools || [];\n  const enabledToolCount = server.tools.length - suppressedTools.length;\n  const showWarning = enabledToolCount > 10;\n  const running = server.running;\n\n  return (\n    <div\n      className={`py-3 px-4 flex items-center justify-between ${\n        isFirst ? \"rounded-t-xl\" : \"\"\n      } ${\n        isLast ? \"rounded-b-xl\" : \"border-b border-white/10\"\n      } cursor-pointer transition-all duration-300 hover:bg-theme-bg-primary ${\n        isSelected ? \"bg-white/10 light:bg-theme-bg-sidebar\" : \"\"\n      }`}\n      onClick={handleClick}\n    >\n      <div className=\"flex items-center gap-x-2 text-sm font-light\">\n        {showWarning && (\n          <Warning\n            data-tooltip-id=\"mcp-server-warning\"\n            className=\"h-4 w-4 text-yellow-500\"\n          />\n        )}\n        {titleCase(server.name.replace(/[_-]/g, \" \"))}\n      </div>\n      <div className=\"flex items-center gap-x-2\">\n        <div\n          className={`text-sm text-theme-text-secondary font-medium ${running ? \"text-green-500\" : \"text-red-500\"}`}\n        >\n          {running ? t(\"common.on\") : t(\"common.stopped\")}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/SQLConnectorSelection/DBConnection.jsx",
    "content": "import PostgreSQLLogo from \"./icons/postgresql.png\";\nimport MySQLLogo from \"./icons/mysql.png\";\nimport MSSQLLogo from \"./icons/mssql.png\";\nimport { PencilSimple, X } from \"@phosphor-icons/react\";\nimport { useModal } from \"@/hooks/useModal\";\nimport EditSQLConnection from \"./SQLConnectionModal\";\n\nexport const DB_LOGOS = {\n  postgresql: PostgreSQLLogo,\n  mysql: MySQLLogo,\n  \"sql-server\": MSSQLLogo,\n};\n\nexport default function DBConnection({\n  connection,\n  onRemove,\n  onUpdate,\n  setHasChanges,\n  connections = [],\n}) {\n  const { database_id, engine } = connection;\n  const { isOpen, openModal, closeModal } = useModal();\n\n  function removeConfirmation() {\n    if (\n      !window.confirm(\n        `Delete ${database_id} from the list of available SQL connections? This cannot be undone.`\n      )\n    )\n      return false;\n    onRemove(database_id);\n  }\n\n  return (\n    <div className=\"flex gap-x-4 items-center\">\n      <img\n        src={DB_LOGOS?.[engine] ?? null}\n        alt={`${engine} logo`}\n        className=\"w-10 h-10 rounded-md\"\n      />\n      <div className=\"flex w-full items-center justify-between\">\n        <div className=\"flex flex-col\">\n          <div className=\"text-sm font-semibold text-white\">{database_id}</div>\n          <div className=\"mt-1 text-xs text-description\">{engine}</div>\n        </div>\n        <div className=\"flex gap-x-2\">\n          <button\n            type=\"button\"\n            data-tooltip-id=\"edit-sql-connection-tooltip\"\n            className=\"border-none text-theme-text-secondary hover:text-theme-text-primary transition-colors duration-200 p-1 rounded\"\n            onClick={openModal}\n          >\n            <PencilSimple size={18} />\n          </button>\n          <button\n            type=\"button\"\n            data-tooltip-id=\"delete-sql-connection-tooltip\"\n            onClick={removeConfirmation}\n            className=\"border-none text-theme-text-secondary hover:text-red-500\"\n          >\n            <X size={18} />\n          </button>\n        </div>\n      </div>\n      <EditSQLConnection\n        isOpen={isOpen}\n        closeModal={closeModal}\n        existingConnection={connection}\n        onSubmit={onUpdate}\n        setHasChanges={setHasChanges}\n        connections={connections}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/SQLConnectorSelection/SQLConnectionModal.jsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { WarningOctagon, X } from \"@phosphor-icons/react\";\nimport { DB_LOGOS } from \"./DBConnection\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport Toggle from \"@/components/lib/Toggle\";\n\n/**\n * Converts a string to a URL-friendly slug format.\n * Matches backend slugify behavior for consistent database_id generation.\n * @param {string} str - The string to slugify\n * @returns {string} - The slugified string (lowercase, hyphens, no special chars)\n */\nfunction slugify(str) {\n  return str\n    .toLowerCase()\n    .trim()\n    .replace(/[^\\w\\s-]/g, \"\") // Remove special characters\n    .replace(/[\\s_]+/g, \"-\") // Replace spaces and underscores with hyphens\n    .replace(/^-+|-+$/g, \"\"); // Remove leading/trailing hyphens\n}\n\n/**\n * Assembles a database connection string based on the engine type and configuration.\n * @param {Object} params - Connection parameters\n * @param {string} params.engine - The database engine ('postgresql', 'mysql', or 'sql-server')\n * @param {string} [params.username=\"\"] - Database username\n * @param {string} [params.password=\"\"] - Database password\n * @param {string} [params.host=\"\"] - Database host/endpoint\n * @param {string} [params.port=\"\"] - Database port\n * @param {string} [params.database=\"\"] - Database name\n * @param {boolean} [params.encrypt=false] - Enable encryption (SQL Server only)\n * @returns {string|null} - The assembled connection string, error message if fields missing, or null if engine invalid\n */\nfunction assembleConnectionString({\n  engine,\n  username = \"\",\n  password = \"\",\n  host = \"\",\n  port = \"\",\n  database = \"\",\n  encrypt = false,\n}) {\n  if ([username, password, host, database].every((i) => !!i) === false)\n    return `Please fill out all the fields above.`;\n  switch (engine) {\n    case \"postgresql\":\n      return `postgres://${username}:${password}@${host}:${port}/${database}`;\n    case \"mysql\":\n      return `mysql://${username}:${password}@${host}:${port}/${database}`;\n    case \"sql-server\":\n      return `mssql://${username}:${password}@${host}:${port}/${database}?encrypt=${encrypt}`;\n    default:\n      return null;\n  }\n}\n\nconst DEFAULT_ENGINE = \"postgresql\";\nconst DEFAULT_CONFIG = {\n  name: \"\",\n  username: null,\n  password: null,\n  host: null,\n  port: null,\n  database: null,\n  schema: null,\n  encrypt: false,\n};\n\n/**\n * Modal component for creating or editing SQL database connections.\n * Supports PostgreSQL, MySQL, and SQL Server with connection validation.\n * Handles duplicate connection name detection and connection string assembly.\n *\n * @param {Object} props - Component props\n * @param {boolean} props.isOpen - Whether the modal is currently open\n * @param {Function} props.closeModal - Callback to close the modal\n * @param {Function} props.onSubmit - Callback when connection is successfully validated and saved\n * @param {Function} props.setHasChanges - Callback to mark that changes have been made\n * @param {Object|null} [props.existingConnection=null] - Existing connection data for edit mode (contains database_id, engine, username, password, host, port, database, schema, encrypt)\n * @param {Array} [props.connections=[]] - List of all existing connections for duplicate detection\n * @returns {React.ReactPortal|null} - Portal containing the modal UI, or null if not open\n */\nexport default function SQLConnectionModal({\n  isOpen,\n  closeModal,\n  onSubmit,\n  setHasChanges,\n  existingConnection = null, // { database_id, engine } for edit mode\n  connections = [], // List of all existing connections for duplicate detection\n}) {\n  const isEditMode = !!existingConnection;\n  const [engine, setEngine] = useState(DEFAULT_ENGINE);\n  const [config, setConfig] = useState(DEFAULT_CONFIG);\n  const [isValidating, setIsValidating] = useState(false);\n\n  // Sync state when modal opens - useState initial values only run once on mount,\n  // so we need this effect to update state when the modal is reopened\n  useEffect(() => {\n    if (!isOpen) return;\n\n    if (existingConnection) {\n      setEngine(existingConnection.engine);\n      setConfig({\n        name: existingConnection.database_id,\n        username: existingConnection.username,\n        password: existingConnection.password,\n        host: existingConnection.host,\n        port: existingConnection.port,\n        database: existingConnection.database,\n        schema: existingConnection.schema,\n        encrypt: existingConnection?.encrypt,\n      });\n    } else {\n      setEngine(DEFAULT_ENGINE);\n      setConfig(DEFAULT_CONFIG);\n    }\n  }, [isOpen, existingConnection]);\n\n  // Track original database ID to send to server for updating if in edit mode\n  const originalDatabaseId = isEditMode ? existingConnection.database_id : null;\n\n  if (!isOpen) return null;\n\n  function handleClose() {\n    setEngine(DEFAULT_ENGINE);\n    setConfig(DEFAULT_CONFIG);\n    closeModal();\n  }\n\n  function onFormChange(e) {\n    const form = new FormData(e.target.form);\n    setConfig({\n      name: form.get(\"name\").trim(),\n      username: form.get(\"username\").trim(),\n      password: form.get(\"password\"),\n      host: form.get(\"host\").trim(),\n      port: form.get(\"port\").trim(),\n      database: form.get(\"database\").trim(),\n      schema: form.get(\"schema\")?.trim() || null,\n      encrypt: form.get(\"encrypt\") === \"true\",\n    });\n  }\n\n  /**\n   * Checks if a connection name (slugified) already exists in the connections list.\n   * For edit mode, excludes the original connection being edited.\n   * @param {string} slugifiedName - The slugified name to check\n   * @returns {boolean} - True if duplicate exists, false otherwise\n   */\n  function isDuplicateConnectionName(slugifiedName) {\n    // Get active connections (not marked for removal)\n    const activeConnections = connections.filter(\n      (conn) => conn.action !== \"remove\"\n    );\n\n    // Check for duplicates, excluding the original connection in edit mode\n    return activeConnections.some((conn) => {\n      // In edit mode, skip the original connection being edited\n      if (isEditMode && conn.database_id === originalDatabaseId) {\n        return false;\n      }\n      return conn.database_id === slugifiedName;\n    });\n  }\n\n  /**\n   * Handles form submission for both creating new connections and updating existing ones.\n   * Process:\n   * 1. Slugify the connection name to match backend behavior\n   * 2. Check for duplicate names (prevents frontend from sending invalid updates)\n   * 3. Validate the connection string by attempting to connect to the database\n   * 4. If valid, submit with appropriate action (\"add\" or \"update\")\n   *\n   * For updates: Includes originalDatabaseId so backend can find and replace the old connection\n   * For new connections: Just includes the new connection data\n   */\n  async function handleUpdate(e) {\n    e.preventDefault();\n    e.stopPropagation();\n    const form = new FormData(e.target);\n    const connectionString = assembleConnectionString({ engine, ...config });\n\n    // Slugify the database_id immediately to match backend behavior\n    const slugifiedDatabaseId = slugify(form.get(\"name\"));\n\n    // Check for duplicate connection names before validation\n    if (isDuplicateConnectionName(slugifiedDatabaseId)) {\n      showToast(\n        `A connection with the name \"${slugifiedDatabaseId}\" already exists. Please choose a different name.`,\n        \"error\",\n        { clear: true }\n      );\n      return;\n    }\n\n    setIsValidating(true);\n    try {\n      // Validate that we can actually connect to this database\n      const { success, error } = await System.validateSQLConnection(\n        engine,\n        connectionString\n      );\n      if (!success) {\n        showToast(\n          error ||\n            \"Failed to establish database connection. Please check your connection details.\",\n          \"error\",\n          { clear: true }\n        );\n        setIsValidating(false);\n        return;\n      }\n\n      const connectionData = {\n        engine,\n        database_id: slugifiedDatabaseId,\n        connectionString,\n        schema: engine === \"postgresql\" ? config.schema : null,\n      };\n\n      if (isEditMode) {\n        // EDIT MODE: Send update action with originalDatabaseId\n        // This tells the backend to find the connection with originalDatabaseId\n        // and replace it with the new connection data\n        onSubmit({\n          ...connectionData,\n          action: \"update\",\n          originalDatabaseId: originalDatabaseId,\n        });\n      } else {\n        // CREATE MODE: Send add action\n        // Backend will check for duplicates and add if unique\n        onSubmit({\n          ...connectionData,\n          action: \"add\",\n        });\n      }\n\n      setHasChanges(true);\n      handleClose();\n    } catch (error) {\n      console.error(\"Error validating connection:\", error);\n      showToast(\n        error?.message ||\n          \"Failed to validate connection. Please check your connection details.\",\n        \"error\",\n        { clear: true }\n      );\n    } finally {\n      setIsValidating(false);\n    }\n    return false;\n  }\n\n  // Cannot do nested forms, it will cause all sorts of issues, so we portal this out\n  // to the parent container form so we don't have nested forms.\n  return createPortal(\n    <ModalWrapper isOpen={isOpen}>\n      <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n        <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n          <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n            <div className=\"w-full flex gap-x-2 items-center\">\n              <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n                {isEditMode ? \"Edit SQL Connection\" : \"New SQL Connection\"}\n              </h3>\n            </div>\n            <button\n              onClick={handleClose}\n              type=\"button\"\n              className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n            >\n              <X size={24} weight=\"bold\" className=\"text-white\" />\n            </button>\n          </div>\n          <form\n            id=\"sql-connection-form\"\n            onChange={onFormChange}\n            onSubmit={handleUpdate}\n          >\n            <div className=\"px-7 py-6\">\n              <div className=\"space-y-6 max-h-[60vh] overflow-y-auto pr-2\">\n                <p className=\"text-sm text-white/60\">\n                  {isEditMode\n                    ? \"Update the connection information for your database below.\"\n                    : \"Add the connection information for your database below and it will be available for future SQL agent calls.\"}\n                </p>\n                <div className=\"flex flex-col w-full\">\n                  <div className=\"border border-red-800 bg-zinc-800 light:bg-red-200/50 p-4 rounded-lg flex items-center gap-x-2 text-sm text-red-400 light:text-red-500\">\n                    <WarningOctagon size={28} className=\"shrink-0\" />\n                    <p>\n                      <b>WARNING:</b> The SQL agent has been <i>instructed</i>{\" \"}\n                      to only perform non-modifying queries. This{\" \"}\n                      <b>does not</b> prevent a hallucination from still\n                      deleting data. Only connect with a user who has{\" \"}\n                      <b>READ_ONLY</b> permissions.\n                    </p>\n                  </div>\n\n                  <label className=\"block mb-2 text-sm font-medium text-white mt-4\">\n                    Select your SQL engine\n                  </label>\n                  <div className=\"grid md:grid-cols-4 gap-4 grid-cols-2\">\n                    <DBEngine\n                      provider=\"postgresql\"\n                      active={engine === \"postgresql\"}\n                      onClick={() => setEngine(\"postgresql\")}\n                    />\n                    <DBEngine\n                      provider=\"mysql\"\n                      active={engine === \"mysql\"}\n                      onClick={() => setEngine(\"mysql\")}\n                    />\n                    <DBEngine\n                      provider=\"sql-server\"\n                      active={engine === \"sql-server\"}\n                      onClick={() => setEngine(\"sql-server\")}\n                    />\n                  </div>\n                </div>\n\n                <div className=\"flex flex-col w-full\">\n                  <label className=\"block mb-2 text-sm font-medium text-white\">\n                    Connection name\n                  </label>\n                  <input\n                    type=\"text\"\n                    name=\"name\"\n                    className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                    placeholder=\"a unique name to identify this SQL connection\"\n                    required={true}\n                    autoComplete=\"off\"\n                    spellCheck={false}\n                    defaultValue={config.name || \"\"}\n                  />\n                </div>\n\n                <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n                  <div className=\"flex flex-col\">\n                    <label className=\"block mb-2 text-sm font-medium text-white\">\n                      Database user\n                    </label>\n                    <input\n                      type=\"text\"\n                      name=\"username\"\n                      className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                      placeholder=\"root\"\n                      required={true}\n                      autoComplete=\"off\"\n                      spellCheck={false}\n                      defaultValue={config.username || \"\"}\n                    />\n                  </div>\n                  <div className=\"flex flex-col\">\n                    <label className=\"block mb-2 text-sm font-medium text-white\">\n                      Database user password\n                    </label>\n                    <input\n                      type=\"password\"\n                      name=\"password\"\n                      className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                      placeholder=\"password123\"\n                      required={true}\n                      autoComplete=\"off\"\n                      spellCheck={false}\n                      defaultValue={config.password || \"\"}\n                    />\n                  </div>\n                </div>\n\n                <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n                  <div className=\"sm:col-span-2\">\n                    <label className=\"block mb-2 text-sm font-medium text-white\">\n                      Server endpoint\n                    </label>\n                    <input\n                      type=\"text\"\n                      name=\"host\"\n                      className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                      placeholder=\"the hostname or endpoint for your database\"\n                      required={true}\n                      autoComplete=\"off\"\n                      spellCheck={false}\n                      defaultValue={config.host || \"\"}\n                    />\n                  </div>\n                  <div>\n                    <label className=\"block mb-2 text-sm font-medium text-white\">\n                      Port\n                    </label>\n                    <input\n                      type=\"text\"\n                      name=\"port\"\n                      className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                      placeholder=\"3306\"\n                      required={false}\n                      autoComplete=\"off\"\n                      spellCheck={false}\n                      defaultValue={config.port || \"\"}\n                    />\n                  </div>\n                </div>\n\n                <div className=\"flex flex-col\">\n                  <label className=\"block mb-2 text-sm font-medium text-white\">\n                    Database\n                  </label>\n                  <input\n                    type=\"text\"\n                    name=\"database\"\n                    className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                    placeholder=\"the database the agent will interact with\"\n                    required={true}\n                    autoComplete=\"off\"\n                    spellCheck={false}\n                    defaultValue={config.database || \"\"}\n                  />\n                </div>\n\n                {engine === \"postgresql\" && (\n                  <div className=\"flex flex-col\">\n                    <label className=\"block mb-2 text-sm font-medium text-white\">\n                      Schema (optional)\n                    </label>\n                    <input\n                      type=\"text\"\n                      name=\"schema\"\n                      className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                      placeholder=\"public (default schema if not specified)\"\n                      required={false}\n                      autoComplete=\"off\"\n                      spellCheck={false}\n                      defaultValue={config.schema || \"\"}\n                    />\n                  </div>\n                )}\n\n                {engine === \"sql-server\" && (\n                  <Toggle\n                    name=\"encrypt\"\n                    value=\"true\"\n                    size=\"md\"\n                    label=\"Enable Encryption\"\n                    enabled={config.encrypt}\n                  />\n                )}\n\n                <p className=\"text-theme-text-secondary text-sm\">\n                  {assembleConnectionString({ engine, ...config })}\n                </p>\n              </div>\n            </div>\n            <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border px-7 pb-6\">\n              <button\n                type=\"button\"\n                onClick={handleClose}\n                className=\"transition-all duration-300 text-white hover:bg-zinc-700 light:hover:bg-theme-bg-primary px-4 py-2 rounded-lg text-sm\"\n              >\n                Cancel\n              </button>\n              <button\n                type=\"submit\"\n                form=\"sql-connection-form\"\n                disabled={isValidating}\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm disabled:opacity-50\"\n              >\n                {isValidating ? \"Validating...\" : \"Save connection\"}\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </ModalWrapper>,\n    document.getElementById(\"workspace-agent-settings-container\")\n  );\n}\n\n/**\n * Database engine selection button component.\n * Displays a database logo and handles selection state.\n *\n * @param {Object} props - Component props\n * @param {string} props.provider - The database provider identifier ('postgresql', 'mysql', 'sql-server')\n * @param {boolean} props.active - Whether this engine is currently selected\n * @param {Function} props.onClick - Callback when the engine is clicked\n * @returns {JSX.Element} - Button element with database logo\n */\nfunction DBEngine({ provider, active, onClick }) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      className={`flex flex-col p-4 border border-white/40 bg-zinc-800 light:bg-theme-settings-input-bg rounded-lg w-fit hover:bg-zinc-700 ${\n        active ? \"!bg-blue-500/50\" : \"\"\n      }`}\n    >\n      <img\n        src={DB_LOGOS[provider]}\n        className=\"h-[100px] rounded-md\"\n        alt={provider}\n      />\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/SQLConnectorSelection/index.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport DBConnection from \"./DBConnection\";\nimport { Plus, Database, CircleNotch } from \"@phosphor-icons/react\";\nimport NewSQLConnection from \"./SQLConnectionModal\";\nimport { useModal } from \"@/hooks/useModal\";\nimport SQLAgentImage from \"@/media/agents/sql-agent.png\";\nimport Admin from \"@/models/admin\";\nimport Toggle from \"@/components/lib/Toggle\";\nimport { Tooltip } from \"react-tooltip\";\n\nexport default function AgentSQLConnectorSelection({\n  skill,\n  title,\n  description,\n  toggleSkill,\n  enabled = false,\n  setHasChanges,\n  hasChanges = false,\n}) {\n  const { isOpen, openModal, closeModal } = useModal();\n  const [connections, setConnections] = useState([]);\n  const [loading, setLoading] = useState(true);\n  const prevHasChanges = useRef(hasChanges);\n\n  // Load connections on mount\n  useEffect(() => {\n    setLoading(true);\n    Admin.systemPreferencesByFields([\"agent_sql_connections\"])\n      .then((res) => setConnections(res?.settings?.agent_sql_connections ?? []))\n      .catch(() => setConnections([]))\n      .finally(() => setLoading(false));\n  }, []);\n\n  // Refresh connections from backend when save completes (hasChanges: true -> false)\n  // This ensures we get clean data without stale action properties\n  useEffect(() => {\n    if (prevHasChanges.current === true && hasChanges === false) {\n      Admin.systemPreferencesByFields([\"agent_sql_connections\"])\n        .then((res) =>\n          setConnections(res?.settings?.agent_sql_connections ?? [])\n        )\n        .catch(() => {});\n    }\n    prevHasChanges.current = hasChanges;\n  }, [hasChanges]);\n\n  /**\n   * Marks a connection for removal by adding action: \"remove\".\n   * The connection stays in the array (for undo capability) until saved.\n   * @param {string} databaseId - The database_id of the connection to remove\n   */\n  function handleRemoveConnection(databaseId) {\n    setHasChanges(true);\n    setConnections((prev) =>\n      prev.map((conn) => {\n        if (conn.database_id === databaseId)\n          return { ...conn, action: \"remove\" };\n        return conn;\n      })\n    );\n  }\n\n  /**\n   * Updates an existing connection by replacing it in the local state.\n   * This removes the old connection (by originalDatabaseId) and adds the updated version.\n   *\n   * Note: The old connection is removed from local state immediately, but the backend\n   * handles the actual update logic when saved. See mergeConnections in server/models/systemSettings.js\n   *\n   * @param {Object} updatedConnection - The updated connection data\n   * @param {string} updatedConnection.originalDatabaseId - The original database_id before the update\n   * @param {string} updatedConnection.database_id - The new database_id\n   * @param {string} updatedConnection.action - Should be \"update\"\n   */\n  function handleUpdateConnection(updatedConnection) {\n    setHasChanges(true);\n    setConnections((prev) =>\n      prev.map((conn) =>\n        conn.database_id === updatedConnection.originalDatabaseId\n          ? updatedConnection\n          : conn\n      )\n    );\n  }\n  /**\n   * Adds a new connection to the local state with action: \"add\".\n   * The backend will validate and deduplicate when saved.\n   * @param {Object} newConnection - The new connection data with action: \"add\"\n   */\n  function handleAddConnection(newConnection) {\n    setHasChanges(true);\n    setConnections((prev) => [...prev, newConnection]);\n  }\n\n  return (\n    <>\n      <div className=\"p-2\">\n        <div className=\"flex flex-col gap-y-[18px] max-w-[500px]\">\n          <div className=\"flex w-full justify-between items-center\">\n            <div className=\"flex items-center gap-x-2\">\n              <Database\n                size={24}\n                color=\"var(--theme-text-primary)\"\n                weight=\"bold\"\n              />\n              <label\n                htmlFor=\"name\"\n                className=\"text-theme-text-primary text-md font-bold\"\n              >\n                {title}\n              </label>\n            </div>\n            <Toggle\n              size=\"lg\"\n              enabled={enabled}\n              onChange={() => toggleSkill(skill)}\n            />\n          </div>\n          <img\n            src={SQLAgentImage}\n            alt=\"SQL Agent\"\n            className=\"w-full rounded-md\"\n          />\n          <p className=\"text-theme-text-secondary text-opacity-60 text-xs font-medium py-1.5\">\n            {description}\n          </p>\n          {enabled && (\n            <>\n              <input\n                name=\"system::agent_sql_connections\"\n                type=\"hidden\"\n                value={JSON.stringify(connections)}\n              />\n              <input\n                type=\"hidden\"\n                value={JSON.stringify(\n                  connections.filter((conn) => conn.action !== \"remove\")\n                )}\n              />\n              <div className=\"flex flex-col mt-2 gap-y-2\">\n                <p className=\"text-theme-text-primary font-semibold text-sm\">\n                  Your database connections\n                </p>\n                <div className=\"flex flex-col gap-y-3\">\n                  {loading ? (\n                    <div className=\"flex items-center justify-center py-4\">\n                      <CircleNotch\n                        size={24}\n                        className=\"animate-spin text-theme-text-primary\"\n                      />\n                    </div>\n                  ) : (\n                    connections\n                      .filter((connection) => connection.action !== \"remove\")\n                      .map((connection) => (\n                        <DBConnection\n                          key={connection.database_id}\n                          connection={connection}\n                          onRemove={handleRemoveConnection}\n                          onUpdate={handleUpdateConnection}\n                          setHasChanges={setHasChanges}\n                          connections={connections}\n                        />\n                      ))\n                  )}\n                  <button\n                    type=\"button\"\n                    onClick={openModal}\n                    className=\"w-fit relative flex h-[40px] items-center border-none hover:bg-theme-bg-secondary rounded-lg\"\n                  >\n                    <div className=\"flex w-full gap-x-2 items-center p-4\">\n                      <div className=\"bg-theme-bg-secondary p-2 rounded-lg h-[24px] w-[24px] flex items-center justify-center\">\n                        <Plus\n                          weight=\"bold\"\n                          size={14}\n                          className=\"shrink-0 text-theme-text-primary\"\n                        />\n                      </div>\n                      <p className=\"text-left text-theme-text-primary text-sm\">\n                        New SQL connection\n                      </p>\n                    </div>\n                  </button>\n                </div>\n              </div>\n            </>\n          )}\n        </div>\n      </div>\n      <NewSQLConnection\n        isOpen={isOpen}\n        closeModal={closeModal}\n        setHasChanges={setHasChanges}\n        onSubmit={handleAddConnection}\n        connections={connections}\n      />\n      <Tooltip\n        id=\"edit-sql-connection-tooltip\"\n        content=\"Edit SQL connection\"\n        place=\"top\"\n        delayShow={300}\n        className=\"tooltip !text-xs !opacity-100\"\n        style={{\n          maxWidth: \"250px\",\n          whiteSpace: \"normal\",\n          wordWrap: \"break-word\",\n        }}\n      />\n      <Tooltip\n        id=\"delete-sql-connection-tooltip\"\n        content=\"Delete SQL connection\"\n        place=\"top\"\n        delayShow={300}\n        className=\"tooltip !text-xs !opacity-100\"\n        style={{\n          maxWidth: \"250px\",\n          whiteSpace: \"normal\",\n          wordWrap: \"break-word\",\n        }}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderItem/index.jsx",
    "content": "export default function SearchProviderItem({ provider, checked, onClick }) {\n  const { name, value, logo, description } = provider;\n  return (\n    <div\n      onClick={onClick}\n      className={`w-full p-2 rounded-md hover:cursor-pointer hover:bg-theme-bg-secondary ${\n        checked ? \"bg-theme-bg-secondary\" : \"\"\n      }`}\n    >\n      <input\n        type=\"checkbox\"\n        value={value}\n        className=\"peer hidden\"\n        checked={checked}\n        readOnly={true}\n        formNoValidate={true}\n      />\n      <div className=\"flex gap-x-4 items-center\">\n        <img src={logo} alt={`${name} logo`} className=\"w-10 h-10 rounded-md\" />\n        <div className=\"flex flex-col\">\n          <div className=\"text-sm font-semibold text-white\">{name}</div>\n          <div className=\"mt-1 text-xs text-description\">{description}</div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderOptions/index.jsx",
    "content": "const SerpApiEngines = [\n  { name: \"Google Search\", value: \"google\" },\n  { name: \"Google Images\", value: \"google_images_light\" },\n  { name: \"Google Jobs\", value: \"google_jobs\" },\n  { name: \"Google Maps\", value: \"google_maps\" },\n  { name: \"Google News\", value: \"google_news_light\" },\n  { name: \"Google Patents\", value: \"google_patents\" },\n  { name: \"Google Scholar\", value: \"google_scholar\" },\n  { name: \"Google Shopping\", value: \"google_shopping_light\" },\n  { name: \"Amazon\", value: \"amazon\" },\n  { name: \"Baidu\", value: \"baidu\" },\n];\nexport function SerpApiOptions({ settings }) {\n  return (\n    <>\n      <p className=\"text-sm text-white/60 my-2\">\n        Get a free API key{\" \"}\n        <a\n          href=\"https://serpapi.com/\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"text-blue-300 underline\"\n        >\n          from SerpApi.\n        </a>\n      </p>\n      <div className=\"flex gap-x-4\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"env::AgentSerpApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"SerpApi API Key\"\n            defaultValue={settings?.AgentSerpApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Engine\n          </label>\n          <select\n            name=\"env::AgentSerpApiEngine\"\n            required={true}\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            defaultValue={settings?.AgentSerpApiEngine || \"google\"}\n          >\n            {SerpApiEngines.map(({ name, value }) => (\n              <option key={name} value={value}>\n                {name}\n              </option>\n            ))}\n          </select>\n          {/* <input\n            type=\"text\"\n            name=\"env::AgentSerpApiEngine\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"SerpApi engine (Google, Amazon...)\"\n            defaultValue={settings?.AgentSerpApiEngine || \"google\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          /> */}\n        </div>\n      </div>\n    </>\n  );\n}\n\nconst SearchApiEngines = [\n  { name: \"Google Search\", value: \"google\" },\n  { name: \"Google Maps\", value: \"google_maps\" },\n  { name: \"Google Shopping\", value: \"google_shopping\" },\n  { name: \"Google News\", value: \"google_news\" },\n  { name: \"Google Jobs\", value: \"google_jobs\" },\n  { name: \"Google Scholar\", value: \"google_scholar\" },\n  { name: \"Google Finance\", value: \"google_finance\" },\n  { name: \"Google Patents\", value: \"google_patents\" },\n  { name: \"YouTube\", value: \"youtube\" },\n  { name: \"Bing\", value: \"bing\" },\n  { name: \"Bing News\", value: \"bing_news\" },\n  { name: \"Amazon Product Search\", value: \"amazon_search\" },\n  { name: \"Baidu\", value: \"baidu\" },\n];\nexport function SearchApiOptions({ settings }) {\n  return (\n    <>\n      <p className=\"text-sm text-white/60 my-2\">\n        You can get a free API key{\" \"}\n        <a\n          href=\"https://www.searchapi.io/\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"text-blue-300 underline\"\n        >\n          from SearchApi.\n        </a>\n      </p>\n      <div className=\"flex gap-x-4\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"env::AgentSearchApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"SearchApi API Key\"\n            defaultValue={settings?.AgentSearchApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            Engine\n          </label>\n          <select\n            name=\"env::AgentSearchApiEngine\"\n            required={true}\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            defaultValue={settings?.AgentSearchApiEngine || \"google\"}\n          >\n            {SearchApiEngines.map(({ name, value }) => (\n              <option key={name} value={value}>\n                {name}\n              </option>\n            ))}\n          </select>\n          {/* <input\n            type=\"text\"\n            name=\"env::AgentSearchApiEngine\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"SearchApi engine (Google, Bing...)\"\n            defaultValue={settings?.AgentSearchApiEngine || \"google\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          /> */}\n        </div>\n      </div>\n    </>\n  );\n}\n\nexport function SerperDotDevOptions({ settings }) {\n  return (\n    <>\n      <p className=\"text-sm text-white/60 my-2\">\n        You can get a free API key{\" \"}\n        <a\n          href=\"https://serper.dev\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"text-blue-300 underline\"\n        >\n          from Serper.dev.\n        </a>\n      </p>\n      <div className=\"flex gap-x-4\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"env::AgentSerperApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Serper.dev API Key\"\n            defaultValue={settings?.AgentSerperApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n    </>\n  );\n}\n\nexport function BingSearchOptions({ settings }) {\n  return (\n    <>\n      <p className=\"text-sm text-white/60 my-2\">\n        You can get a Bing Web Search API subscription key{\" \"}\n        <a\n          href=\"https://portal.azure.com/\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"text-blue-300 underline\"\n        >\n          from the Azure portal.\n        </a>\n      </p>\n      <div className=\"flex gap-x-4\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"env::AgentBingSearchApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Bing Web Search API Key\"\n            defaultValue={settings?.AgentBingSearchApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n      <p className=\"text-sm text-white/60 my-2\">\n        To set up a Bing Web Search API subscription:\n      </p>\n      <ol className=\"list-decimal text-sm text-white/60 ml-6\">\n        <li>\n          Go to the Azure portal:{\" \"}\n          <a\n            href=\"https://portal.azure.com/\"\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"text-blue-300 underline\"\n          >\n            https://portal.azure.com/\n          </a>\n        </li>\n        <li>Create a new Azure account or sign in with an existing one.</li>\n        <li>\n          Navigate to the \"Create a resource\" section and search for \"Grounding\n          with Bing Search\".\n        </li>\n        <li>\n          Select the \"Grounding with Bing Search\" resource and create a new\n          subscription.\n        </li>\n        <li>Choose the pricing tier that suits your needs.</li>\n        <li>\n          Obtain the API key for your Grounding with Bing Search subscription.\n        </li>\n      </ol>\n    </>\n  );\n}\n\nexport function SerplySearchOptions({ settings }) {\n  return (\n    <>\n      <p className=\"text-sm text-white/60 my-2\">\n        You can get a free API key{\" \"}\n        <a\n          href=\"https://serply.io\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"text-blue-300 underline\"\n        >\n          from Serply.io.\n        </a>\n      </p>\n      <div className=\"flex gap-x-4\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"env::AgentSerplyApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Serply API Key\"\n            defaultValue={settings?.AgentSerplyApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n    </>\n  );\n}\n\nexport function SearXNGOptions({ settings }) {\n  return (\n    <div className=\"flex gap-x-4\">\n      <div className=\"flex flex-col w-60\">\n        <label className=\"text-white text-sm font-semibold block mb-3\">\n          SearXNG API Base URL\n        </label>\n        <input\n          type=\"url\"\n          name=\"env::AgentSearXNGApiUrl\"\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n          placeholder=\"SearXNG API Base URL\"\n          defaultValue={settings?.AgentSearXNGApiUrl}\n          required={true}\n          autoComplete=\"off\"\n          spellCheck={false}\n        />\n      </div>\n    </div>\n  );\n}\n\nexport function TavilySearchOptions({ settings }) {\n  return (\n    <>\n      <p className=\"text-sm text-white/60 my-2\">\n        You can get an API key{\" \"}\n        <a\n          href=\"https://tavily.com/\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"text-blue-300 underline\"\n        >\n          from Tavily.\n        </a>\n      </p>\n      <div className=\"flex gap-x-4\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"env::AgentTavilyApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Tavily API Key\"\n            defaultValue={settings?.AgentTavilyApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n    </>\n  );\n}\n\nexport function DuckDuckGoOptions() {\n  return (\n    <>\n      <p className=\"text-sm text-white/60 my-2\">\n        DuckDuckGo is ready to use without any additional configuration.\n      </p>\n    </>\n  );\n}\n\nexport function ExaSearchOptions({ settings }) {\n  return (\n    <>\n      <p className=\"text-sm text-white/60 my-2\">\n        You can get an API key{\" \"}\n        <a\n          href=\"https://exa.ai\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"text-blue-300 underline\"\n        >\n          from Exa.\n        </a>\n      </p>\n      <div className=\"flex gap-x-4\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"env::AgentExaApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Exa API Key\"\n            defaultValue={settings?.AgentExaApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n    </>\n  );\n}\n\nexport function PerplexitySearchOptions({ settings }) {\n  return (\n    <>\n      <p className=\"text-sm text-white/60 my-2\">\n        You can get an API key{\" \"}\n        <a\n          href=\"https://console.perplexity.ai\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"text-blue-300 underline\"\n        >\n          from Perplexity.\n        </a>\n      </p>\n      <div className=\"flex gap-x-4\">\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-white text-sm font-semibold block mb-3\">\n            API Key\n          </label>\n          <input\n            type=\"password\"\n            name=\"env::AgentPerplexityApiKey\"\n            className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            placeholder=\"Perplexity API Key\"\n            defaultValue={settings?.AgentPerplexityApiKey ? \"*\".repeat(20) : \"\"}\n            required={true}\n            autoComplete=\"off\"\n            spellCheck={false}\n          />\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx",
    "content": "import React, { useEffect, useRef, useState } from \"react\";\nimport Admin from \"@/models/admin\";\nimport AnythingLLMIcon from \"@/media/logo/anything-llm-icon.png\";\nimport SerpApiIcon from \"./icons/serpapi.png\";\nimport SearchApiIcon from \"./icons/searchapi.png\";\nimport SerperDotDevIcon from \"./icons/serper.png\";\nimport BingSearchIcon from \"./icons/bing.png\";\nimport SerplySearchIcon from \"./icons/serply.png\";\nimport SearXNGSearchIcon from \"./icons/searxng.png\";\nimport TavilySearchIcon from \"./icons/tavily.svg\";\nimport DuckDuckGoIcon from \"./icons/duckduckgo.png\";\nimport ExaIcon from \"./icons/exa.png\";\nimport PerplexitySearchIcon from \"./icons/perplexity.png\";\nimport {\n  CaretUpDown,\n  MagnifyingGlass,\n  X,\n  ListMagnifyingGlass,\n} from \"@phosphor-icons/react\";\nimport Toggle from \"@/components/lib/Toggle\";\nimport SearchProviderItem from \"./SearchProviderItem\";\nimport WebSearchImage from \"@/media/agents/scrape-websites.png\";\nimport {\n  SerpApiOptions,\n  SearchApiOptions,\n  SerperDotDevOptions,\n  BingSearchOptions,\n  SerplySearchOptions,\n  SearXNGOptions,\n  TavilySearchOptions,\n  DuckDuckGoOptions,\n  ExaSearchOptions,\n  PerplexitySearchOptions,\n} from \"./SearchProviderOptions\";\n\nconst SEARCH_PROVIDERS = [\n  {\n    name: \"Please make a selection\",\n    value: \"none\",\n    logo: AnythingLLMIcon,\n    options: () => <React.Fragment />,\n    description:\n      \"Web search will be disabled until a provider and keys are provided.\",\n  },\n  {\n    name: \"DuckDuckGo\",\n    value: \"duckduckgo-engine\",\n    logo: DuckDuckGoIcon,\n    options: () => <DuckDuckGoOptions />,\n    description: \"Free and privacy-focused web search using DuckDuckGo.\",\n  },\n  {\n    name: \"SerpApi\",\n    value: \"serpapi\",\n    logo: SerpApiIcon,\n    options: (settings) => <SerpApiOptions settings={settings} />,\n    description:\n      \"Scrape Google and several other search engines with SerpApi. 250 free searches every month, and then paid.\",\n  },\n  {\n    name: \"SearchApi\",\n    value: \"searchapi\",\n    logo: SearchApiIcon,\n    options: (settings) => <SearchApiOptions settings={settings} />,\n    description:\n      \"SearchApi delivers structured data from multiple search engines. Free for 100 queries, but then paid. \",\n  },\n  {\n    name: \"Serper.dev\",\n    value: \"serper-dot-dev\",\n    logo: SerperDotDevIcon,\n    options: (settings) => <SerperDotDevOptions settings={settings} />,\n    description:\n      \"Serper.dev web-search. Free account with a 2,500 calls, but then paid.\",\n  },\n  {\n    name: \"Bing Search\",\n    value: \"bing-search\",\n    logo: BingSearchIcon,\n    options: (settings) => <BingSearchOptions settings={settings} />,\n    description: \"Web search powered by the Bing Search API (paid service).\",\n  },\n  {\n    name: \"Serply.io\",\n    value: \"serply-engine\",\n    logo: SerplySearchIcon,\n    options: (settings) => <SerplySearchOptions settings={settings} />,\n    description:\n      \"Serply.io web-search. Free account with a 100 calls/month forever.\",\n  },\n  {\n    name: \"SearXNG\",\n    value: \"searxng-engine\",\n    logo: SearXNGSearchIcon,\n    options: (settings) => <SearXNGOptions settings={settings} />,\n    description:\n      \"Free, open-source, internet meta-search engine with no tracking.\",\n  },\n  {\n    name: \"Tavily Search\",\n    value: \"tavily-search\",\n    logo: TavilySearchIcon,\n    options: (settings) => <TavilySearchOptions settings={settings} />,\n    description:\n      \"Tavily Search API. Offers a free tier with 1000 queries per month.\",\n  },\n  {\n    name: \"Exa Search\",\n    value: \"exa-search\",\n    logo: ExaIcon,\n    options: (settings) => <ExaSearchOptions settings={settings} />,\n    description:\n      \"One of the best web search APIs for AI agents with real-time results and full page contents.\",\n  },\n  {\n    name: \"Perplexity Search\",\n    value: \"perplexity-search\",\n    logo: PerplexitySearchIcon,\n    options: (settings) => <PerplexitySearchOptions settings={settings} />,\n    description: \"AI-powered web search using the Perplexity Search API.\",\n  },\n];\n\nexport default function AgentWebSearchSelection({\n  skill,\n  title,\n  description,\n  settings,\n  toggleSkill,\n  enabled = false,\n  setHasChanges,\n}) {\n  const searchInputRef = useRef(null);\n  const [filteredResults, setFilteredResults] = useState([]);\n  const [selectedProvider, setSelectedProvider] = useState(\"none\");\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [searchMenuOpen, setSearchMenuOpen] = useState(false);\n\n  function updateChoice(selection) {\n    setSearchQuery(\"\");\n    setSelectedProvider(selection);\n    setSearchMenuOpen(false);\n    setHasChanges(true);\n  }\n\n  function handleXButton() {\n    if (searchQuery.length > 0) {\n      setSearchQuery(\"\");\n      if (searchInputRef.current) searchInputRef.current.value = \"\";\n    } else {\n      setSearchMenuOpen(!searchMenuOpen);\n    }\n  }\n\n  useEffect(() => {\n    const filtered = SEARCH_PROVIDERS.filter((provider) =>\n      provider.name.toLowerCase().includes(searchQuery.toLowerCase())\n    );\n    setFilteredResults(filtered);\n  }, [searchQuery, selectedProvider]);\n\n  useEffect(() => {\n    Admin.systemPreferencesByFields([\"agent_search_provider\"])\n      .then((res) =>\n        setSelectedProvider(res?.settings?.agent_search_provider ?? \"none\")\n      )\n      .catch(() => setSelectedProvider(\"none\"));\n  }, []);\n\n  const selectedSearchProviderObject =\n    SEARCH_PROVIDERS.find((provider) => provider.value === selectedProvider) ??\n    SEARCH_PROVIDERS[1];\n\n  return (\n    <div className=\"p-2\">\n      <div className=\"flex flex-col gap-y-[18px] max-w-[500px]\">\n        <div className=\"flex w-full justify-between items-center\">\n          <div className=\"flex items-center gap-x-2\">\n            <ListMagnifyingGlass\n              size={24}\n              color=\"var(--theme-text-primary)\"\n              weight=\"bold\"\n            />\n            <label\n              htmlFor=\"name\"\n              className=\"text-theme-text-primary text-md font-bold\"\n            >\n              {title}\n            </label>\n          </div>\n          <Toggle\n            size=\"lg\"\n            enabled={enabled}\n            onChange={() => toggleSkill(skill)}\n          />\n        </div>\n        <img\n          src={WebSearchImage}\n          alt=\"Web Search\"\n          className=\"w-full rounded-md\"\n        />\n        <p className=\"text-theme-text-secondary text-opacity-60 text-xs font-medium py-1.5\">\n          {description}\n        </p>\n        <div hidden={!enabled}>\n          <div className=\"relative\">\n            <input\n              type=\"hidden\"\n              name=\"system::agent_search_provider\"\n              value={selectedProvider}\n            />\n            {searchMenuOpen && (\n              <div\n                className=\"fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10\"\n                onClick={() => setSearchMenuOpen(false)}\n              />\n            )}\n            {searchMenuOpen ? (\n              <div className=\"absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] min-h-[64px] bg-theme-settings-input-bg rounded-lg flex flex-col justify-between cursor-pointer border-2 border-primary-button z-20\">\n                <div className=\"w-full flex flex-col gap-y-1\">\n                  <div className=\"flex items-center sticky top-0 z-10 border-b border-[#9CA3AF] mx-4 bg-theme-settings-input-bg\">\n                    <MagnifyingGlass\n                      size={20}\n                      weight=\"bold\"\n                      className=\"absolute left-4 z-30 text-theme-text-primary -ml-4 my-2\"\n                    />\n                    <input\n                      type=\"text\"\n                      name=\"web-provider-search\"\n                      autoComplete=\"off\"\n                      placeholder=\"Search available web-search providers\"\n                      className=\"border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none text-theme-text-primary placeholder:text-theme-text-primary placeholder:font-medium\"\n                      onChange={(e) => setSearchQuery(e.target.value)}\n                      ref={searchInputRef}\n                      onKeyDown={(e) => {\n                        if (e.key === \"Enter\") e.preventDefault();\n                      }}\n                    />\n                    <X\n                      size={20}\n                      weight=\"bold\"\n                      className=\"cursor-pointer text-white hover:text-x-button\"\n                      onClick={handleXButton}\n                    />\n                  </div>\n                  <div className=\"flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4 max-h-[245px]\">\n                    {filteredResults.map((provider) => {\n                      return (\n                        <SearchProviderItem\n                          provider={provider}\n                          key={provider.name}\n                          checked={selectedProvider === provider.value}\n                          onClick={() => updateChoice(provider.value)}\n                        />\n                      );\n                    })}\n                  </div>\n                </div>\n              </div>\n            ) : (\n              <button\n                className=\"w-full max-w-[640px] h-[64px] bg-theme-settings-input-bg rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-primary-button transition-all duration-300\"\n                type=\"button\"\n                onClick={() => setSearchMenuOpen(true)}\n              >\n                <div className=\"flex gap-x-4 items-center\">\n                  <img\n                    src={selectedSearchProviderObject.logo}\n                    alt={`${selectedSearchProviderObject.name} logo`}\n                    className=\"w-10 h-10 rounded-md\"\n                  />\n                  <div className=\"flex flex-col text-left\">\n                    <div className=\"text-sm font-semibold text-white\">\n                      {selectedSearchProviderObject.name}\n                    </div>\n                    <div className=\"mt-1 text-xs text-description\">\n                      {selectedSearchProviderObject.description}\n                    </div>\n                  </div>\n                </div>\n                <CaretUpDown size={24} weight=\"bold\" className=\"text-white\" />\n              </button>\n            )}\n          </div>\n          {selectedProvider !== \"none\" && (\n            <div className=\"mt-4 flex flex-col gap-y-1\">\n              {selectedSearchProviderObject.options(settings)}\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/index.jsx",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport Admin from \"@/models/admin\";\nimport System from \"@/models/system\";\nimport MCPServers from \"@/models/mcpServers\";\nimport showToast from \"@/utils/toast\";\nimport {\n  CaretLeft,\n  CaretRight,\n  Plug,\n  Robot,\n  Hammer,\n  FlowArrow,\n} from \"@phosphor-icons/react\";\nimport ContextualSaveBar from \"@/components/ContextualSaveBar\";\nimport { castToType } from \"@/utils/types\";\nimport { FullScreenLoader } from \"@/components/Preloader\";\nimport { getDefaultSkills, getConfigurableSkills } from \"./skills\";\nimport { DefaultBadge } from \"./Badges/default\";\nimport ImportedSkillList from \"./Imported/SkillList\";\nimport ImportedSkillConfig from \"./Imported/ImportedSkillConfig\";\nimport { Tooltip } from \"react-tooltip\";\nimport AgentFlowsList from \"./AgentFlows\";\nimport FlowPanel from \"./AgentFlows/FlowPanel\";\nimport { MCPServersList, MCPServerHeader } from \"./MCPServers\";\nimport ServerPanel from \"./MCPServers/ServerPanel\";\nimport { Link } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\nimport AgentFlows from \"@/models/agentFlows\";\nimport AgentSkillSettings from \"./AgentSkillSettings\";\n\nconst IGNORE_CHANGE_SETTINGS = [\n  \"agentSkillRerankerEnabled\",\n  \"agentSkillRerankerTopN\",\n  \"agentSkillMaxToolCalls\",\n];\n\nexport default function AdminAgents() {\n  const { t } = useTranslation();\n  const formEl = useRef(null);\n  const [hasChanges, setHasChanges] = useState(false);\n  const [settings, setSettings] = useState({});\n  const [selectedSkill, setSelectedSkill] = useState(\"\");\n  const [loading, setLoading] = useState(true);\n  const [showSkillModal, setShowSkillModal] = useState(false);\n\n  const [agentSkills, setAgentSkills] = useState([]);\n  const [importedSkills, setImportedSkills] = useState([]);\n  const [disabledAgentSkills, setDisabledAgentSkills] = useState([]);\n\n  const [agentFlows, setAgentFlows] = useState([]);\n  const [selectedFlow, setSelectedFlow] = useState(null);\n  const [activeFlowIds, setActiveFlowIds] = useState([]);\n\n  // MCP Servers are lazy loaded to not block the UI thread\n  const [mcpServers, setMcpServers] = useState([]);\n  const [selectedMcpServer, setSelectedMcpServer] = useState(null);\n\n  const defaultSkills = getDefaultSkills(t);\n  const configurableSkills = getConfigurableSkills(t);\n\n  // Alert user if they try to leave the page with unsaved changes\n  useEffect(() => {\n    const handleBeforeUnload = (event) => {\n      if (hasChanges) {\n        event.preventDefault();\n        event.returnValue = \"\";\n      }\n    };\n    window.addEventListener(\"beforeunload\", handleBeforeUnload);\n    return () => {\n      window.removeEventListener(\"beforeunload\", handleBeforeUnload);\n    };\n  }, [hasChanges]);\n\n  useEffect(() => {\n    async function fetchSettings() {\n      const _settings = await System.keys();\n      const _preferences = await Admin.systemPreferencesByFields([\n        \"disabled_agent_skills\",\n        \"default_agent_skills\",\n        \"imported_agent_skills\",\n        \"active_agent_flows\",\n      ]);\n      const { flows = [] } = await AgentFlows.listFlows();\n\n      setSettings({ ..._settings, preferences: _preferences.settings } ?? {});\n      setAgentSkills(_preferences.settings?.default_agent_skills ?? []);\n      setDisabledAgentSkills(\n        _preferences.settings?.disabled_agent_skills ?? []\n      );\n      setImportedSkills(_preferences.settings?.imported_agent_skills ?? []);\n      setActiveFlowIds(_preferences.settings?.active_agent_flows ?? []);\n      setAgentFlows(flows);\n      setLoading(false);\n    }\n    fetchSettings();\n  }, []);\n\n  const toggleDefaultSkill = (skillName) => {\n    setDisabledAgentSkills((prev) => {\n      const updatedSkills = prev.includes(skillName)\n        ? prev.filter((name) => name !== skillName)\n        : [...prev, skillName];\n      setHasChanges(true);\n      return updatedSkills;\n    });\n  };\n\n  const toggleAgentSkill = (skillName) => {\n    setAgentSkills((prev) => {\n      const updatedSkills = prev.includes(skillName)\n        ? prev.filter((name) => name !== skillName)\n        : [...prev, skillName];\n      setHasChanges(true);\n      return updatedSkills;\n    });\n  };\n\n  const toggleFlow = (flowId) => {\n    setActiveFlowIds((prev) => {\n      const updatedFlows = prev.includes(flowId)\n        ? prev.filter((id) => id !== flowId)\n        : [...prev, flowId];\n      return updatedFlows;\n    });\n  };\n\n  const toggleMCP = (serverName) => {\n    setMcpServers((prev) => {\n      return prev.map((server) => {\n        if (server.name !== serverName) return server;\n        return { ...server, running: !server.running };\n      });\n    });\n  };\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const data = {\n      workspace: {},\n      system: {},\n      env: {},\n    };\n\n    const form = new FormData(formEl.current);\n    for (var [key, value] of form.entries()) {\n      if (key.startsWith(\"system::\")) {\n        const [_, label] = key.split(\"system::\");\n        data.system[label] = String(value);\n        continue;\n      }\n\n      if (key.startsWith(\"env::\")) {\n        const [_, label] = key.split(\"env::\");\n        data.env[label] = String(value);\n        continue;\n      }\n      data.workspace[key] = castToType(key, value);\n    }\n\n    const { success } = await Admin.updateSystemPreferences(data.system);\n    await System.updateSystem(data.env);\n\n    if (success) {\n      const _settings = await System.keys();\n      const _preferences = await Admin.systemPreferencesByFields([\n        \"disabled_agent_skills\",\n        \"default_agent_skills\",\n        \"imported_agent_skills\",\n      ]);\n      setSettings({ ..._settings, preferences: _preferences.settings } ?? {});\n      setAgentSkills(_preferences.settings?.default_agent_skills ?? []);\n      setDisabledAgentSkills(\n        _preferences.settings?.disabled_agent_skills ?? []\n      );\n      setImportedSkills(_preferences.settings?.imported_agent_skills ?? []);\n      showToast(`Agent preferences saved successfully.`, \"success\", {\n        clear: true,\n      });\n    } else {\n      showToast(`Agent preferences failed to save.`, \"error\", { clear: true });\n    }\n\n    setHasChanges(false);\n  };\n\n  let SelectedSkillComponent = null;\n  if (selectedFlow) {\n    SelectedSkillComponent = FlowPanel;\n  } else if (selectedMcpServer) {\n    SelectedSkillComponent = ServerPanel;\n  } else if (selectedSkill?.imported) {\n    SelectedSkillComponent = ImportedSkillConfig;\n  } else if (configurableSkills[selectedSkill]) {\n    SelectedSkillComponent = configurableSkills[selectedSkill]?.component;\n  } else {\n    SelectedSkillComponent = defaultSkills[selectedSkill]?.component;\n  }\n\n  // Update the click handlers to clear the other selection\n  const handleDefaultSkillClick = (skill) => {\n    setSelectedFlow(null);\n    setSelectedMcpServer(null);\n    setSelectedSkill(skill);\n    if (isMobile) setShowSkillModal(true);\n  };\n\n  const handleSkillClick = (skill) => {\n    setSelectedFlow(null);\n    setSelectedMcpServer(null);\n    setSelectedSkill(skill);\n    if (isMobile) setShowSkillModal(true);\n  };\n\n  const handleFlowClick = (flow) => {\n    setSelectedSkill(null);\n    setSelectedMcpServer(null);\n    setSelectedFlow(flow);\n    if (isMobile) setShowSkillModal(true);\n  };\n\n  const handleMCPClick = (server) => {\n    setSelectedSkill(null);\n    setSelectedFlow(null);\n    setSelectedMcpServer(server);\n    if (isMobile) setShowSkillModal(true);\n  };\n\n  const handleFlowDelete = (flowId) => {\n    setSelectedFlow(null);\n    setActiveFlowIds((prev) => prev.filter((id) => id !== flowId));\n    setAgentFlows((prev) => prev.filter((flow) => flow.uuid !== flowId));\n  };\n\n  const handleMCPServerDelete = (serverName) => {\n    setSelectedMcpServer(null);\n    setMcpServers((prev) =>\n      prev.filter((server) => server.name !== serverName)\n    );\n  };\n\n  const handleMCPToolToggle = async (serverName, toolName, enabled) => {\n    const { success, error, suppressedTools } = await MCPServers.toggleTool(\n      serverName,\n      toolName,\n      enabled\n    );\n\n    if (!success) {\n      showToast(error || \"Failed to toggle tool.\", \"error\", { clear: true });\n      return;\n    }\n\n    setMcpServers((prev) =>\n      prev.map((server) => {\n        if (server.name !== serverName) return server;\n        return {\n          ...server,\n          config: {\n            ...server.config,\n            anythingllm: {\n              ...server.config?.anythingllm,\n              suppressedTools,\n            },\n          },\n        };\n      })\n    );\n\n    setSelectedMcpServer((prev) => {\n      if (!prev || prev.name !== serverName) return prev;\n      return {\n        ...prev,\n        config: {\n          ...prev.config,\n          anythingllm: {\n            ...prev.config?.anythingllm,\n            suppressedTools,\n          },\n        },\n      };\n    });\n  };\n\n  if (loading) {\n    return (\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] w-full h-full flex justify-center items-center\"\n      >\n        <FullScreenLoader />\n      </div>\n    );\n  }\n\n  if (isMobile) {\n    return (\n      <SkillLayout\n        hasChanges={hasChanges}\n        handleCancel={() => setHasChanges(false)}\n        handleSubmit={handleSubmit}\n      >\n        <form\n          onSubmit={handleSubmit}\n          onChange={() => !selectedFlow && setHasChanges(true)}\n          ref={formEl}\n          className=\"flex flex-col w-full p-4 mt-10\"\n        >\n          <input\n            name=\"system::default_agent_skills\"\n            type=\"hidden\"\n            value={agentSkills.join(\",\")}\n          />\n          <input\n            name=\"system::disabled_agent_skills\"\n            type=\"hidden\"\n            value={disabledAgentSkills.join(\",\")}\n          />\n\n          {/* Skill settings nav */}\n          <div\n            hidden={showSkillModal}\n            className=\"flex flex-col gap-y-[18px] overflow-y-scroll no-scroll\"\n          >\n            <div className=\"text-theme-text-primary flex items-center gap-x-2\">\n              <Robot size={24} />\n              <p className=\"text-lg font-medium\">Agent Skills</p>\n            </div>\n            {/* Default skills */}\n            <SkillList\n              skills={defaultSkills}\n              selectedSkill={selectedSkill}\n              handleClick={handleDefaultSkillClick}\n              activeSkills={Object.keys(defaultSkills).filter(\n                (skill) => !disabledAgentSkills.includes(skill)\n              )}\n            />\n            {/* Configurable skills */}\n            <SkillList\n              skills={configurableSkills}\n              selectedSkill={selectedSkill}\n              handleClick={handleDefaultSkillClick}\n              activeSkills={agentSkills}\n            />\n\n            <div className=\"text-theme-text-primary flex items-center gap-x-2\">\n              <Plug size={24} />\n              <p className=\"text-lg font-medium\">Custom Skills</p>\n            </div>\n            <ImportedSkillList\n              skills={importedSkills}\n              selectedSkill={selectedSkill}\n              handleClick={handleSkillClick}\n            />\n\n            <div className=\"text-theme-text-primary flex items-center gap-x-2 mt-6\">\n              <FlowArrow size={24} />\n              <p className=\"text-lg font-medium\">Agent Flows</p>\n            </div>\n            <AgentFlowsList\n              flows={agentFlows}\n              selectedFlow={selectedFlow}\n              handleClick={handleFlowClick}\n            />\n            <input\n              type=\"hidden\"\n              name=\"system::active_agent_flows\"\n              id=\"active_agent_flows\"\n              value={activeFlowIds.join(\",\")}\n            />\n            <MCPServerHeader\n              setMcpServers={setMcpServers}\n              setSelectedMcpServer={setSelectedMcpServer}\n            >\n              {({ loadingMcpServers }) => {\n                return (\n                  <MCPServersList\n                    isLoading={loadingMcpServers}\n                    servers={mcpServers}\n                    selectedServer={selectedMcpServer}\n                    handleClick={handleMCPClick}\n                  />\n                );\n              }}\n            </MCPServerHeader>\n          </div>\n\n          {/* Selected agent skill modal */}\n          {showSkillModal && (\n            <div className=\"fixed top-0 left-0 w-full h-full bg-sidebar z-30\">\n              <div className=\"flex flex-col h-full\">\n                <div className=\"flex items-center p-4\">\n                  <button\n                    type=\"button\"\n                    onClick={() => {\n                      setShowSkillModal(false);\n                      setSelectedSkill(\"\");\n                    }}\n                    className=\"text-white/60 hover:text-white transition-colors duration-200\"\n                  >\n                    <div className=\"flex items-center text-sky-400\">\n                      <CaretLeft size={24} />\n                      <div>Back</div>\n                    </div>\n                  </button>\n                </div>\n                <div className=\"flex-1 overflow-y-auto p-4\">\n                  <div className=\" bg-theme-bg-secondary text-white rounded-xl p-4 overflow-y-scroll no-scroll\">\n                    {SelectedSkillComponent ? (\n                      <>\n                        {selectedMcpServer ? (\n                          <ServerPanel\n                            server={selectedMcpServer}\n                            toggleServer={toggleMCP}\n                            onDelete={handleMCPServerDelete}\n                            onToggleTool={handleMCPToolToggle}\n                          />\n                        ) : selectedFlow ? (\n                          <FlowPanel\n                            flow={selectedFlow}\n                            toggleFlow={toggleFlow}\n                            enabled={activeFlowIds.includes(selectedFlow.uuid)}\n                            onDelete={handleFlowDelete}\n                          />\n                        ) : selectedSkill.imported ? (\n                          <ImportedSkillConfig\n                            key={selectedSkill.hubId}\n                            selectedSkill={selectedSkill}\n                            setImportedSkills={setImportedSkills}\n                          />\n                        ) : (\n                          <>\n                            {defaultSkills?.[selectedSkill] ? (\n                              // The selected skill is a default skill - show the default skill panel\n                              <SelectedSkillComponent\n                                skill={defaultSkills[selectedSkill]?.skill}\n                                settings={settings}\n                                toggleSkill={toggleDefaultSkill}\n                                enabled={\n                                  !disabledAgentSkills.includes(\n                                    defaultSkills[selectedSkill]?.skill\n                                  )\n                                }\n                                setHasChanges={setHasChanges}\n                                {...defaultSkills[selectedSkill]}\n                              />\n                            ) : (\n                              // The selected skill is a configurable skill - show the configurable skill panel\n                              <SelectedSkillComponent\n                                skill={configurableSkills[selectedSkill]?.skill}\n                                settings={settings}\n                                toggleSkill={toggleAgentSkill}\n                                enabled={agentSkills.includes(\n                                  configurableSkills[selectedSkill]?.skill\n                                )}\n                                setHasChanges={setHasChanges}\n                                hasChanges={hasChanges}\n                                {...configurableSkills[selectedSkill]}\n                              />\n                            )}\n                          </>\n                        )}\n                      </>\n                    ) : (\n                      <div className=\"flex flex-col items-center justify-center h-full text-theme-text-secondary\">\n                        <Robot size={40} />\n                        <p className=\"font-medium\">\n                          Select an Agent Skill, Agent Flow, or MCP Server\n                        </p>\n                      </div>\n                    )}\n                  </div>\n                </div>\n              </div>\n            </div>\n          )}\n        </form>\n      </SkillLayout>\n    );\n  }\n\n  return (\n    <SkillLayout\n      hasChanges={hasChanges}\n      handleCancel={() => setHasChanges(false)}\n      handleSubmit={handleSubmit}\n    >\n      <form\n        onSubmit={handleSubmit}\n        onChange={(e) => {\n          if (IGNORE_CHANGE_SETTINGS.includes(e.target.name)) return;\n          if (!selectedSkill?.imported && !selectedFlow) setHasChanges(true);\n        }}\n        ref={formEl}\n        className=\"flex-1 flex gap-x-6 p-4 mt-10\"\n      >\n        <input\n          name=\"system::default_agent_skills\"\n          type=\"hidden\"\n          value={agentSkills.join(\",\")}\n        />\n        <input\n          name=\"system::disabled_agent_skills\"\n          type=\"hidden\"\n          value={disabledAgentSkills.join(\",\")}\n        />\n        <input\n          type=\"hidden\"\n          name=\"system::active_agent_flows\"\n          id=\"active_agent_flows\"\n          value={activeFlowIds.join(\",\")}\n        />\n\n        {/* Skill settings nav - Make this section scrollable */}\n        <div className=\"flex flex-col min-w-[360px] h-[calc(100vh-90px)]\">\n          <div className=\"flex-none flex justify-between items-center mb-4\">\n            <div className=\"text-theme-text-primary flex items-center gap-x-2\">\n              <Robot size={24} />\n              <p className=\"text-lg font-medium\">Agent Skills</p>\n            </div>\n            <AgentSkillSettings />\n          </div>\n\n          <div className=\"flex-1 overflow-y-auto pr-2 pb-4\">\n            <div className=\"space-y-4\">\n              {/* Default skills list */}\n              <SkillList\n                skills={defaultSkills}\n                selectedSkill={selectedSkill}\n                handleClick={handleSkillClick}\n                activeSkills={Object.keys(defaultSkills).filter(\n                  (skill) => !disabledAgentSkills.includes(skill)\n                )}\n              />\n              {/* Configurable skills */}\n              <SkillList\n                skills={configurableSkills}\n                selectedSkill={selectedSkill}\n                handleClick={handleSkillClick}\n                activeSkills={agentSkills}\n              />\n\n              <div className=\"text-theme-text-primary flex items-center gap-x-2 mt-4\">\n                <Plug size={24} />\n                <p className=\"text-lg font-medium\">Custom Skills</p>\n              </div>\n              <ImportedSkillList\n                skills={importedSkills}\n                selectedSkill={selectedSkill}\n                handleClick={handleSkillClick}\n              />\n\n              <div className=\"text-theme-text-primary flex items-center justify-between gap-x-2 mt-4\">\n                <div className=\"flex items-center gap-x-2\">\n                  <FlowArrow size={24} />\n                  <p className=\"text-lg font-medium\">Agent Flows</p>\n                </div>\n                {agentFlows.length === 0 ? (\n                  <Link\n                    to={paths.agents.builder()}\n                    className=\"text-cta-button flex items-center gap-x-1 hover:underline\"\n                  >\n                    <Hammer size={16} />\n                    <p className=\"text-sm\">Create Flow</p>\n                  </Link>\n                ) : (\n                  <Link\n                    to={paths.agents.builder()}\n                    className=\"text-theme-text-secondary hover:text-cta-button flex items-center gap-x-1\"\n                  >\n                    <Hammer size={16} />\n                    <p className=\"text-sm\">Open Builder</p>\n                  </Link>\n                )}\n              </div>\n              <AgentFlowsList\n                flows={agentFlows}\n                selectedFlow={selectedFlow}\n                handleClick={handleFlowClick}\n              />\n\n              <MCPServerHeader\n                setMcpServers={setMcpServers}\n                setSelectedMcpServer={setSelectedMcpServer}\n              >\n                {({ loadingMcpServers }) => {\n                  return (\n                    <MCPServersList\n                      isLoading={loadingMcpServers}\n                      servers={mcpServers}\n                      selectedServer={selectedMcpServer}\n                      handleClick={handleMCPClick}\n                    />\n                  );\n                }}\n              </MCPServerHeader>\n            </div>\n          </div>\n        </div>\n\n        {/* Selected agent skill setting panel */}\n        <div className=\"flex-[2] flex flex-col gap-y-[18px] mt-10\">\n          <div className=\"bg-theme-bg-secondary text-white rounded-xl flex-1 p-4 overflow-y-scroll no-scroll\">\n            {SelectedSkillComponent ? (\n              <>\n                {selectedMcpServer ? (\n                  <ServerPanel\n                    server={selectedMcpServer}\n                    toggleServer={toggleMCP}\n                    onDelete={handleMCPServerDelete}\n                    onToggleTool={handleMCPToolToggle}\n                  />\n                ) : selectedFlow ? (\n                  <FlowPanel\n                    flow={selectedFlow}\n                    toggleFlow={toggleFlow}\n                    enabled={activeFlowIds.includes(selectedFlow.uuid)}\n                    onDelete={handleFlowDelete}\n                  />\n                ) : selectedSkill.imported ? (\n                  <ImportedSkillConfig\n                    key={selectedSkill.hubId}\n                    selectedSkill={selectedSkill}\n                    setImportedSkills={setImportedSkills}\n                  />\n                ) : (\n                  <>\n                    {defaultSkills?.[selectedSkill] ? (\n                      // The selected skill is a default skill - show the default skill panel\n                      <SelectedSkillComponent\n                        skill={defaultSkills[selectedSkill]?.skill}\n                        settings={settings}\n                        toggleSkill={toggleDefaultSkill}\n                        enabled={\n                          !disabledAgentSkills.includes(\n                            defaultSkills[selectedSkill]?.skill\n                          )\n                        }\n                        setHasChanges={setHasChanges}\n                        {...defaultSkills[selectedSkill]}\n                      />\n                    ) : (\n                      // The selected skill is a configurable skill - show the configurable skill panel\n                      <SelectedSkillComponent\n                        skill={configurableSkills[selectedSkill]?.skill}\n                        settings={settings}\n                        toggleSkill={toggleAgentSkill}\n                        enabled={agentSkills.includes(\n                          configurableSkills[selectedSkill]?.skill\n                        )}\n                        setHasChanges={setHasChanges}\n                        hasChanges={hasChanges}\n                        {...configurableSkills[selectedSkill]}\n                      />\n                    )}\n                  </>\n                )}\n              </>\n            ) : (\n              <div className=\"flex flex-col items-center justify-center h-full text-theme-text-secondary\">\n                <Robot size={40} />\n                <p className=\"font-medium\">\n                  Select an Agent Skill, Agent Flow, or MCP Server\n                </p>\n              </div>\n            )}\n          </div>\n        </div>\n      </form>\n    </SkillLayout>\n  );\n}\n\nfunction SkillLayout({ children, hasChanges, handleSubmit, handleCancel }) {\n  return (\n    <div\n      id=\"workspace-agent-settings-container\"\n      className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex md:mt-0 mt-6\"\n    >\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] w-full h-full flex\"\n      >\n        {children}\n        <ContextualSaveBar\n          showing={hasChanges}\n          onSave={handleSubmit}\n          onCancel={handleCancel}\n        />\n      </div>\n    </div>\n  );\n}\n\nfunction SkillList({\n  isDefault = false,\n  skills = [],\n  selectedSkill = null,\n  handleClick = null,\n  activeSkills = [],\n}) {\n  if (skills.length === 0) return null;\n\n  return (\n    <>\n      <div\n        className={`bg-theme-bg-secondary text-white rounded-xl ${\n          isMobile ? \"w-full\" : \"min-w-[360px] w-fit\"\n        }`}\n      >\n        {Object.entries(skills).map(([skill, settings], index) => (\n          <div\n            key={skill}\n            className={`py-3 px-4 flex items-center justify-between ${\n              index === 0 ? \"rounded-t-xl\" : \"\"\n            } ${\n              index === Object.keys(skills).length - 1\n                ? \"rounded-b-xl\"\n                : \"border-b border-white/10\"\n            } cursor-pointer transition-all duration-300  hover:bg-theme-bg-primary ${\n              selectedSkill === skill\n                ? \"bg-white/10 light:bg-theme-bg-sidebar\"\n                : \"\"\n            }`}\n            onClick={() => handleClick?.(skill)}\n          >\n            <div className=\"text-sm font-light\">{settings.title}</div>\n            <div className=\"flex items-center gap-x-2\">\n              {isDefault ? (\n                <DefaultBadge title={skill} />\n              ) : (\n                <div className=\"text-sm text-theme-text-secondary font-medium\">\n                  {activeSkills.includes(skill) ? \"On\" : \"Off\"}\n                </div>\n              )}\n              <CaretRight\n                size={14}\n                weight=\"bold\"\n                className=\"text-theme-text-secondary\"\n              />\n            </div>\n          </div>\n        ))}\n      </div>\n      {/* Tooltip for default skills - only render when skill list is passed isDefault */}\n      {isDefault && (\n        <Tooltip\n          id=\"default-skill\"\n          place=\"bottom\"\n          delayShow={300}\n          className=\"tooltip light:invert-0 !text-xs\"\n        />\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Agents/skills.js",
    "content": "import AgentWebSearchSelection from \"./WebSearchSelection\";\nimport AgentSQLConnectorSelection from \"./SQLConnectorSelection\";\nimport GenericSkillPanel from \"./GenericSkillPanel\";\nimport DefaultSkillPanel from \"./DefaultSkillPanel\";\nimport {\n  Brain,\n  File,\n  Browser,\n  ChartBar,\n  FileMagnifyingGlass,\n} from \"@phosphor-icons/react\";\nimport RAGImage from \"@/media/agents/rag-memory.png\";\nimport SummarizeImage from \"@/media/agents/view-summarize.png\";\nimport ScrapeWebsitesImage from \"@/media/agents/scrape-websites.png\";\nimport GenerateChartsImage from \"@/media/agents/generate-charts.png\";\nimport GenerateSaveImages from \"@/media/agents/generate-save-files.png\";\n\nexport const getDefaultSkills = (t) => ({\n  \"rag-memory\": {\n    title: t(\"agent.skill.rag.title\"),\n    description: t(\"agent.skill.rag.description\"),\n    component: DefaultSkillPanel,\n    icon: Brain,\n    image: RAGImage,\n    skill: \"rag-memory\",\n  },\n  \"document-summarizer\": {\n    title: t(\"agent.skill.view.title\"),\n    description: t(\"agent.skill.view.description\"),\n    component: DefaultSkillPanel,\n    icon: File,\n    image: SummarizeImage,\n    skill: \"document-summarizer\",\n  },\n  \"web-scraping\": {\n    title: t(\"agent.skill.scrape.title\"),\n    description: t(\"agent.skill.scrape.description\"),\n    component: DefaultSkillPanel,\n    icon: Browser,\n    image: ScrapeWebsitesImage,\n    skill: \"web-scraping\",\n  },\n});\n\nexport const getConfigurableSkills = (t) => ({\n  \"save-file-to-browser\": {\n    title: t(\"agent.skill.save.title\"),\n    description: t(\"agent.skill.save.description\"),\n    component: GenericSkillPanel,\n    skill: \"save-file-to-browser\",\n    icon: FileMagnifyingGlass,\n    image: GenerateSaveImages,\n  },\n  \"create-chart\": {\n    title: t(\"agent.skill.generate.title\"),\n    description: t(\"agent.skill.generate.description\"),\n    component: GenericSkillPanel,\n    skill: \"create-chart\",\n    icon: ChartBar,\n    image: GenerateChartsImage,\n  },\n  \"web-browsing\": {\n    title: t(\"agent.skill.web.title\"),\n    description: t(\"agent.skill.web.description\"),\n    component: AgentWebSearchSelection,\n    skill: \"web-browsing\",\n  },\n  \"sql-agent\": {\n    title: t(\"agent.skill.sql.title\"),\n    description: t(\"agent.skill.sql.description\"),\n    component: AgentSQLConnectorSelection,\n    skill: \"sql-agent\",\n  },\n});\n"
  },
  {
    "path": "frontend/src/pages/Admin/DefaultSystemPrompt/index.jsx",
    "content": "import SettingsSidebar from \"@/components/SettingsSidebar\";\nimport { useEffect, useState, Fragment } from \"react\";\nimport { isMobile } from \"react-device-detect\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport Highlighter from \"react-highlight-words\";\nimport SystemPromptVariable from \"@/models/systemPromptVariable\";\nimport { Link } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\n\nexport default function DefaultSystemPrompt() {\n  const [systemPromptForm, setSystemPromptForm] = useState({\n    value: \"\",\n    default: \"\",\n    isDirty: false,\n    isSubmitting: false,\n    isLoading: true,\n    isEditing: false,\n  });\n  const [saneDefaultSystemPrompt, setSaneDefaultSystemPrompt] = useState(\"\");\n  const [availableVariables, setAvailableVariables] = useState([]);\n  useEffect(() => {\n    async function setupVariableHighlighting() {\n      const { variables } = await SystemPromptVariable.getAll();\n      setAvailableVariables(variables);\n    }\n    setupVariableHighlighting();\n  }, []);\n\n  useEffect(() => {\n    async function fetchDefaultSystemPrompt() {\n      setSystemPromptForm((prev) => ({\n        ...prev,\n        isLoading: true,\n      }));\n      const { defaultSystemPrompt, saneDefaultSystemPrompt } =\n        await System.fetchDefaultSystemPrompt();\n      setSaneDefaultSystemPrompt(saneDefaultSystemPrompt);\n      if (!defaultSystemPrompt)\n        return setSystemPromptForm((prev) => ({\n          ...prev,\n          isLoading: false,\n        }));\n\n      setSystemPromptForm((prev) => ({\n        ...prev,\n        default: defaultSystemPrompt,\n        value: defaultSystemPrompt,\n        isLoading: false,\n      }));\n    }\n    fetchDefaultSystemPrompt();\n  }, []);\n\n  const handleChange = (e) => {\n    const value = e.target.value;\n    const isDirty = value !== systemPromptForm.default;\n\n    setSystemPromptForm((prev) => ({\n      ...prev,\n      value,\n      isDirty,\n      isSubmitting: false,\n    }));\n  };\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    setSystemPromptForm((prev) => ({\n      ...prev,\n      isSubmitting: true,\n    }));\n    const newSystemPrompt = systemPromptForm.value.trim();\n    await System.updateDefaultSystemPrompt(newSystemPrompt)\n      .then(({ success, message }) => {\n        if (!success) throw new Error(message);\n\n        // If the user has set the default system prompt to the sane default, reset the value to the sane default.\n        if (\n          !newSystemPrompt ||\n          newSystemPrompt.trim() === saneDefaultSystemPrompt\n        ) {\n          return setSystemPromptForm((prev) => ({\n            ...prev,\n            value: saneDefaultSystemPrompt,\n          }));\n        }\n\n        showToast(\"Default system prompt updated successfully.\", \"success\");\n        setSystemPromptForm((prev) => ({\n          ...prev,\n          default: newSystemPrompt,\n          isDirty: false,\n          isSubmitting: false,\n        }));\n      })\n      .catch((error) => {\n        showToast(\n          `Failed to update default system prompt: ${error.message}`,\n          \"error\"\n        );\n        setSystemPromptForm((prev) => ({\n          ...prev,\n          isSubmitting: false,\n        }));\n      });\n  };\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <SettingsSidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2\">\n            <div className=\"items-center flex gap-x-4\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                Default System Prompt\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary\">\n              This is the default system prompt that will be used for new\n              workspaces.\n            </p>\n          </div>\n          <div>\n            {systemPromptForm.isLoading ? (\n              <div className=\"mt-8 flex flex-col gap-y-4\">\n                <Skeleton.default\n                  height={20}\n                  width={160}\n                  highlightColor=\"var(--theme-bg-primary)\"\n                  baseColor=\"var(--theme-bg-secondary)\"\n                />\n                <Skeleton.default\n                  height={120}\n                  width=\"100%\"\n                  highlightColor=\"var(--theme-bg-primary)\"\n                  baseColor=\"var(--theme-bg-secondary)\"\n                  className=\"rounded-lg\"\n                />\n                <Skeleton.default\n                  height={36}\n                  width={140}\n                  highlightColor=\"var(--theme-bg-primary)\"\n                  baseColor=\"var(--theme-bg-secondary)\"\n                />\n              </div>\n            ) : (\n              <div className=\"mt-6\">\n                <form onSubmit={handleSubmit} className=\"space-y-3\">\n                  <label\n                    htmlFor=\"default-system-prompt\"\n                    className=\" text-base font-bold text-white\"\n                  >\n                    System Prompt\n                  </label>\n                  <div className=\"space-y-1\">\n                    <p className=\"text-white text-opacity-60 text-xs font-medium\">\n                      A system prompt provides instructions that shape the AI’s\n                      responses and behavior. This prompt will be automatically\n                      applied to all newly created workspaces. To change the\n                      system prompt of a{\" \"}\n                      <span className=\"font-bold\">specific workspace</span>,\n                      edit the prompt in the{\" \"}\n                      <span className=\"font-bold\">workspace settings</span>. To\n                      restore the system prompt to our sane default, leave this\n                      field empty and save changes.\n                    </p>\n                    <p className=\"text-white text-opacity-60 text-xs font-medium mb-2\">\n                      You can insert{\" \"}\n                      <Link\n                        to={paths.settings.systemPromptVariables()}\n                        className=\"text-primary-button\"\n                      >\n                        system prompt variables\n                      </Link>{\" \"}\n                      like:{\" \"}\n                      {availableVariables.slice(0, 3).map((v, i) => (\n                        <Fragment key={v.key}>\n                          <span className=\"bg-theme-settings-input-bg px-1 py-0.5 rounded\">\n                            {`{${v.key}}`}\n                          </span>\n                          {i < availableVariables.length - 1 && \", \"}\n                        </Fragment>\n                      ))}\n                      {availableVariables.length > 3 && (\n                        <Link\n                          to={paths.settings.systemPromptVariables()}\n                          className=\"text-primary-button\"\n                        >\n                          +{availableVariables.length - 3} more...\n                        </Link>\n                      )}\n                    </p>\n                  </div>\n\n                  {systemPromptForm.isEditing ? (\n                    <textarea\n                      autoFocus={true}\n                      value={systemPromptForm.value}\n                      onChange={handleChange}\n                      onBlur={() =>\n                        setSystemPromptForm((prev) => ({\n                          ...prev,\n                          isEditing: false,\n                        }))\n                      }\n                      placeholder={\n                        systemPromptForm.isLoading\n                          ? \"Loading...\"\n                          : \"You are an AI assistant that can answer questions and help with tasks.\"\n                      }\n                      rows={5}\n                      style={{\n                        resize: \"vertical\",\n                        overflowY: \"scroll\",\n                        minHeight: \"150px\",\n                      }}\n                      className=\"w-full border-none bg-theme-settings-input-bg placeholder:text-theme-settings-input-placeholder text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block p-2.5\"\n                    />\n                  ) : (\n                    <div\n                      onClick={() =>\n                        setSystemPromptForm((prev) => ({\n                          ...prev,\n                          isEditing: true,\n                        }))\n                      }\n                      style={{\n                        resize: \"vertical\",\n                        overflowY: \"scroll\",\n                        minHeight: \"150px\",\n                      }}\n                      className=\"w-full border-none bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block p-2.5 cursor-text\"\n                    >\n                      <Highlighter\n                        className=\"whitespace-pre-wrap\"\n                        highlightClassName=\"bg-cta-button p-0.5 rounded-md\"\n                        searchWords={availableVariables.map(\n                          (v) => `{${v.key}}`\n                        )}\n                        autoEscape={true}\n                        caseSensitive={true}\n                        textToHighlight={systemPromptForm.value || \"\"}\n                      />\n                    </div>\n                  )}\n                  <button\n                    disabled={\n                      !systemPromptForm.isDirty || systemPromptForm.isSubmitting\n                    }\n                    className={`enabled:hover:bg-secondary enabled:hover:text-white rounded-lg bg-primary-button w-fit py-2 px-4 font-semibold text-xs disabled:opacity-20 disabled:cursor-not-allowed`}\n                    type=\"submit\"\n                  >\n                    Save Changes\n                  </button>\n                </form>\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/ExperimentalFeatures/Features/LiveSync/manage/DocumentSyncQueueRow/index.jsx",
    "content": "import { useRef } from \"react\";\nimport { Trash } from \"@phosphor-icons/react\";\nimport { stripUuidAndJsonFromString } from \"@/components/Modals/ManageWorkspace/Documents/Directory/utils\";\nimport moment from \"moment\";\nimport System from \"@/models/system\";\n\nexport default function DocumentSyncQueueRow({ queue }) {\n  const rowRef = useRef(null);\n  const handleDelete = async () => {\n    rowRef?.current?.remove();\n    await System.experimentalFeatures.liveSync.setWatchStatusForDocument(\n      queue.workspaceDoc.workspace.slug,\n      queue.workspaceDoc.docpath,\n      false\n    );\n  };\n\n  return (\n    <>\n      <tr\n        ref={rowRef}\n        className=\"bg-transparent text-white text-opacity-80 text-sm font-medium\"\n      >\n        <td scope=\"row\" className=\"px-6 py-4 whitespace-nowrap\">\n          {stripUuidAndJsonFromString(queue.workspaceDoc.filename)}\n        </td>\n        <td className=\"px-6 py-4\">{moment(queue.lastSyncedAt).fromNow()}</td>\n        <td className=\"px-6 py-4\">\n          {moment(queue.nextSyncAt).format(\"lll\")}\n          <i className=\"text-xs px-2\">({moment(queue.nextSyncAt).fromNow()})</i>\n        </td>\n        <td className=\"px-6 py-4\">{moment(queue.createdAt).format(\"lll\")}</td>\n        <td className=\"px-6 py-4 flex items-center gap-x-6\">\n          <button\n            onClick={handleDelete}\n            className=\"border-none font-medium px-2 py-1 rounded-lg text-theme-text-primary hover:text-red-500\"\n          >\n            <Trash className=\"h-5 w-5\" />\n          </button>\n        </td>\n      </tr>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/ExperimentalFeatures/Features/LiveSync/manage/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport Sidebar from \"@/components/Sidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport System from \"@/models/system\";\nimport DocumentSyncQueueRow from \"./DocumentSyncQueueRow\";\n\nexport default function LiveDocumentSyncManager() {\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2\">\n            <div className=\"items-center flex gap-x-4\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                Watched documents\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary\">\n              These are all the documents that are currently being watched in\n              your instance. The content of these documents will be periodically\n              synced.\n            </p>\n          </div>\n          <div className=\"overflow-x-auto\">\n            <WatchedDocumentsContainer />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction WatchedDocumentsContainer() {\n  const [loading, setLoading] = useState(true);\n  const [queues, setQueues] = useState([]);\n\n  useEffect(() => {\n    async function fetchData() {\n      const _queues = await System.experimentalFeatures.liveSync.queues();\n      setQueues(_queues);\n      setLoading(false);\n    }\n    fetchData();\n  }, []);\n\n  if (loading) {\n    return (\n      <Skeleton.default\n        height=\"80vh\"\n        width=\"100%\"\n        highlightColor=\"var(--theme-bg-primary)\"\n        baseColor=\"var(--theme-bg-secondary)\"\n        count={1}\n        className=\"w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6\"\n        containerClassName=\"flex w-full\"\n      />\n    );\n  }\n\n  return (\n    <table className=\"w-full text-sm text-left rounded-lg mt-6 min-w-[640px]\">\n      <thead className=\"text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b\">\n        <tr>\n          <th scope=\"col\" className=\"px-6 py-3 rounded-tl-lg\">\n            Document Name\n          </th>\n          <th scope=\"col\" className=\"px-6 py-3\">\n            Last Synced\n          </th>\n          <th scope=\"col\" className=\"px-6 py-3\">\n            Time until next refresh\n          </th>\n          <th scope=\"col\" className=\"px-6 py-3\">\n            Created On\n          </th>\n          <th scope=\"col\" className=\"px-6 py-3 rounded-tr-lg\">\n            {\" \"}\n          </th>\n        </tr>\n      </thead>\n      <tbody>\n        {queues.map((queue) => (\n          <DocumentSyncQueueRow key={queue.id} queue={queue} />\n        ))}\n      </tbody>\n    </table>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/ExperimentalFeatures/Features/LiveSync/toggle.jsx",
    "content": "import System from \"@/models/system\";\nimport paths from \"@/utils/paths\";\nimport showToast from \"@/utils/toast\";\nimport { ArrowSquareOut } from \"@phosphor-icons/react\";\nimport { useState } from \"react\";\nimport { Link } from \"react-router-dom\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nexport default function LiveSyncToggle({ enabled = false, onToggle }) {\n  const [status, setStatus] = useState(enabled);\n\n  async function toggleFeatureFlag() {\n    const updated =\n      await System.experimentalFeatures.liveSync.toggleFeature(!status);\n    if (!updated) {\n      showToast(\"Failed to update status of feature.\", \"error\", {\n        clear: true,\n      });\n      return false;\n    }\n\n    setStatus(!status);\n    showToast(\n      `Live document content sync has been ${\n        !status ? \"enabled\" : \"disabled\"\n      }.`,\n      \"success\",\n      { clear: true }\n    );\n    onToggle();\n  }\n\n  return (\n    <div className=\"p-4\">\n      <div className=\"flex flex-col gap-y-6 max-w-[500px]\">\n        <div className=\"flex items-center justify-between\">\n          <h2 className=\"text-theme-text-primary text-md font-bold\">\n            Automatic Document Content Sync\n          </h2>\n          <Toggle size=\"lg\" enabled={status} onChange={toggleFeatureFlag} />\n        </div>\n        <div className=\"flex flex-col space-y-4\">\n          <p className=\"text-theme-text-secondary text-sm\">\n            Enable the ability to specify a document to be \"watched\". Watched\n            document's content will be regularly fetched and updated in\n            AnythingLLM.\n          </p>\n          <p className=\"text-theme-text-secondary text-sm\">\n            Watched documents will automatically update in all workspaces they\n            are referenced in at the same time of update.\n          </p>\n          <p className=\"text-theme-text-secondary text-xs italic\">\n            This feature only applies to web-based content, such as websites,\n            Confluence, YouTube, and GitHub files.\n          </p>\n        </div>\n      </div>\n      <div className=\"mt-8\">\n        <ul className=\"space-y-2\">\n          <li>\n            <a\n              href=\"https://docs.anythingllm.com/beta-preview/active-features/live-document-sync\"\n              target=\"_blank\"\n              className=\"text-sm text-blue-400 light:text-blue-500 hover:underline flex items-center gap-x-1\"\n              rel=\"noreferrer\"\n            >\n              <ArrowSquareOut size={14} />\n              <span>Feature Documentation and Warnings</span>\n            </a>\n          </li>\n          <li>\n            <Link\n              to={paths.experimental.liveDocumentSync.manage()}\n              className=\"text-sm text-blue-400 light:text-blue-500 hover:underline\"\n            >\n              Manage Watched Documents &rarr;\n            </Link>\n          </li>\n        </ul>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/ExperimentalFeatures/features.js",
    "content": "import LiveSyncToggle from \"./Features/LiveSync/toggle\";\n\nexport const configurableFeatures = {\n  experimental_live_file_sync: {\n    title: \"Live Document Sync\",\n    component: LiveSyncToggle,\n    key: \"experimental_live_file_sync\",\n  },\n};\n"
  },
  {
    "path": "frontend/src/pages/Admin/ExperimentalFeatures/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport Admin from \"@/models/admin\";\nimport { FullScreenLoader } from \"@/components/Preloader\";\nimport { CaretRight, Flask } from \"@phosphor-icons/react\";\nimport { configurableFeatures } from \"./features\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport paths from \"@/utils/paths\";\nimport showToast from \"@/utils/toast\";\n\nexport default function ExperimentalFeatures() {\n  const [featureFlags, setFeatureFlags] = useState({});\n  const [loading, setLoading] = useState(true);\n  const [selectedFeature, setSelectedFeature] = useState(\n    \"experimental_live_file_sync\"\n  );\n\n  useEffect(() => {\n    async function fetchSettings() {\n      setLoading(true);\n      const { settings } = await Admin.systemPreferencesByFields([\n        \"feature_flags\",\n      ]);\n      setFeatureFlags(settings?.feature_flags ?? {});\n      setLoading(false);\n    }\n    fetchSettings();\n  }, []);\n\n  const refresh = async () => {\n    const { settings } = await Admin.systemPreferencesByFields([\n      \"feature_flags\",\n    ]);\n    setFeatureFlags(settings?.feature_flags ?? {});\n  };\n\n  if (loading) {\n    return (\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] w-full h-full flex justify-center items-center\"\n      >\n        <FullScreenLoader />\n      </div>\n    );\n  }\n\n  return (\n    <FeatureLayout>\n      <div className=\"flex-1 flex gap-x-6 p-4 mt-10\">\n        {/* Feature settings nav */}\n        <div className=\"flex flex-col gap-y-[18px]\">\n          <div className=\"text-white flex items-center gap-x-2\">\n            <Flask size={24} />\n            <p className=\"text-lg font-medium\">Experimental Features</p>\n          </div>\n          {/* Feature list */}\n          <div className=\"bg-theme-bg-secondary text-white rounded-xl min-w-[360px] w-fit\">\n            {Object.values(configurableFeatures).map((feature, index) => {\n              const isFirst = index === 0;\n              const isLast =\n                index === Object.values(configurableFeatures).length - 1;\n              return (\n                <FeatureItem\n                  key={feature.key}\n                  feature={feature}\n                  isSelected={selectedFeature === feature.key}\n                  isActive={featureFlags[feature.key]}\n                  handleClick={setSelectedFeature}\n                  borderClass={[\n                    ...(isFirst ? [\"rounded-t-xl\"] : []),\n                    ...(isLast\n                      ? [\"rounded-b-xl\"]\n                      : [\"border-b border-white/10\"]),\n                  ].join(\" \")}\n                />\n              );\n            })}\n          </div>\n        </div>\n\n        {/* Selected feature setting panel */}\n        <FeatureVerification>\n          <div className=\"flex-[2] flex flex-col gap-y-[18px] mt-10\">\n            <div className=\"bg-theme-bg-secondary text-white rounded-xl flex-1 p-4\">\n              {selectedFeature ? (\n                <SelectedFeatureComponent\n                  feature={configurableFeatures[selectedFeature]}\n                  settings={featureFlags}\n                  refresh={refresh}\n                />\n              ) : (\n                <div className=\"flex flex-col items-center justify-center h-full text-white/60\">\n                  <Flask size={40} />\n                  <p className=\"font-medium\">Select an experimental feature</p>\n                </div>\n              )}\n            </div>\n          </div>\n        </FeatureVerification>\n      </div>\n    </FeatureLayout>\n  );\n}\n\nfunction FeatureLayout({ children }) {\n  return (\n    <div\n      id=\"workspace-feature-settings-container\"\n      className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex md:mt-0 mt-6\"\n    >\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] w-full h-full flex\"\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\nfunction FeatureItem({\n  feature = {},\n  isSelected = false,\n  isActive = false,\n  handleClick = () => {},\n  borderClass = \"border-b border-white/10\",\n}) {\n  return (\n    <div\n      key={feature.key}\n      className={`py-3 px-4 flex items-center justify-between cursor-pointer transition-all duration-300 hover:bg-white/5 ${borderClass} ${\n        isSelected ? \"bg-white/10 light:bg-theme-bg-sidebar\" : \"\"\n      }`}\n      onClick={() => {\n        if (feature?.href) window.location = feature.href;\n        else handleClick?.(feature.key);\n      }}\n    >\n      <div className=\"text-sm font-light\">{feature.title}</div>\n      <div className=\"flex items-center gap-x-2\">\n        {feature.autoEnabled ? (\n          <>\n            <div className=\"text-sm text-theme-text-secondary font-medium\">\n              On\n            </div>\n            <div className=\"w-[14px]\" />\n          </>\n        ) : (\n          <>\n            <div className=\"text-sm text-theme-text-secondary font-medium\">\n              {isActive ? \"On\" : \"Off\"}\n            </div>\n            <CaretRight\n              size={14}\n              weight=\"bold\"\n              className=\"text-theme-text-secondary\"\n            />\n          </>\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction SelectedFeatureComponent({ feature, settings, refresh }) {\n  const Component = feature?.component;\n  return Component ? (\n    <Component\n      enabled={settings[feature.key]}\n      feature={feature.key}\n      onToggle={refresh}\n    />\n  ) : null;\n}\n\nfunction FeatureVerification({ children }) {\n  if (\n    !window.localStorage.getItem(\"anythingllm_tos_experimental_feature_set\")\n  ) {\n    function acceptTos(e) {\n      e.preventDefault();\n\n      window.localStorage.setItem(\n        \"anythingllm_tos_experimental_feature_set\",\n        \"accepted\"\n      );\n      showToast(\n        \"Experimental Feature set enabled. Reloading the page.\",\n        \"success\"\n      );\n      setTimeout(() => {\n        window.location.reload();\n      }, 2_500);\n      return;\n    }\n\n    return (\n      <>\n        <ModalWrapper isOpen={true}>\n          <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden\">\n            <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n              <div className=\"flex items-center gap-2\">\n                <Flask size={24} className=\"text-theme-text-primary\" />\n                <h3 className=\"text-xl font-semibold text-white\">\n                  Terms of use for experimental features\n                </h3>\n              </div>\n            </div>\n            <form onSubmit={acceptTos}>\n              <div className=\"py-7 px-9 space-y-4 flex-col\">\n                <div className=\"w-full text-white text-md flex flex-col gap-y-4\">\n                  <p>\n                    Experimental features of AnythingLLM are features that we\n                    are piloting and are <b>opt-in</b>. We proactively will\n                    condition or warn you on any potential concerns should any\n                    exist prior to approval of any feature.\n                  </p>\n\n                  <div>\n                    <p>\n                      Use of any feature on this page can result in, but not\n                      limited to, the following possibilities.\n                    </p>\n                    <ul className=\"list-disc ml-6 text-sm font-mono mt-2\">\n                      <li>Loss of data.</li>\n                      <li>Change in quality of results.</li>\n                      <li>Increased storage.</li>\n                      <li>Increased resource consumption.</li>\n                      <li>\n                        Increased cost or use of any connected LLM or embedding\n                        provider.\n                      </li>\n                      <li>Potential bugs or issues using AnythingLLM.</li>\n                    </ul>\n                  </div>\n\n                  <div>\n                    <p>\n                      Use of an experimental feature also comes with the\n                      following list of non-exhaustive conditions.\n                    </p>\n                    <ul className=\"list-disc ml-6 text-sm font-mono mt-2\">\n                      <li>Feature may not exist in future updates.</li>\n                      <li>The feature being used is not currently stable.</li>\n                      <li>\n                        The feature may not be available in future versions,\n                        configurations, or subscriptions of AnythingLLM.\n                      </li>\n                      <li>\n                        Your privacy settings <b>will be honored</b> with use of\n                        any beta feature.\n                      </li>\n                      <li>These conditions may change in future updates.</li>\n                    </ul>\n                  </div>\n\n                  <p>\n                    Access to any features requires approval of this modal. If\n                    you would like to read more you can refer to{\" \"}\n                    <a\n                      href=\"https://docs.anythingllm.com/beta-preview/overview\"\n                      className=\"underline text-blue-500\"\n                    >\n                      docs.anythingllm.com\n                    </a>{\" \"}\n                    or email{\" \"}\n                    <a\n                      href=\"mailto:team@mintplexlabs.com\"\n                      className=\"underline text-blue-500\"\n                    >\n                      team@mintplexlabs.com\n                    </a>\n                  </p>\n                </div>\n              </div>\n              <div className=\"flex w-full justify-between items-center p-6 space-x-2 border-t border-theme-modal-border rounded-b\">\n                <a\n                  href={paths.home()}\n                  className=\"transition-all duration-300 bg-transparent text-white hover:bg-red-500/50 light:hover:bg-red-300/50 px-4 py-2 rounded-lg text-sm border border-theme-modal-border\"\n                >\n                  Reject & close\n                </a>\n                <button\n                  type=\"submit\"\n                  className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm border border-theme-modal-border\"\n                >\n                  I understand\n                </button>\n              </div>\n            </form>\n          </div>\n        </ModalWrapper>\n        {children}\n      </>\n    );\n  }\n  return <>{children}</>;\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Invitations/InviteRow/index.jsx",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport { titleCase } from \"text-case\";\nimport Admin from \"@/models/admin\";\nimport { Trash } from \"@phosphor-icons/react\";\n\nexport default function InviteRow({ invite }) {\n  const rowRef = useRef(null);\n  const [status, setStatus] = useState(invite.status);\n  const [copied, setCopied] = useState(false);\n  const handleDelete = async () => {\n    if (\n      !window.confirm(\n        `Are you sure you want to deactivate this invite?\\nAfter you do this it will not longer be useable.\\n\\nThis action is irreversible.`\n      )\n    )\n      return false;\n    if (rowRef?.current) {\n      rowRef.current.children[0].innerText = \"Disabled\";\n    }\n    setStatus(\"disabled\");\n    await Admin.disableInvite(invite.id);\n  };\n  const copyInviteLink = () => {\n    if (!invite) return false;\n    window.navigator.clipboard.writeText(\n      `${window.location.origin}/accept-invite/${invite.code}`\n    );\n    setCopied(true);\n  };\n\n  useEffect(() => {\n    function resetStatus() {\n      if (!copied) return false;\n      setTimeout(() => {\n        setCopied(false);\n      }, 3000);\n    }\n    resetStatus();\n  }, [copied]);\n\n  return (\n    <>\n      <tr\n        ref={rowRef}\n        className=\"bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10\"\n      >\n        <td scope=\"row\" className=\"px-6 whitespace-nowrap\">\n          {titleCase(status)}\n        </td>\n        <td className=\"px-6\">\n          {invite.claimedBy\n            ? invite.claimedBy?.username || \"deleted user\"\n            : \"--\"}\n        </td>\n        <td className=\"px-6\">{invite.createdBy?.username || \"deleted user\"}</td>\n        <td className=\"px-6\">{invite.createdAt}</td>\n        <td className=\"px-6 flex items-center gap-x-6 h-full mt-1\">\n          {status === \"pending\" && (\n            <>\n              <button\n                onClick={copyInviteLink}\n                disabled={copied}\n                className=\"text-xs font-medium text-blue-300 rounded-lg hover:text-blue-400 hover:underline\"\n              >\n                {copied ? \"Copied\" : \"Copy Invite Link\"}\n              </button>\n              <button\n                onClick={handleDelete}\n                className=\"text-xs font-medium text-white/80 light:text-black/80 hover:light:text-red-500 hover:text-red-300 rounded-lg px-2 py-1 hover:bg-white hover:light:bg-red-50 hover:bg-opacity-10\"\n              >\n                <Trash className=\"h-5 w-5\" />\n              </button>\n            </>\n          )}\n        </td>\n      </tr>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { X, Copy, Check } from \"@phosphor-icons/react\";\nimport Admin from \"@/models/admin\";\nimport Workspace from \"@/models/workspace\";\nimport showToast from \"@/utils/toast\";\n\nexport default function NewInviteModal({ closeModal, onSuccess }) {\n  const [invite, setInvite] = useState(null);\n  const [error, setError] = useState(null);\n  const [copied, setCopied] = useState(false);\n  const [workspaces, setWorkspaces] = useState([]);\n  const [selectedWorkspaceIds, setSelectedWorkspaceIds] = useState([]);\n\n  const handleCreate = async (e) => {\n    setError(null);\n    e.preventDefault();\n\n    const { invite: newInvite, error } = await Admin.newInvite({\n      role: null,\n      workspaceIds: selectedWorkspaceIds,\n    });\n    if (!!newInvite) {\n      setInvite(newInvite);\n      onSuccess();\n    }\n    setError(error);\n  };\n\n  const copyInviteLink = () => {\n    if (!invite) return false;\n    window.navigator.clipboard.writeText(\n      `${window.location.origin}/accept-invite/${invite.code}`\n    );\n    setCopied(true);\n    showToast(\"Invite link copied to clipboard\", \"success\", {\n      clear: true,\n    });\n  };\n\n  const handleWorkspaceSelection = (workspaceId) => {\n    if (selectedWorkspaceIds.includes(workspaceId)) {\n      const updated = selectedWorkspaceIds.filter((id) => id !== workspaceId);\n      setSelectedWorkspaceIds(updated);\n      return;\n    }\n    setSelectedWorkspaceIds([...selectedWorkspaceIds, workspaceId]);\n  };\n\n  useEffect(() => {\n    function resetStatus() {\n      if (!copied) return false;\n      setTimeout(() => {\n        setCopied(false);\n      }, 3000);\n    }\n    resetStatus();\n  }, [copied]);\n\n  useEffect(() => {\n    async function fetchWorkspaces() {\n      Workspace.all()\n        .then((workspaces) => setWorkspaces(workspaces))\n        .catch(() => setWorkspaces([]));\n    }\n    fetchWorkspaces();\n  }, []);\n\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n      <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Create new invite\n            </h3>\n          </div>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div className=\"p-6\">\n          <form onSubmit={handleCreate}>\n            <div className=\"space-y-4\">\n              {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n              {invite && (\n                <div className=\"relative\">\n                  <input\n                    type=\"url\"\n                    defaultValue={`${window.location.origin}/accept-invite/${invite.code}`}\n                    disabled={true}\n                    className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg outline-none block w-full p-2.5 pr-10\"\n                  />\n                  <button\n                    type=\"button\"\n                    onClick={copyInviteLink}\n                    disabled={copied}\n                    className=\"absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-md hover:bg-theme-modal-border transition-all duration-300\"\n                  >\n                    {copied ? (\n                      <Check\n                        size={20}\n                        className=\"text-green-400\"\n                        weight=\"bold\"\n                      />\n                    ) : (\n                      <Copy size={20} className=\"text-white\" weight=\"bold\" />\n                    )}\n                  </button>\n                </div>\n              )}\n              <p className=\"text-white text-opacity-60 text-xs md:text-sm\">\n                After creation you will be able to copy the invite and send it\n                to a new user where they can create an account as the{\" \"}\n                <b>default</b> role and automatically be added to workspaces\n                selected.\n              </p>\n            </div>\n\n            {workspaces.length > 0 && !invite && (\n              <div className=\"mt-6\">\n                <div className=\"w-full\">\n                  <div className=\"flex flex-col gap-y-1 mb-2\">\n                    <label\n                      htmlFor=\"workspaces\"\n                      className=\"block text-sm font-medium text-white\"\n                    >\n                      Auto-add invitee to workspaces\n                    </label>\n                    <p className=\"text-white text-opacity-60 text-xs\">\n                      You can optionally automatically assign the user to the\n                      workspaces below by selecting them. By default, the user\n                      will not have any workspaces visible. You can assign\n                      workspaces later post-invite acceptance.\n                    </p>\n                  </div>\n\n                  <div className=\"flex flex-col gap-y-2 mt-2\">\n                    {workspaces.map((workspace) => (\n                      <WorkspaceOption\n                        key={workspace.id}\n                        workspace={workspace}\n                        selected={selectedWorkspaceIds.includes(workspace.id)}\n                        toggleSelection={handleWorkspaceSelection}\n                      />\n                    ))}\n                  </div>\n                </div>\n              </div>\n            )}\n\n            <div className=\"flex justify-end items-center mt-6 pt-6 border-t border-theme-modal-border\">\n              {!invite ? (\n                <>\n                  <button\n                    onClick={closeModal}\n                    type=\"button\"\n                    className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm mr-2\"\n                  >\n                    Cancel\n                  </button>\n                  <button\n                    type=\"submit\"\n                    className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n                  >\n                    Create Invite\n                  </button>\n                </>\n              ) : (\n                <button\n                  onClick={closeModal}\n                  type=\"button\"\n                  className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n                >\n                  Close\n                </button>\n              )}\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction WorkspaceOption({ workspace, selected, toggleSelection }) {\n  return (\n    <button\n      type=\"button\"\n      onClick={() => toggleSelection(workspace.id)}\n      className={`transition-all duration-300 w-full h-11 p-2.5 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border ${\n        selected\n          ? \"border-theme-sidebar-item-workspace-active bg-theme-bg-secondary\"\n          : \"border-theme-sidebar-border\"\n      } hover:border-theme-sidebar-border hover:bg-theme-bg-secondary`}\n    >\n      <input\n        type=\"radio\"\n        name=\"workspace\"\n        value={workspace.id}\n        checked={selected}\n        className=\"hidden\"\n      />\n      <div\n        className={`w-4 h-4 rounded-full border-2 border-theme-sidebar-border mr-2 ${\n          selected ? \"bg-[var(--theme-sidebar-item-workspace-active)]\" : \"\"\n        }`}\n      ></div>\n      <div className=\"text-theme-text-primary text-sm font-medium font-['Plus Jakarta Sans'] leading-tight\">\n        {workspace.name}\n      </div>\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Invitations/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport { EnvelopeSimple } from \"@phosphor-icons/react\";\nimport Admin from \"@/models/admin\";\nimport InviteRow from \"./InviteRow\";\nimport NewInviteModal from \"./NewInviteModal\";\nimport { useModal } from \"@/hooks/useModal\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport CTAButton from \"@/components/lib/CTAButton\";\n\nexport default function AdminInvites() {\n  const { isOpen, openModal, closeModal } = useModal();\n  const [loading, setLoading] = useState(true);\n  const [invites, setInvites] = useState([]);\n\n  const fetchInvites = async () => {\n    const _invites = await Admin.invites();\n    setInvites(_invites);\n    setLoading(false);\n  };\n\n  useEffect(() => {\n    fetchInvites();\n  }, []);\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2\">\n            <div className=\"items-center flex gap-x-4\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                Invitations\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary mt-2\">\n              Create invitation links for people in your organization to accept\n              and sign up with. Invitations can only be used by a single user.\n            </p>\n          </div>\n          <div className=\"w-full justify-end flex\">\n            <CTAButton\n              onClick={openModal}\n              className=\"mt-3 mr-0 mb-4 md:-mb-12 z-10\"\n            >\n              <EnvelopeSimple className=\"h-4 w-4\" weight=\"bold\" /> Create Invite\n              Link\n            </CTAButton>\n          </div>\n          <div className=\"overflow-x-auto mt-6\">\n            {loading ? (\n              <Skeleton.default\n                height=\"80vh\"\n                width=\"100%\"\n                highlightColor=\"var(--theme-bg-primary)\"\n                baseColor=\"var(--theme-bg-secondary)\"\n                count={1}\n                className=\"w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm\"\n                containerClassName=\"flex w-full\"\n              />\n            ) : (\n              <table className=\"w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0\">\n                <thead className=\"text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b\">\n                  <tr>\n                    <th scope=\"col\" className=\"px-6 py-3 rounded-tl-lg\">\n                      Status\n                    </th>\n                    <th scope=\"col\" className=\"px-6 py-3\">\n                      Accepted By\n                    </th>\n                    <th scope=\"col\" className=\"px-6 py-3\">\n                      Created By\n                    </th>\n                    <th scope=\"col\" className=\"px-6 py-3\">\n                      Created\n                    </th>\n                    <th scope=\"col\" className=\"px-6 py-3 rounded-tr-lg\">\n                      {\" \"}\n                    </th>\n                  </tr>\n                </thead>\n                <tbody>\n                  {invites.length === 0 ? (\n                    <tr className=\"bg-transparent text-theme-text-secondary text-sm font-medium\">\n                      <td colSpan=\"5\" className=\"px-6 py-4 text-center\">\n                        No invitations found\n                      </td>\n                    </tr>\n                  ) : (\n                    invites.map((invite) => (\n                      <InviteRow key={invite.id} invite={invite} />\n                    ))\n                  )}\n                </tbody>\n              </table>\n            )}\n          </div>\n        </div>\n        <ModalWrapper isOpen={isOpen}>\n          <NewInviteModal closeModal={closeModal} onSuccess={fetchInvites} />\n        </ModalWrapper>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Logging/LogRow/index.jsx",
    "content": "import { CaretDown, CaretUp } from \"@phosphor-icons/react\";\nimport { useEffect, useState } from \"react\";\nimport { safeJsonParse } from \"@/utils/request\";\n\nexport default function LogRow({ log }) {\n  const [expanded, setExpanded] = useState(false);\n  const [metadata, setMetadata] = useState(null);\n  const [hasMetadata, setHasMetadata] = useState(false);\n\n  useEffect(() => {\n    function parseAndSetMetadata() {\n      const data = safeJsonParse(log.metadata, {});\n      setHasMetadata(Object.keys(data)?.length > 0);\n      setMetadata(data);\n    }\n    parseAndSetMetadata();\n  }, [log.metadata]);\n\n  const handleRowClick = () => {\n    if (log.metadata !== \"{}\") {\n      setExpanded(!expanded);\n    }\n  };\n\n  return (\n    <>\n      <tr\n        onClick={handleRowClick}\n        className={`bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10 ${\n          hasMetadata ? \"cursor-pointer hover:bg-white/5\" : \"\"\n        }`}\n      >\n        <EventBadge event={log.event} />\n        <td className=\"px-6 border-transparent transform transition-transform duration-200\">\n          {log.user.username}\n        </td>\n        <td className=\"px-6 border-transparent transform transition-transform duration-200\">\n          {log.occurredAt}\n        </td>\n        {hasMetadata && (\n          <div className=\"mt-1\">\n            {expanded ? (\n              <td\n                className={`px-2 gap-x-1 flex items-center justify-center transform transition-transform duration-200`}\n              >\n                <CaretUp weight=\"bold\" size={20} />\n                <p className=\"text-xs text-white/50 w-[20px]\">hide</p>\n              </td>\n            ) : (\n              <td\n                className={`px-2 gap-x-1 flex items-center justify-center transform transition-transform duration-200`}\n              >\n                <CaretDown weight=\"bold\" size={20} />\n                <p className=\"text-xs text-white/50 w-[20px]\">show</p>\n              </td>\n            )}\n          </div>\n        )}\n      </tr>\n      <EventMetadata metadata={metadata} expanded={expanded} />\n    </>\n  );\n}\n\nconst EventMetadata = ({ metadata, expanded = false }) => {\n  if (!metadata || !expanded) return null;\n  return (\n    <tr className=\"bg-theme-bg-primary\">\n      <td\n        colSpan=\"2\"\n        className=\"px-6 py-4 font-medium text-theme-text-primary rounded-l-2xl\"\n      >\n        Event Metadata\n      </td>\n      <td colSpan=\"4\" className=\"px-6 py-4 rounded-r-2xl\">\n        <div className=\"w-full rounded-lg bg-theme-bg-secondary p-2 text-white shadow-sm border-white/10 border bg-opacity-10\">\n          <pre className=\"overflow-scroll\">\n            {JSON.stringify(metadata, null, 2)}\n          </pre>\n        </div>\n      </td>\n    </tr>\n  );\n};\n\nconst EventBadge = ({ event }) => {\n  let colorTheme = {\n    bg: \"bg-sky-600/20\",\n    text: \"text-sky-400 light:text-sky-800\",\n  };\n  if (event.includes(\"update\"))\n    colorTheme = {\n      bg: \"bg-yellow-600/20\",\n      text: \"text-yellow-400 light:text-yellow-800\",\n    };\n  if (event.includes(\"failed_\") || event.includes(\"deleted\"))\n    colorTheme = {\n      bg: \"bg-red-600/20\",\n      text: \"text-red-400 light:text-red-800\",\n    };\n  if (event === \"login_event\")\n    colorTheme = {\n      bg: \"bg-green-600/20\",\n      text: \"text-green-400 light:text-green-800\",\n    };\n\n  return (\n    <td className=\"px-6 py-2 font-medium whitespace-nowrap text-white flex items-center\">\n      <span\n        className={`rounded-full ${colorTheme.bg} px-2 py-0.5 text-xs font-medium ${colorTheme.text} shadow-sm`}\n      >\n        {event}\n      </span>\n    </td>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/Admin/Logging/index.jsx",
    "content": "import Sidebar from \"@/components/SettingsSidebar\";\nimport useQuery from \"@/hooks/useQuery\";\nimport System from \"@/models/system\";\nimport { useEffect, useState } from \"react\";\nimport { isMobile } from \"react-device-detect\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport LogRow from \"./LogRow\";\nimport showToast from \"@/utils/toast\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function AdminLogs() {\n  const query = useQuery();\n  const [loading, setLoading] = useState(true);\n  const [logs, setLogs] = useState([]);\n  const [offset, setOffset] = useState(Number(query.get(\"offset\") || 0));\n  const [canNext, setCanNext] = useState(false);\n  const { t } = useTranslation();\n\n  useEffect(() => {\n    async function fetchLogs() {\n      const { logs: _logs, hasPages = false } = await System.eventLogs(offset);\n      setLogs(_logs);\n      setCanNext(hasPages);\n      setLoading(false);\n    }\n    fetchLogs();\n  }, [offset]);\n\n  const handleResetLogs = async () => {\n    if (\n      !window.confirm(\n        \"Are you sure you want to clear all event logs? This action is irreversible.\"\n      )\n    )\n      return;\n    const { success, error } = await System.clearEventLogs();\n    if (success) {\n      showToast(\"Event logs cleared successfully.\", \"success\");\n      setLogs([]);\n      setCanNext(false);\n      setOffset(0);\n    } else {\n      showToast(`Failed to clear logs: ${error}`, \"error\");\n    }\n  };\n\n  const handlePrevious = () => {\n    setOffset(Math.max(offset - 1, 0));\n  };\n\n  const handleNext = () => {\n    setOffset(offset + 1);\n  };\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2\">\n            <div className=\"flex gap-x-4 items-center\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                {t(\"event.title\")}\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary mt-2\">\n              {t(\"event.description\")}\n            </p>\n          </div>\n          <div className=\"w-full justify-end flex\">\n            <CTAButton\n              onClick={handleResetLogs}\n              className=\"mt-3 mr-0 mb-4 md:-mb-14 z-10\"\n            >\n              {t(\"event.clear\")}\n            </CTAButton>\n          </div>\n          <div className=\"overflow-x-auto mt-6\">\n            <LogsContainer\n              loading={loading}\n              logs={logs}\n              offset={offset}\n              canNext={canNext}\n              handleNext={handleNext}\n              handlePrevious={handlePrevious}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction LogsContainer({\n  loading,\n  logs,\n  offset,\n  canNext,\n  handleNext,\n  handlePrevious,\n}) {\n  const { t } = useTranslation();\n  if (loading) {\n    return (\n      <Skeleton.default\n        height=\"80vh\"\n        width=\"100%\"\n        highlightColor=\"var(--theme-bg-primary)\"\n        baseColor=\"var(--theme-bg-secondary)\"\n        count={1}\n        className=\"w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm\"\n        containerClassName=\"flex w-full\"\n      />\n    );\n  }\n\n  return (\n    <>\n      <table className=\"w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0\">\n        <thead className=\"text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b\">\n          <tr>\n            <th scope=\"col\" className=\"px-6 py-3 rounded-tl-lg\">\n              {t(\"event.table.type\")}\n            </th>\n            <th scope=\"col\" className=\"px-6 py-3\">\n              {t(\"event.table.user\")}\n            </th>\n            <th scope=\"col\" className=\"px-6 py-3\">\n              {t(\"event.table.occurred\")}\n            </th>\n            <th scope=\"col\" className=\"px-6 py-3 rounded-tr-lg\">\n              {\" \"}\n            </th>\n          </tr>\n        </thead>\n        <tbody>\n          {!!logs && logs.map((log) => <LogRow key={log.id} log={log} />)}\n        </tbody>\n      </table>\n      <div className=\"flex w-full justify-between items-center mt-6\">\n        <button\n          onClick={handlePrevious}\n          className=\"px-4 py-2 rounded-lg border border-slate-200 text-slate-200 light:text-theme-text-secondary light:border-theme-sidebar-border text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible\"\n          disabled={offset === 0}\n        >\n          {t(\"common.previous\")}\n        </button>\n        <button\n          onClick={handleNext}\n          className=\"px-4 py-2 rounded-lg border border-slate-200 text-slate-200 light:text-theme-text-secondary light:border-theme-sidebar-border text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible\"\n          disabled={!canNext}\n        >\n          {t(\"common.next\")}\n        </button>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/SystemPromptVariables/AddVariableModal/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\n\nexport default function AddVariableModal({ closeModal, onRefresh }) {\n  const [error, setError] = useState(null);\n\n  const handleCreate = async (e) => {\n    e.preventDefault();\n    setError(null);\n    const formData = new FormData(e.target);\n    const newVariable = {};\n    for (const [key, value] of formData.entries())\n      newVariable[key] = value.trim();\n\n    if (!newVariable.key || !newVariable.value) {\n      setError(\"Key and value are required\");\n      return;\n    }\n\n    try {\n      await System.promptVariables.create(newVariable);\n      showToast(\"Variable created successfully\", \"success\", { clear: true });\n      if (onRefresh) onRefresh();\n      closeModal();\n    } catch (error) {\n      console.error(\"Error creating variable:\", error);\n      setError(\"Failed to create variable\");\n    }\n  };\n\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n      <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Add New Variable\n            </h3>\n          </div>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div className=\"p-6\">\n          <form onSubmit={handleCreate}>\n            <div className=\"space-y-4\">\n              <div>\n                <label\n                  htmlFor=\"key\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Key\n                </label>\n                <input\n                  name=\"key\"\n                  type=\"text\"\n                  minLength={3}\n                  maxLength={255}\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"e.g., company_name\"\n                  required={true}\n                  autoComplete=\"off\"\n                  pattern=\"^[a-zA-Z0-9_]+$\"\n                />\n                <p className=\"mt-2 text-xs text-white/60\">\n                  Key must be unique and will be used in prompts as {\"{key}\"}.\n                  Only letters, numbers and underscores are allowed.\n                </p>\n              </div>\n              <div>\n                <label\n                  htmlFor=\"value\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Value\n                </label>\n                <input\n                  name=\"value\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"e.g., Acme Corp\"\n                  required={true}\n                  autoComplete=\"off\"\n                />\n              </div>\n              <div>\n                <label\n                  htmlFor=\"description\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Description\n                </label>\n                <input\n                  name=\"description\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"Optional description\"\n                  autoComplete=\"off\"\n                />\n              </div>\n              {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n            </div>\n            <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border\">\n              <button\n                onClick={closeModal}\n                type=\"button\"\n                className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n              >\n                Cancel\n              </button>\n              <button\n                type=\"submit\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                Create variable\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/SystemPromptVariables/VariableRow/EditVariableModal/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\n\nexport default function EditVariableModal({ variable, closeModal, onRefresh }) {\n  const [error, setError] = useState(null);\n\n  const handleUpdate = async (e) => {\n    if (!variable.id) return;\n    e.preventDefault();\n    setError(null);\n    const formData = new FormData(e.target);\n    const updatedVariable = {};\n    for (const [key, value] of formData.entries())\n      updatedVariable[key] = value.trim();\n\n    if (!updatedVariable.key || !updatedVariable.value) {\n      setError(\"Key and value are required\");\n      return;\n    }\n\n    try {\n      await System.promptVariables.update(variable.id, updatedVariable);\n      showToast(\"Variable updated successfully\", \"success\", { clear: true });\n      if (onRefresh) onRefresh();\n      closeModal();\n    } catch (error) {\n      console.error(\"Error updating variable:\", error);\n      setError(\"Failed to update variable\");\n    }\n  };\n\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n      <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Edit {variable.key}\n            </h3>\n          </div>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div className=\"p-6\">\n          <form onSubmit={handleUpdate}>\n            <div className=\"space-y-4\">\n              <div>\n                <label\n                  htmlFor=\"key\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Key\n                </label>\n                <input\n                  name=\"key\"\n                  minLength={3}\n                  maxLength={255}\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"e.g., company_name\"\n                  defaultValue={variable.key}\n                  required={true}\n                  autoComplete=\"off\"\n                  pattern=\"^[a-zA-Z0-9_]+$\"\n                />\n                <p className=\"mt-2 text-xs text-white/60\">\n                  Key must be unique and will be used in prompts as {\"{key}\"}.\n                  Only letters, numbers and underscores are allowed.\n                </p>\n              </div>\n              <div>\n                <label\n                  htmlFor=\"value\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Value\n                </label>\n                <input\n                  name=\"value\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"e.g., Acme Corp\"\n                  defaultValue={variable.value}\n                  required={true}\n                  autoComplete=\"off\"\n                />\n              </div>\n              <div>\n                <label\n                  htmlFor=\"description\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Description\n                </label>\n                <input\n                  name=\"description\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"Optional description\"\n                  defaultValue={variable.description}\n                  autoComplete=\"off\"\n                />\n              </div>\n              {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n            </div>\n            <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border\">\n              <button\n                onClick={closeModal}\n                type=\"button\"\n                className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n              >\n                Cancel\n              </button>\n              <button\n                type=\"submit\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                Update variable\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/SystemPromptVariables/VariableRow/index.jsx",
    "content": "import { useRef } from \"react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { useModal } from \"@/hooks/useModal\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport EditVariableModal from \"./EditVariableModal\";\nimport { titleCase } from \"text-case\";\nimport truncate from \"truncate\";\nimport { Trash } from \"@phosphor-icons/react\";\n\n/**\n * A row component for displaying a system prompt variable\n * @param {{id: number|null, key: string, value: string, description: string, type: string}} variable - The system prompt variable to display\n * @param {Function} onRefresh - A function to call when the variable is refreshed\n * @returns {JSX.Element} A JSX element for displaying the variable\n */\nexport default function VariableRow({ variable, onRefresh }) {\n  const rowRef = useRef(null);\n  const { isOpen, openModal, closeModal } = useModal();\n\n  const handleDelete = async () => {\n    if (!variable.id) return;\n    if (\n      !window.confirm(\n        `Are you sure you want to delete the variable \"${variable.key}\"?\\nThis action is irreversible.`\n      )\n    )\n      return false;\n\n    try {\n      await System.promptVariables.delete(variable.id);\n      rowRef?.current?.remove();\n      showToast(\"Variable deleted successfully\", \"success\", { clear: true });\n      if (onRefresh) onRefresh();\n    } catch (error) {\n      console.error(\"Error deleting variable:\", error);\n      showToast(\"Failed to delete variable\", \"error\", { clear: true });\n    }\n  };\n\n  const getTypeColorTheme = (type) => {\n    switch (type) {\n      case \"system\":\n        return {\n          bg: \"bg-blue-600/20\",\n          text: \"text-blue-400 light:text-blue-800\",\n        };\n      case \"user\":\n        return {\n          bg: \"bg-green-600/20\",\n          text: \"text-green-400 light:text-green-800\",\n        };\n      case \"workspace\":\n        return {\n          bg: \"bg-cyan-600/20\",\n          text: \"text-cyan-400 light:text-cyan-800\",\n        };\n      default:\n        return {\n          bg: \"bg-yellow-600/20\",\n          text: \"text-yellow-400 light:text-yellow-800\",\n        };\n    }\n  };\n\n  const colorTheme = getTypeColorTheme(variable.type);\n\n  return (\n    <>\n      <tr\n        ref={rowRef}\n        className=\"bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10\"\n      >\n        <th scope=\"row\" className=\"px-4 py-2 whitespace-nowrap\">\n          {variable.key}\n        </th>\n        <td className=\"px-4 py-2\">\n          {typeof variable.value === \"function\"\n            ? variable.value()\n            : truncate(variable.value, 50)}\n        </td>\n        <td className=\"px-4 py-2\">\n          {truncate(variable.description || \"-\", 50)}\n        </td>\n        <td className=\"px-4 py-2\">\n          <span\n            className={`rounded-full ${colorTheme.bg} px-2 py-0.5 text-xs leading-5 font-semibold ${colorTheme.text} shadow-sm`}\n          >\n            {titleCase(variable?.type ?? \"static\")}\n          </span>\n        </td>\n        <td className=\"px-4 py-2 flex items-center justify-end gap-x-4\">\n          {variable.type === \"static\" && (\n            <>\n              <button\n                onClick={openModal}\n                className=\"text-xs font-medium text-white/80 light:text-black/80 rounded-lg hover:text-white hover:light:text-gray-500 px-2 py-1 hover:bg-white hover:bg-opacity-10\"\n              >\n                Edit\n              </button>\n              <button\n                onClick={handleDelete}\n                className=\"text-xs font-medium text-white/80 light:text-black/80 hover:light:text-red-500 hover:text-red-300 rounded-lg px-2 py-1 hover:bg-white hover:light:bg-red-50 hover:bg-opacity-10\"\n              >\n                <Trash className=\"h-4 w-4\" />\n              </button>\n            </>\n          )}\n        </td>\n      </tr>\n      <ModalWrapper isOpen={isOpen}>\n        <EditVariableModal\n          variable={variable}\n          closeModal={closeModal}\n          onRefresh={onRefresh}\n        />\n      </ModalWrapper>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/SystemPromptVariables/index.jsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { Plus } from \"@phosphor-icons/react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport VariableRow from \"./VariableRow\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport AddVariableModal from \"./AddVariableModal\";\nimport { useModal } from \"@/hooks/useModal\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\n\nexport default function SystemPromptVariables() {\n  const [variables, setVariables] = useState([]);\n  const [loading, setLoading] = useState(true);\n  const { isOpen, openModal, closeModal } = useModal();\n\n  useEffect(() => {\n    fetchVariables();\n  }, []);\n\n  const fetchVariables = async () => {\n    setLoading(true);\n    try {\n      const { variables } = await System.promptVariables.getAll();\n      setVariables(variables || []);\n    } catch (error) {\n      console.error(\"Error fetching variables:\", error);\n      showToast(\"No variables found\", \"error\");\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2\">\n            <div className=\"items-center flex gap-x-4\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                System Prompt Variables\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary\">\n              System prompt variables are used to store configuration values\n              that can be referenced in your system prompt to enable dynamic\n              content in your prompts.\n            </p>\n          </div>\n\n          <div className=\"w-full justify-end flex\">\n            <CTAButton\n              onClick={openModal}\n              className=\"mt-3 mr-0 mb-4 md:-mb-6 z-10\"\n            >\n              <Plus className=\"h-4 w-4\" weight=\"bold\" /> Add Variable\n            </CTAButton>\n          </div>\n\n          <div className=\"overflow-x-auto\">\n            {loading ? (\n              <Skeleton.default\n                height=\"80vh\"\n                width=\"100%\"\n                highlightColor=\"var(--theme-bg-primary)\"\n                baseColor=\"var(--theme-bg-secondary)\"\n                count={1}\n                className=\"w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-8\"\n                containerClassName=\"flex w-full\"\n              />\n            ) : variables.length === 0 ? (\n              <div className=\"text-center py-4 text-theme-text-secondary\">\n                No variables found\n              </div>\n            ) : (\n              <table className=\"w-full text-sm text-left rounded-lg min-w-[640px] border-spacing-0\">\n                <thead className=\"text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b\">\n                  <tr>\n                    <th scope=\"col\" className=\"px-4 py-2 rounded-tl-lg\">\n                      Key\n                    </th>\n                    <th scope=\"col\" className=\"px-4 py-2\">\n                      Value\n                    </th>\n                    <th scope=\"col\" className=\"px-4 py-2\">\n                      Description\n                    </th>\n                    <th scope=\"col\" className=\"px-4 py-2\">\n                      Type\n                    </th>\n                  </tr>\n                </thead>\n                <tbody>\n                  {variables.map((variable) => (\n                    <VariableRow\n                      key={variable.id}\n                      variable={variable}\n                      onRefresh={fetchVariables}\n                    />\n                  ))}\n                </tbody>\n              </table>\n            )}\n          </div>\n        </div>\n      </div>\n\n      <ModalWrapper isOpen={isOpen}>\n        <AddVariableModal closeModal={closeModal} onRefresh={fetchVariables} />\n      </ModalWrapper>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Users/NewUserModal/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport Admin from \"@/models/admin\";\nimport { userFromStorage } from \"@/utils/request\";\nimport { MessageLimitInput, RoleHintDisplay } from \"..\";\nimport { useTranslation } from \"react-i18next\";\nimport {\n  USERNAME_MIN_LENGTH,\n  USERNAME_MAX_LENGTH,\n  USERNAME_PATTERN,\n} from \"@/utils/username\";\n\nexport default function NewUserModal({ closeModal }) {\n  const [error, setError] = useState(null);\n  const [role, setRole] = useState(\"default\");\n  const [messageLimit, setMessageLimit] = useState({\n    enabled: false,\n    limit: 10,\n  });\n  const { t } = useTranslation();\n\n  const handleCreate = async (e) => {\n    setError(null);\n    e.preventDefault();\n    const data = {};\n    const form = new FormData(e.target);\n    for (var [key, value] of form.entries()) data[key] = value;\n    data.dailyMessageLimit = messageLimit.enabled ? messageLimit.limit : null;\n\n    const { user, error } = await Admin.newUser(data);\n    if (!!user) window.location.reload();\n    setError(error);\n  };\n\n  const user = userFromStorage();\n\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n      <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Add user to instance\n            </h3>\n          </div>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div className=\"p-6\">\n          <form onSubmit={handleCreate}>\n            <div className=\"space-y-4\">\n              <div>\n                <label\n                  htmlFor=\"username\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Username\n                </label>\n                <input\n                  name=\"username\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"User's username\"\n                  minLength={USERNAME_MIN_LENGTH}\n                  maxLength={USERNAME_MAX_LENGTH}\n                  pattern={USERNAME_PATTERN}\n                  required={true}\n                  autoComplete=\"off\"\n                />\n                <p className=\"mt-2 text-xs text-white/60\">\n                  {t(\"common.username_requirements\")}\n                </p>\n              </div>\n              <div>\n                <label\n                  htmlFor=\"password\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Password\n                </label>\n                <input\n                  name=\"password\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"User's initial password\"\n                  required={true}\n                  autoComplete=\"off\"\n                  minLength={8}\n                />\n                <p className=\"mt-2 text-xs text-white/60\">\n                  Password must be at least 8 characters long\n                </p>\n              </div>\n              <div>\n                <label\n                  htmlFor=\"bio\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Bio\n                </label>\n                <textarea\n                  name=\"bio\"\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"User's bio\"\n                  autoComplete=\"off\"\n                  rows={3}\n                />\n              </div>\n              <div>\n                <label\n                  htmlFor=\"role\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Role\n                </label>\n                <select\n                  name=\"role\"\n                  required={true}\n                  defaultValue={\"default\"}\n                  onChange={(e) => setRole(e.target.value)}\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                >\n                  <option value=\"default\">Default</option>\n                  <option value=\"manager\">Manager</option>\n                  {user?.role === \"admin\" && (\n                    <option value=\"admin\">Administrator</option>\n                  )}\n                </select>\n                <RoleHintDisplay role={role} />\n              </div>\n              <MessageLimitInput\n                role={role}\n                enabled={messageLimit.enabled}\n                limit={messageLimit.limit}\n                updateState={setMessageLimit}\n              />\n              {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n              <p className=\"text-white text-xs md:text-sm\">\n                After creating a user they will need to login with their initial\n                login to get access.\n              </p>\n            </div>\n            <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border\">\n              <button\n                onClick={closeModal}\n                type=\"button\"\n                className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n              >\n                Cancel\n              </button>\n              <button\n                type=\"submit\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                Add user\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport Admin from \"@/models/admin\";\nimport { MessageLimitInput, RoleHintDisplay } from \"../..\";\nimport { AUTH_USER } from \"@/utils/constants\";\nimport { useTranslation } from \"react-i18next\";\nimport {\n  USERNAME_MIN_LENGTH,\n  USERNAME_MAX_LENGTH,\n  USERNAME_PATTERN,\n} from \"@/utils/username\";\n\nexport default function EditUserModal({ currentUser, user, closeModal }) {\n  const [role, setRole] = useState(user.role);\n  const [error, setError] = useState(null);\n  const [messageLimit, setMessageLimit] = useState({\n    enabled: user.dailyMessageLimit !== null,\n    limit: user.dailyMessageLimit || 10,\n  });\n  const { t } = useTranslation();\n\n  const handleUpdate = async (e) => {\n    setError(null);\n    e.preventDefault();\n    const data = {};\n    const form = new FormData(e.target);\n    for (var [key, value] of form.entries()) {\n      if (!value || value === null) continue;\n      data[key] = value;\n    }\n    if (messageLimit.enabled) {\n      data.dailyMessageLimit = messageLimit.limit;\n    } else {\n      data.dailyMessageLimit = null;\n    }\n\n    const { success, error } = await Admin.updateUser(user.id, data);\n    if (success) {\n      // Update local storage if we're editing our own user\n      if (currentUser && currentUser.id === user.id) {\n        currentUser.username = data.username;\n        currentUser.bio = data.bio;\n        currentUser.role = data.role;\n        localStorage.setItem(AUTH_USER, JSON.stringify(currentUser));\n      }\n\n      window.location.reload();\n    }\n    setError(error);\n  };\n\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n      <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Edit {user.username}\n            </h3>\n          </div>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div className=\"p-6\">\n          <form onSubmit={handleUpdate}>\n            <div className=\"space-y-4\">\n              <div>\n                <label\n                  htmlFor=\"username\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Username\n                </label>\n                <input\n                  name=\"username\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"User's username\"\n                  defaultValue={user.username}\n                  minLength={USERNAME_MIN_LENGTH}\n                  maxLength={USERNAME_MAX_LENGTH}\n                  pattern={USERNAME_PATTERN}\n                  required={true}\n                  autoComplete=\"off\"\n                />\n                <p className=\"mt-2 text-xs text-white/60\">\n                  {t(\"common.username_requirements\")}\n                </p>\n              </div>\n              <div>\n                <label\n                  htmlFor=\"password\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  New Password\n                </label>\n                <input\n                  name=\"password\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder={`${user.username}'s new password`}\n                  autoComplete=\"off\"\n                  minLength={8}\n                />\n                <p className=\"mt-2 text-xs text-white/60\">\n                  Password must be at least 8 characters long\n                </p>\n              </div>\n              <div>\n                <label\n                  htmlFor=\"bio\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Bio\n                </label>\n                <textarea\n                  name=\"bio\"\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"User's bio\"\n                  defaultValue={user.bio}\n                  autoComplete=\"off\"\n                  rows={3}\n                />\n              </div>\n              <div>\n                <label\n                  htmlFor=\"role\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  Role\n                </label>\n                <select\n                  name=\"role\"\n                  required={true}\n                  defaultValue={user.role}\n                  onChange={(e) => setRole(e.target.value)}\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                >\n                  <option value=\"default\">Default</option>\n                  <option value=\"manager\">Manager</option>\n                  {currentUser?.role === \"admin\" && (\n                    <option value=\"admin\">Administrator</option>\n                  )}\n                </select>\n                <RoleHintDisplay role={role} />\n              </div>\n              <MessageLimitInput\n                role={role}\n                enabled={messageLimit.enabled}\n                limit={messageLimit.limit}\n                updateState={setMessageLimit}\n              />\n              {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n            </div>\n            <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border\">\n              <button\n                onClick={closeModal}\n                type=\"button\"\n                className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n              >\n                Cancel\n              </button>\n              <button\n                type=\"submit\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                Update user\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Users/UserRow/index.jsx",
    "content": "import { useRef, useState } from \"react\";\nimport { titleCase } from \"text-case\";\nimport Admin from \"@/models/admin\";\nimport EditUserModal from \"./EditUserModal\";\nimport showToast from \"@/utils/toast\";\nimport { useModal } from \"@/hooks/useModal\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\n\nconst ModMap = {\n  admin: [\"admin\", \"manager\", \"default\"],\n  manager: [\"manager\", \"default\"],\n  default: [],\n};\n\nexport default function UserRow({ currUser, user }) {\n  const rowRef = useRef(null);\n  const canModify = ModMap[currUser?.role || \"default\"].includes(user.role);\n  const [suspended, setSuspended] = useState(user.suspended === 1);\n  const { isOpen, openModal, closeModal } = useModal();\n  const handleSuspend = async () => {\n    if (\n      !window.confirm(\n        `Are you sure you want to suspend ${user.username}?\\nAfter you do this they will be logged out and unable to log back into this instance of AnythingLLM until unsuspended by an admin.`\n      )\n    )\n      return false;\n\n    const { success, error } = await Admin.updateUser(user.id, {\n      suspended: suspended ? 0 : 1,\n    });\n    if (!success) showToast(error, \"error\", { clear: true });\n    if (success) {\n      showToast(\n        `User ${!suspended ? \"has been suspended\" : \"is no longer suspended\"}.`,\n        \"success\",\n        { clear: true }\n      );\n      setSuspended(!suspended);\n    }\n  };\n  const handleDelete = async () => {\n    if (\n      !window.confirm(\n        `Are you sure you want to delete ${user.username}?\\nAfter you do this they will be logged out and unable to use this instance of AnythingLLM.\\n\\nThis action is irreversible.`\n      )\n    )\n      return false;\n    const { success, error } = await Admin.deleteUser(user.id);\n    if (!success) showToast(error, \"error\", { clear: true });\n    if (success) {\n      rowRef?.current?.remove();\n      showToast(\"User deleted from system.\", \"success\", { clear: true });\n    }\n  };\n\n  return (\n    <>\n      <tr\n        ref={rowRef}\n        className=\"bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10\"\n      >\n        <th scope=\"row\" className=\"px-6 whitespace-nowrap\">\n          {user.username}\n        </th>\n        <td className=\"px-6\">{titleCase(user.role)}</td>\n        <td className=\"px-6\">{user.createdAt}</td>\n        <td className=\"px-6 flex items-center gap-x-6 h-full mt-2\">\n          {canModify && (\n            <button\n              onClick={openModal}\n              className=\"text-xs font-medium text-white/80 light:text-black/80 rounded-lg hover:text-white hover:light:text-gray-500 px-2 py-1 hover:bg-white hover:bg-opacity-10\"\n            >\n              Edit\n            </button>\n          )}\n          {currUser?.id !== user.id && canModify && (\n            <>\n              <button\n                onClick={handleSuspend}\n                className=\"text-xs font-medium text-white/80 light:text-black/80 hover:light:text-orange-500 hover:text-orange-300 rounded-lg px-2 py-1 hover:bg-white hover:light:bg-orange-50 hover:bg-opacity-10\"\n              >\n                {suspended ? \"Unsuspend\" : \"Suspend\"}\n              </button>\n              <button\n                onClick={handleDelete}\n                className=\"text-xs font-medium text-white/80 light:text-black/80 hover:light:text-red-500 hover:text-red-300 rounded-lg px-2 py-1 hover:bg-white hover:light:bg-red-50 hover:bg-opacity-10\"\n              >\n                Delete\n              </button>\n            </>\n          )}\n        </td>\n      </tr>\n      <ModalWrapper isOpen={isOpen}>\n        <EditUserModal\n          currentUser={currUser}\n          user={user}\n          closeModal={closeModal}\n        />\n      </ModalWrapper>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Users/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport { UserPlus } from \"@phosphor-icons/react\";\nimport Admin from \"@/models/admin\";\nimport UserRow from \"./UserRow\";\nimport useUser from \"@/hooks/useUser\";\nimport NewUserModal from \"./NewUserModal\";\nimport { useModal } from \"@/hooks/useModal\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nexport default function AdminUsers() {\n  const { isOpen, openModal, closeModal } = useModal();\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2\">\n            <div className=\"items-center flex gap-x-4\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                Users\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary\">\n              These are all the accounts which have an account on this instance.\n              Removing an account will instantly remove their access to this\n              instance.\n            </p>\n          </div>\n          <div className=\"w-full justify-end flex\">\n            <CTAButton\n              onClick={openModal}\n              className=\"mt-3 mr-0 mb-4 md:-mb-6 z-10\"\n            >\n              <UserPlus className=\"h-4 w-4\" weight=\"bold\" /> Add user\n            </CTAButton>\n          </div>\n          <div className=\"overflow-x-auto\">\n            <UsersContainer />\n          </div>\n        </div>\n        <ModalWrapper isOpen={isOpen}>\n          <NewUserModal closeModal={closeModal} />\n        </ModalWrapper>\n      </div>\n    </div>\n  );\n}\n\nfunction UsersContainer() {\n  const { user: currUser } = useUser();\n  const [loading, setLoading] = useState(true);\n  const [users, setUsers] = useState([]);\n\n  useEffect(() => {\n    async function fetchUsers() {\n      const _users = await Admin.users();\n      setUsers(_users);\n      setLoading(false);\n    }\n    fetchUsers();\n  }, []);\n\n  if (loading) {\n    return (\n      <Skeleton.default\n        height=\"80vh\"\n        width=\"100%\"\n        highlightColor=\"var(--theme-bg-primary)\"\n        baseColor=\"var(--theme-bg-secondary)\"\n        count={1}\n        className=\"w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-8\"\n        containerClassName=\"flex w-full\"\n      />\n    );\n  }\n\n  return (\n    <table className=\"w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0\">\n      <thead className=\"text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b\">\n        <tr>\n          <th scope=\"col\" className=\"px-6 py-3 rounded-tl-lg\">\n            Username\n          </th>\n          <th scope=\"col\" className=\"px-6 py-3\">\n            Role\n          </th>\n          <th scope=\"col\" className=\"px-6 py-3\">\n            Date Added\n          </th>\n          <th scope=\"col\" className=\"px-6 py-3 rounded-tr-lg\">\n            {\" \"}\n          </th>\n        </tr>\n      </thead>\n      <tbody>\n        {users.map((user) => (\n          <UserRow key={user.id} currUser={currUser} user={user} />\n        ))}\n      </tbody>\n    </table>\n  );\n}\n\nconst ROLE_HINT = {\n  default: [\n    \"Can only send chats with workspaces they are added to by admin or managers.\",\n    \"Cannot modify any settings at all.\",\n  ],\n  manager: [\n    \"Can view, create, and delete any workspaces and modify workspace-specific settings.\",\n    \"Can create, update and invite new users to the instance.\",\n    \"Cannot modify LLM, vectorDB, embedding, or other connections.\",\n  ],\n  admin: [\n    \"Highest user level privilege.\",\n    \"Can see and do everything across the system.\",\n  ],\n};\n\nexport function RoleHintDisplay({ role }) {\n  return (\n    <div className=\"flex flex-col gap-y-1 py-1 pb-4\">\n      <p className=\"text-sm font-medium text-theme-text-primary\">Permissions</p>\n      <ul className=\"flex flex-col gap-y-1 list-disc px-4\">\n        {ROLE_HINT[role ?? \"default\"].map((hints, i) => {\n          return (\n            <li key={i} className=\"text-xs text-theme-text-secondary\">\n              {hints}\n            </li>\n          );\n        })}\n      </ul>\n    </div>\n  );\n}\n\nexport function MessageLimitInput({ enabled, limit, updateState, role }) {\n  if (role === \"admin\") return null;\n  return (\n    <div className=\"mt-4 mb-8\">\n      <Toggle\n        size=\"md\"\n        variant=\"horizontal\"\n        label=\"Limit messages per day\"\n        description=\"Restrict this user to a number of successful queries or chats within a 24 hour window.\"\n        enabled={enabled}\n        onChange={(checked) => {\n          updateState((prev) => ({\n            ...prev,\n            enabled: checked,\n          }));\n        }}\n      />\n      {enabled && (\n        <div className=\"mt-4\">\n          <label className=\"text-white text-sm font-semibold block mb-4\">\n            Message limit per day\n          </label>\n          <div className=\"relative mt-2\">\n            <input\n              type=\"number\"\n              onScroll={(e) => e.target.blur()}\n              onChange={(e) => {\n                updateState({\n                  enabled: true,\n                  limit: Number(e?.target?.value || 0),\n                });\n              }}\n              value={limit}\n              min={1}\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n            />\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Workspaces/NewWorkspaceModal/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport Admin from \"@/models/admin\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function NewWorkspaceModal({ closeModal }) {\n  const [error, setError] = useState(null);\n  const { t } = useTranslation();\n  const handleCreate = async (e) => {\n    setError(null);\n    e.preventDefault();\n    const form = new FormData(e.target);\n    const { workspace, error } = await Admin.newWorkspace(form.get(\"name\"));\n    if (!!workspace) window.location.reload();\n    setError(error);\n  };\n\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n      <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Create new workspace\n            </h3>\n          </div>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div className=\"p-6\">\n          <form onSubmit={handleCreate}>\n            <div className=\"space-y-4\">\n              <div>\n                <label\n                  htmlFor=\"name\"\n                  className=\"block mb-2 text-sm font-medium text-white\"\n                >\n                  {t(\"common.workspaces-name\")}\n                </label>\n                <input\n                  name=\"name\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"My workspace\"\n                  minLength={4}\n                  required={true}\n                  autoComplete=\"off\"\n                />\n              </div>\n              {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n              <p className=\"text-white text-opacity-60 text-xs md:text-sm\">\n                After creating this workspace only admins will be able to see\n                it. You can add users after it has been created.\n              </p>\n            </div>\n            <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border\">\n              <button\n                onClick={closeModal}\n                type=\"button\"\n                className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n              >\n                Cancel\n              </button>\n              <button\n                type=\"submit\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                Create workspace\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Workspaces/WorkspaceRow/index.jsx",
    "content": "import { useRef } from \"react\";\nimport Admin from \"@/models/admin\";\nimport paths from \"@/utils/paths\";\nimport { LinkSimple, Trash } from \"@phosphor-icons/react\";\n\nexport default function WorkspaceRow({ workspace, users: _users }) {\n  const rowRef = useRef(null);\n  const handleDelete = async () => {\n    if (\n      !window.confirm(\n        `Are you sure you want to delete ${workspace.name}?\\nAfter you do this it will be unavailable in this instance of AnythingLLM.\\n\\nThis action is irreversible.`\n      )\n    )\n      return false;\n    rowRef?.current?.remove();\n    await Admin.deleteWorkspace(workspace.id);\n  };\n\n  return (\n    <>\n      <tr\n        ref={rowRef}\n        className=\"bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10\"\n      >\n        <th scope=\"row\" className=\"px-6 whitespace-nowrap\">\n          {workspace.name}\n        </th>\n        <td className=\"px-6 flex items-center\">\n          <a\n            href={paths.workspace.chat(workspace.slug)}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"text-white flex items-center hover:underline\"\n          >\n            <LinkSimple className=\"mr-2 w-4 h-4\" /> {workspace.slug}\n          </a>\n        </td>\n        <td className=\"px-6\">\n          <a\n            href={paths.workspace.settings.members(workspace.slug)}\n            className=\"text-white flex items-center underline\"\n          >\n            {workspace.userIds?.length}\n          </a>\n        </td>\n        <td className=\"px-6\">{workspace.createdAt}</td>\n        <td className=\"px-6 flex items-center gap-x-6 h-full mt-1\">\n          <button\n            onClick={handleDelete}\n            className=\"text-xs font-medium text-white/80 light:text-black/80 hover:light:text-red-500 hover:text-red-300 rounded-lg px-2 py-1 hover:bg-white hover:light:bg-red-50 hover:bg-opacity-10\"\n          >\n            <Trash className=\"h-5 w-5\" />\n          </button>\n        </td>\n      </tr>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Admin/Workspaces/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport { BookOpen } from \"@phosphor-icons/react\";\nimport Admin from \"@/models/admin\";\nimport WorkspaceRow from \"./WorkspaceRow\";\nimport NewWorkspaceModal from \"./NewWorkspaceModal\";\nimport { useModal } from \"@/hooks/useModal\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport CTAButton from \"@/components/lib/CTAButton\";\n\nexport default function AdminWorkspaces() {\n  const { isOpen, openModal, closeModal } = useModal();\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2\">\n            <div className=\"items-center flex gap-x-4\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                Instance Workspaces\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary\">\n              These are all the workspaces that exist on this instance. Removing\n              a workspace will delete all of its associated chats and settings.\n            </p>\n          </div>\n          <div className=\"w-full justify-end flex\">\n            <CTAButton\n              onClick={openModal}\n              className=\"mt-3 mr-0 mb-4 md:-mb-14 z-10\"\n            >\n              <BookOpen className=\"h-4 w-4\" weight=\"bold\" /> New Workspace\n            </CTAButton>\n          </div>\n          <div className=\"overflow-x-auto\">\n            <WorkspacesContainer />\n          </div>\n        </div>\n        <ModalWrapper isOpen={isOpen}>\n          <NewWorkspaceModal closeModal={closeModal} />\n        </ModalWrapper>\n      </div>\n    </div>\n  );\n}\n\nfunction WorkspacesContainer() {\n  const [loading, setLoading] = useState(true);\n  const [users, setUsers] = useState([]);\n  const [workspaces, setWorkspaces] = useState([]);\n\n  useEffect(() => {\n    async function fetchData() {\n      const _users = await Admin.users();\n      const _workspaces = await Admin.workspaces();\n      setUsers(_users);\n      setWorkspaces(_workspaces);\n      setLoading(false);\n    }\n    fetchData();\n  }, []);\n\n  if (loading) {\n    return (\n      <Skeleton.default\n        height=\"80vh\"\n        width=\"100%\"\n        highlightColor=\"var(--theme-bg-primary)\"\n        baseColor=\"var(--theme-bg-secondary)\"\n        count={1}\n        className=\"w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6\"\n        containerClassName=\"flex w-full\"\n      />\n    );\n  }\n\n  return (\n    <table className=\"w-full text-xs text-left rounded-lg mt-6 min-w-[640px] border-spacing-0\">\n      <thead className=\"text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b\">\n        <tr>\n          <th scope=\"col\" className=\"px-6 py-3 rounded-tl-lg\">\n            Name\n          </th>\n          <th scope=\"col\" className=\"px-6 py-3\">\n            Link\n          </th>\n          <th scope=\"col\" className=\"px-6 py-3\">\n            Users\n          </th>\n          <th scope=\"col\" className=\"px-6 py-3\">\n            Created On\n          </th>\n          <th scope=\"col\" className=\"px-6 py-3 rounded-tr-lg\">\n            {\" \"}\n          </th>\n        </tr>\n      </thead>\n      <tbody>\n        {workspaces.map((workspace) => (\n          <WorkspaceRow\n            key={workspace.id}\n            workspace={workspace}\n            users={users}\n          />\n        ))}\n      </tbody>\n    </table>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/ApiKeys/ApiKeyRow/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport Admin from \"@/models/admin\";\nimport showToast from \"@/utils/toast\";\nimport { Trash } from \"@phosphor-icons/react\";\nimport { userFromStorage } from \"@/utils/request\";\nimport System from \"@/models/system\";\n\nexport default function ApiKeyRow({ apiKey, removeApiKey }) {\n  const [copied, setCopied] = useState(false);\n  const handleDelete = async () => {\n    if (\n      !window.confirm(\n        `Are you sure you want to deactivate this api key?\\nAfter you do this it will not longer be useable.\\n\\nThis action is irreversible.`\n      )\n    )\n      return false;\n\n    const user = userFromStorage();\n    const Model = !!user ? Admin : System;\n    await Model.deleteApiKey(apiKey.id);\n    showToast(\"API Key permanently deleted\", \"info\");\n    removeApiKey(apiKey.id);\n  };\n\n  const copyApiKey = () => {\n    if (!apiKey) return false;\n    window.navigator.clipboard.writeText(apiKey.secret);\n    showToast(\"API Key copied to clipboard\", \"success\");\n    setCopied(true);\n  };\n\n  useEffect(() => {\n    function resetStatus() {\n      if (!copied) return false;\n      setTimeout(() => {\n        setCopied(false);\n      }, 3000);\n    }\n    resetStatus();\n  }, [copied]);\n\n  return (\n    <>\n      <tr className=\"bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10\">\n        <td scope=\"row\" className=\"px-6 whitespace-nowrap\">\n          {apiKey.secret}\n        </td>\n        <td className=\"px-6 text-left\">{apiKey.createdBy?.username || \"--\"}</td>\n        <td className=\"px-6\">{apiKey.createdAt}</td>\n        <td className=\"px-6 flex items-center gap-x-6 h-full mt-1\">\n          <button\n            onClick={copyApiKey}\n            disabled={copied}\n            className=\"text-xs font-medium text-blue-300 rounded-lg hover:text-white hover:light:text-blue-500 hover:text-opacity-60 hover:underline\"\n          >\n            {copied ? \"Copied\" : \"Copy API Key\"}\n          </button>\n          <button\n            onClick={handleDelete}\n            className=\"text-xs font-medium text-white/80 light:text-black/80 hover:light:text-red-500 hover:text-red-300 rounded-lg px-2 py-1 hover:bg-white hover:light:bg-red-50 hover:bg-opacity-10\"\n          >\n            <Trash className=\"h-5 w-5\" />\n          </button>\n        </td>\n      </tr>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/ApiKeys/NewApiKeyModal/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { X, Copy, Check } from \"@phosphor-icons/react\";\nimport Admin from \"@/models/admin\";\nimport paths from \"@/utils/paths\";\nimport { userFromStorage } from \"@/utils/request\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\n\nexport default function NewApiKeyModal({ closeModal, onSuccess }) {\n  const [apiKey, setApiKey] = useState(null);\n  const [error, setError] = useState(null);\n  const [copied, setCopied] = useState(false);\n\n  const handleCreate = async (e) => {\n    setError(null);\n    e.preventDefault();\n    const user = userFromStorage();\n    const Model = !!user ? Admin : System;\n\n    const { apiKey: newApiKey, error } = await Model.generateApiKey();\n    if (!!newApiKey) {\n      setApiKey(newApiKey);\n      onSuccess();\n    }\n    setError(error);\n  };\n\n  const copyApiKey = () => {\n    if (!apiKey) return false;\n    window.navigator.clipboard.writeText(apiKey.secret);\n    setCopied(true);\n    showToast(\"API key copied to clipboard\", \"success\", {\n      clear: true,\n    });\n  };\n\n  useEffect(() => {\n    function resetStatus() {\n      if (!copied) return false;\n      setTimeout(() => {\n        setCopied(false);\n      }, 3000);\n    }\n    resetStatus();\n  }, [copied]);\n\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n      <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Create new API key\n            </h3>\n          </div>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div className=\"px-7 py-6\">\n          <form onSubmit={handleCreate}>\n            <div className=\"space-y-6 max-h-[60vh] overflow-y-auto pr-2\">\n              {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n              {apiKey && (\n                <div className=\"relative\">\n                  <input\n                    type=\"text\"\n                    defaultValue={`${apiKey.secret}`}\n                    disabled={true}\n                    className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg outline-none block w-full p-2.5 pr-10\"\n                  />\n                  <button\n                    type=\"button\"\n                    onClick={copyApiKey}\n                    disabled={copied}\n                    className=\"absolute right-2 top-1/2 -translate-y-1/2 p-1 rounded-md hover:bg-theme-modal-border transition-all duration-300\"\n                  >\n                    {copied ? (\n                      <Check\n                        size={20}\n                        className=\"text-green-400\"\n                        weight=\"bold\"\n                      />\n                    ) : (\n                      <Copy size={20} className=\"text-white\" weight=\"bold\" />\n                    )}\n                  </button>\n                </div>\n              )}\n              <p className=\"text-white text-opacity-60 text-xs md:text-sm\">\n                Once created the API key can be used to programmatically access\n                and configure this AnythingLLM instance.\n              </p>\n              <a\n                href={paths.apiDocs()}\n                target=\"_blank\"\n                rel=\"noreferrer\"\n                className=\"text-blue-400 hover:underline\"\n              >\n                Read the API documentation &rarr;\n              </a>\n            </div>\n            <div className=\"flex justify-end items-center mt-6 pt-6 border-t border-theme-modal-border\">\n              {!apiKey ? (\n                <>\n                  <button\n                    onClick={closeModal}\n                    type=\"button\"\n                    className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm mr-2\"\n                  >\n                    Cancel\n                  </button>\n                  <button\n                    type=\"submit\"\n                    className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n                  >\n                    Create API Key\n                  </button>\n                </>\n              ) : (\n                <button\n                  onClick={closeModal}\n                  type=\"button\"\n                  className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n                >\n                  Close\n                </button>\n              )}\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/ApiKeys/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport { PlusCircle } from \"@phosphor-icons/react\";\nimport Admin from \"@/models/admin\";\nimport ApiKeyRow from \"./ApiKeyRow\";\nimport NewApiKeyModal from \"./NewApiKeyModal\";\nimport paths from \"@/utils/paths\";\nimport { userFromStorage } from \"@/utils/request\";\nimport System from \"@/models/system\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { useModal } from \"@/hooks/useModal\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function AdminApiKeys() {\n  const { isOpen, openModal, closeModal } = useModal();\n  const { t } = useTranslation();\n  const [loading, setLoading] = useState(true);\n  const [apiKeys, setApiKeys] = useState([]);\n\n  const fetchExistingKeys = async () => {\n    const user = userFromStorage();\n    const Model = !!user ? Admin : System;\n    const { apiKeys: foundKeys } = await Model.getApiKeys();\n    setApiKeys(foundKeys);\n    setLoading(false);\n  };\n\n  useEffect(() => {\n    fetchExistingKeys();\n  }, []);\n\n  const removeApiKey = (id) => {\n    setApiKeys((prevKeys) => prevKeys.filter((apiKey) => apiKey.id !== id));\n  };\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2\">\n            <div className=\"items-center flex gap-x-4\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                {t(\"api.title\")}\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary mt-2\">\n              {t(\"api.description\")}\n            </p>\n            <a\n              href={paths.apiDocs()}\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              className=\"text-xs leading-[18px] font-base text-blue-300 light:text-blue-500 hover:underline mt-1\"\n            >\n              {t(\"api.link\")} &rarr;\n            </a>\n          </div>\n          <div className=\"w-full justify-end flex\">\n            <CTAButton\n              onClick={openModal}\n              className=\"mt-3 mr-0 mb-4 md:-mb-14 z-10\"\n            >\n              <PlusCircle className=\"h-4 w-4\" weight=\"bold\" />{\" \"}\n              {t(\"api.generate\")}\n            </CTAButton>\n          </div>\n          <div className=\"overflow-x-auto mt-6\">\n            {loading ? (\n              <Skeleton.default\n                height=\"80vh\"\n                width=\"100%\"\n                highlightColor=\"var(--theme-bg-primary)\"\n                baseColor=\"var(--theme-bg-secondary)\"\n                count={1}\n                className=\"w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm\"\n                containerClassName=\"flex w-full\"\n              />\n            ) : (\n              <table className=\"w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0\">\n                <thead className=\"text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b\">\n                  <tr>\n                    <th scope=\"col\" className=\"px-6 py-3 rounded-tl-lg\">\n                      {t(\"api.table.key\")}\n                    </th>\n                    <th scope=\"col\" className=\"px-6 py-3\">\n                      {t(\"api.table.by\")}\n                    </th>\n                    <th scope=\"col\" className=\"px-6 py-3\">\n                      {t(\"api.table.created\")}\n                    </th>\n                    <th scope=\"col\" className=\"px-6 py-3 rounded-tr-lg\">\n                      {\" \"}\n                    </th>\n                  </tr>\n                </thead>\n                <tbody>\n                  {apiKeys.length === 0 ? (\n                    <tr className=\"bg-transparent text-theme-text-secondary text-sm font-medium\">\n                      <td colSpan=\"4\" className=\"px-6 py-4 text-center\">\n                        No API keys found\n                      </td>\n                    </tr>\n                  ) : (\n                    apiKeys.map((apiKey) => (\n                      <ApiKeyRow\n                        key={apiKey.id}\n                        apiKey={apiKey}\n                        removeApiKey={removeApiKey}\n                      />\n                    ))\n                  )}\n                </tbody>\n              </table>\n            )}\n          </div>\n        </div>\n        <ModalWrapper isOpen={isOpen}>\n          <NewApiKeyModal\n            closeModal={closeModal}\n            onSuccess={fetchExistingKeys}\n          />\n        </ModalWrapper>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/AudioPreference/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { isMobile } from \"react-device-detect\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport System from \"@/models/system\";\nimport PreLoader from \"@/components/Preloader\";\nimport SpeechToTextProvider from \"./stt\";\nimport TextToSpeechProvider from \"./tts\";\n\nexport default function AudioPreference() {\n  const [settings, setSettings] = useState(null);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function fetchKeys() {\n      const _settings = await System.keys();\n      setSettings(_settings);\n      setLoading(false);\n    }\n    fetchKeys();\n  }, []);\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      {loading ? (\n        <div\n          style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n          className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n        >\n          <div className=\"w-full h-full flex justify-center items-center\">\n            <PreLoader />\n          </div>\n        </div>\n      ) : (\n        <div\n          style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n          className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n        >\n          <SpeechToTextProvider settings={settings} />\n          <TextToSpeechProvider settings={settings} />\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/AudioPreference/stt.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport LLMItem from \"@/components/LLMSelection/LLMItem\";\nimport { CaretUpDown, MagnifyingGlass, X } from \"@phosphor-icons/react\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport AnythingLLMIcon from \"@/media/logo/anything-llm-icon.png\";\nimport BrowserNative from \"@/components/SpeechToText/BrowserNative\";\n\nconst PROVIDERS = [\n  {\n    name: \"System native\",\n    value: \"native\",\n    logo: AnythingLLMIcon,\n    options: (settings) => <BrowserNative settings={settings} />,\n    description: \"Uses your browser's built in STT service if supported.\",\n  },\n];\n\nexport default function SpeechToTextProvider({ settings }) {\n  const [saving, setSaving] = useState(false);\n  const [hasChanges, setHasChanges] = useState(false);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [filteredProviders, setFilteredProviders] = useState([]);\n  const [selectedProvider, setSelectedProvider] = useState(\n    settings?.SpeechToTextProvider || \"native\"\n  );\n  const [searchMenuOpen, setSearchMenuOpen] = useState(false);\n  const searchInputRef = useRef(null);\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = e.target;\n    const data = { SpeechToTextProvider: selectedProvider };\n    const formData = new FormData(form);\n\n    for (var [key, value] of formData.entries()) data[key] = value;\n    const { error } = await System.updateSystem(data);\n    setSaving(true);\n\n    if (error) {\n      showToast(`Failed to save preferences: ${error}`, \"error\");\n    } else {\n      showToast(\"Speech-to-text preferences saved successfully.\", \"success\");\n    }\n    setSaving(false);\n    setHasChanges(!!error);\n  };\n\n  const updateProviderChoice = (selection) => {\n    setSearchQuery(\"\");\n    setSelectedProvider(selection);\n    setSearchMenuOpen(false);\n    setHasChanges(true);\n  };\n\n  const handleXButton = () => {\n    if (searchQuery.length > 0) {\n      setSearchQuery(\"\");\n      if (searchInputRef.current) searchInputRef.current.value = \"\";\n    } else {\n      setSearchMenuOpen(!searchMenuOpen);\n    }\n  };\n\n  useEffect(() => {\n    const filtered = PROVIDERS.filter((provider) =>\n      provider.name.toLowerCase().includes(searchQuery.toLowerCase())\n    );\n    setFilteredProviders(filtered);\n  }, [searchQuery, selectedProvider]);\n\n  const selectedProviderObject = PROVIDERS.find(\n    (provider) => provider.value === selectedProvider\n  );\n\n  return (\n    <form onSubmit={handleSubmit} className=\"flex w-full\">\n      <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n        <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n          <div className=\"flex gap-x-4 items-center\">\n            <p className=\"text-lg leading-6 font-bold text-white\">\n              Speech-to-text Preference\n            </p>\n          </div>\n          <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n            Here you can specify what kind of text-to-speech and speech-to-text\n            providers you would want to use in your AnythingLLM experience. By\n            default, we use the browser's built in support for these services,\n            but you may want to use others.\n          </p>\n        </div>\n        <div className=\"w-full justify-end flex\">\n          {hasChanges && (\n            <CTAButton\n              onClick={() => handleSubmit()}\n              className=\"mt-3 mr-0 -mb-14 z-10\"\n            >\n              {saving ? \"Saving...\" : \"Save changes\"}\n            </CTAButton>\n          )}\n        </div>\n        <div className=\"text-base font-bold text-white mt-6 mb-4\">Provider</div>\n        <div className=\"relative\">\n          {searchMenuOpen && (\n            <div\n              className=\"fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10\"\n              onClick={() => setSearchMenuOpen(false)}\n            />\n          )}\n          {searchMenuOpen ? (\n            <div className=\"absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] min-h-[64px] bg-theme-settings-input-bg rounded-lg flex flex-col justify-between cursor-pointer border-2 border-primary-button z-20\">\n              <div className=\"w-full flex flex-col gap-y-1\">\n                <div className=\"flex items-center sticky top-0 z-10 border-b border-[#9CA3AF] mx-4 bg-theme-settings-input-bg\">\n                  <MagnifyingGlass\n                    size={20}\n                    weight=\"bold\"\n                    className=\"absolute left-4 z-30 text-theme-text-primary -ml-4 my-2\"\n                  />\n                  <input\n                    type=\"text\"\n                    name=\"stt-provider-search\"\n                    autoComplete=\"off\"\n                    placeholder=\"Search speech to text providers\"\n                    className=\"border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none text-theme-text-primary placeholder:text-theme-text-primary placeholder:font-medium\"\n                    onChange={(e) => setSearchQuery(e.target.value)}\n                    ref={searchInputRef}\n                    onKeyDown={(e) => {\n                      if (e.key === \"Enter\") e.preventDefault();\n                    }}\n                  />\n                  <X\n                    size={20}\n                    weight=\"bold\"\n                    className=\"cursor-pointer text-white hover:text-x-button\"\n                    onClick={handleXButton}\n                  />\n                </div>\n                <div className=\"flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4 max-h-[245px]\">\n                  {filteredProviders.map((provider) => (\n                    <LLMItem\n                      key={provider.name}\n                      name={provider.name}\n                      value={provider.value}\n                      image={provider.logo}\n                      description={provider.description}\n                      checked={selectedProvider === provider.value}\n                      onClick={() => updateProviderChoice(provider.value)}\n                    />\n                  ))}\n                </div>\n              </div>\n            </div>\n          ) : (\n            <button\n              className=\"w-full max-w-[640px] h-[64px] bg-theme-settings-input-bg rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-primary-button transition-all duration-300\"\n              type=\"button\"\n              onClick={() => setSearchMenuOpen(true)}\n            >\n              <div className=\"flex gap-x-4 items-center\">\n                <img\n                  src={selectedProviderObject.logo}\n                  alt={`${selectedProviderObject.name} logo`}\n                  className=\"w-10 h-10 rounded-md\"\n                />\n                <div className=\"flex flex-col text-left\">\n                  <div className=\"text-sm font-semibold text-white\">\n                    {selectedProviderObject.name}\n                  </div>\n                  <div className=\"mt-1 text-xs text-description\">\n                    {selectedProviderObject.description}\n                  </div>\n                </div>\n              </div>\n              <CaretUpDown size={24} weight=\"bold\" className=\"text-white\" />\n            </button>\n          )}\n        </div>\n        <div\n          onChange={() => setHasChanges(true)}\n          className=\"mt-4 flex flex-col gap-y-1\"\n        >\n          {selectedProvider &&\n            PROVIDERS.find(\n              (provider) => provider.value === selectedProvider\n            )?.options(settings)}\n        </div>\n      </div>\n    </form>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/AudioPreference/tts.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport LLMItem from \"@/components/LLMSelection/LLMItem\";\nimport { CaretUpDown, MagnifyingGlass, X } from \"@phosphor-icons/react\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport OpenAiLogo from \"@/media/llmprovider/openai.png\";\nimport AnythingLLMIcon from \"@/media/logo/anything-llm-icon.png\";\nimport ElevenLabsIcon from \"@/media/ttsproviders/elevenlabs.png\";\nimport PiperTTSIcon from \"@/media/ttsproviders/piper.png\";\nimport GenericOpenAiLogo from \"@/media/ttsproviders/generic-openai.png\";\n\nimport BrowserNative from \"@/components/TextToSpeech/BrowserNative\";\nimport OpenAiTTSOptions from \"@/components/TextToSpeech/OpenAiOptions\";\nimport ElevenLabsTTSOptions from \"@/components/TextToSpeech/ElevenLabsOptions\";\nimport PiperTTSOptions from \"@/components/TextToSpeech/PiperTTSOptions\";\nimport OpenAiGenericTTSOptions from \"@/components/TextToSpeech/OpenAiGenericOptions\";\n\nconst PROVIDERS = [\n  {\n    name: \"System native\",\n    value: \"native\",\n    logo: AnythingLLMIcon,\n    options: (settings) => <BrowserNative settings={settings} />,\n    description: \"Uses your browser's built in TTS service if supported.\",\n  },\n  {\n    name: \"OpenAI\",\n    value: \"openai\",\n    logo: OpenAiLogo,\n    options: (settings) => <OpenAiTTSOptions settings={settings} />,\n    description: \"Use OpenAI's text to speech voices.\",\n  },\n  {\n    name: \"ElevenLabs\",\n    value: \"elevenlabs\",\n    logo: ElevenLabsIcon,\n    options: (settings) => <ElevenLabsTTSOptions settings={settings} />,\n    description: \"Use ElevenLabs's text to speech voices and technology.\",\n  },\n  {\n    name: \"PiperTTS\",\n    value: \"piper_local\",\n    logo: PiperTTSIcon,\n    options: (settings) => <PiperTTSOptions settings={settings} />,\n    description: \"Run TTS models locally in your browser privately.\",\n  },\n  {\n    name: \"OpenAI Compatible\",\n    value: \"generic-openai\",\n    logo: GenericOpenAiLogo,\n    options: (settings) => <OpenAiGenericTTSOptions settings={settings} />,\n    description:\n      \"Connect to an OpenAI compatible TTS service running locally or remotely.\",\n  },\n];\n\nexport default function TextToSpeechProvider({ settings }) {\n  const [saving, setSaving] = useState(false);\n  const [hasChanges, setHasChanges] = useState(false);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [filteredProviders, setFilteredProviders] = useState([]);\n  const [selectedProvider, setSelectedProvider] = useState(\n    settings?.TextToSpeechProvider || \"native\"\n  );\n  const [searchMenuOpen, setSearchMenuOpen] = useState(false);\n  const searchInputRef = useRef(null);\n\n  const handleSubmit = async (e) => {\n    e?.preventDefault();\n    const form = e.target;\n    const data = { TextToSpeechProvider: selectedProvider };\n    const formData = new FormData(form);\n\n    for (var [key, value] of formData.entries()) data[key] = value;\n    const { error } = await System.updateSystem(data);\n    setSaving(true);\n\n    if (error) {\n      showToast(`Failed to save preferences: ${error}`, \"error\");\n    } else {\n      showToast(\"Text-to-speech preferences saved successfully.\", \"success\");\n    }\n    setSaving(false);\n    setHasChanges(!!error);\n  };\n\n  const updateProviderChoice = (selection) => {\n    setSearchQuery(\"\");\n    setSelectedProvider(selection);\n    setSearchMenuOpen(false);\n    setHasChanges(true);\n  };\n\n  const handleXButton = () => {\n    if (searchQuery.length > 0) {\n      setSearchQuery(\"\");\n      if (searchInputRef.current) searchInputRef.current.value = \"\";\n    } else {\n      setSearchMenuOpen(!searchMenuOpen);\n    }\n  };\n\n  useEffect(() => {\n    const filtered = PROVIDERS.filter((provider) =>\n      provider.name.toLowerCase().includes(searchQuery.toLowerCase())\n    );\n    setFilteredProviders(filtered);\n  }, [searchQuery, selectedProvider]);\n\n  const selectedProviderObject = PROVIDERS.find(\n    (provider) => provider.value === selectedProvider\n  );\n\n  return (\n    <form onSubmit={handleSubmit} className=\"flex w-full\">\n      <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n        <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n          <div className=\"flex gap-x-4 items-center\">\n            <p className=\"text-lg leading-6 font-bold text-white\">\n              Text-to-speech Preference\n            </p>\n          </div>\n          <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n            Here you can specify what kind of text-to-speech providers you would\n            want to use in your AnythingLLM experience. By default, we use the\n            browser's built in support for these services, but you may want to\n            use others.\n          </p>\n        </div>\n        <div className=\"w-full justify-end flex\">\n          {hasChanges && (\n            <CTAButton className=\"mt-3 mr-0 -mb-14 z-10\">\n              {saving ? \"Saving...\" : \"Save changes\"}\n            </CTAButton>\n          )}\n        </div>\n        <div className=\"text-base font-bold text-white mt-6 mb-4\">Provider</div>\n        <div className=\"relative\">\n          {searchMenuOpen && (\n            <div\n              className=\"fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10\"\n              onClick={() => setSearchMenuOpen(false)}\n            />\n          )}\n          {searchMenuOpen ? (\n            <div className=\"absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] min-h-[64px] bg-theme-settings-input-bg rounded-lg flex flex-col justify-between cursor-pointer border-2 border-primary-button z-20\">\n              <div className=\"w-full flex flex-col gap-y-1\">\n                <div className=\"flex items-center sticky top-0 z-10 border-b border-[#9CA3AF] mx-4 bg-theme-settings-input-bg\">\n                  <MagnifyingGlass\n                    size={20}\n                    weight=\"bold\"\n                    className=\"absolute left-4 z-30 text-theme-text-primary -ml-4 my-2\"\n                  />\n                  <input\n                    type=\"text\"\n                    name=\"tts-provider-search\"\n                    autoComplete=\"off\"\n                    placeholder=\"Search text to speech providers\"\n                    className=\"border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none text-theme-text-primary placeholder:text-theme-text-primary placeholder:font-medium\"\n                    onChange={(e) => setSearchQuery(e.target.value)}\n                    ref={searchInputRef}\n                    onKeyDown={(e) => {\n                      if (e.key === \"Enter\") e.preventDefault();\n                    }}\n                  />\n                  <X\n                    size={20}\n                    weight=\"bold\"\n                    className=\"cursor-pointer text-white hover:text-x-button\"\n                    onClick={handleXButton}\n                  />\n                </div>\n                <div className=\"flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4 max-h-[245px]\">\n                  {filteredProviders.map((provider) => (\n                    <LLMItem\n                      key={provider.name}\n                      name={provider.name}\n                      value={provider.value}\n                      image={provider.logo}\n                      description={provider.description}\n                      checked={selectedProvider === provider.value}\n                      onClick={() => updateProviderChoice(provider.value)}\n                    />\n                  ))}\n                </div>\n              </div>\n            </div>\n          ) : (\n            <button\n              className=\"w-full max-w-[640px] h-[64px] bg-theme-settings-input-bg rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-primary-button transition-all duration-300\"\n              type=\"button\"\n              onClick={() => setSearchMenuOpen(true)}\n            >\n              <div className=\"flex gap-x-4 items-center\">\n                <img\n                  src={selectedProviderObject.logo}\n                  alt={`${selectedProviderObject.name} logo`}\n                  className=\"w-10 h-10 rounded-md\"\n                />\n                <div className=\"flex flex-col text-left\">\n                  <div className=\"text-sm font-semibold text-white\">\n                    {selectedProviderObject.name}\n                  </div>\n                  <div className=\"mt-1 text-xs text-description\">\n                    {selectedProviderObject.description}\n                  </div>\n                </div>\n              </div>\n              <CaretUpDown size={24} weight=\"bold\" className=\"text-white\" />\n            </button>\n          )}\n        </div>\n        <div\n          onChange={() => setHasChanges(true)}\n          className=\"mt-4 flex flex-col gap-y-1\"\n        >\n          {selectedProvider &&\n            PROVIDERS.find(\n              (provider) => provider.value === selectedProvider\n            )?.options(settings)}\n        </div>\n      </div>\n    </form>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/BrowserExtensionApiKey/BrowserExtensionApiKeyRow/index.jsx",
    "content": "import { useRef, useState } from \"react\";\nimport BrowserExtensionApiKey from \"@/models/browserExtensionApiKey\";\nimport showToast from \"@/utils/toast\";\nimport { Trash, Copy, Check, Plug } from \"@phosphor-icons/react\";\nimport { POPUP_BROWSER_EXTENSION_EVENT } from \"@/utils/constants\";\n\nexport default function BrowserExtensionApiKeyRow({\n  apiKey,\n  removeApiKey,\n  connectionString,\n  isMultiUser,\n}) {\n  const rowRef = useRef(null);\n  const [copied, setCopied] = useState(false);\n\n  const handleRevoke = async () => {\n    if (\n      !window.confirm(\n        `Are you sure you want to revoke this browser extension API key?\\nAfter you do this it will no longer be useable.\\n\\nThis action is irreversible.`\n      )\n    )\n      return false;\n\n    const result = await BrowserExtensionApiKey.revoke(apiKey.id);\n    if (result.success) {\n      removeApiKey(apiKey.id);\n      showToast(\"Browser Extension API Key permanently revoked\", \"info\", {\n        clear: true,\n      });\n    } else {\n      showToast(\"Failed to revoke API Key\", \"error\", {\n        clear: true,\n      });\n    }\n  };\n\n  const handleCopy = () => {\n    navigator.clipboard.writeText(connectionString);\n    showToast(\"Connection string copied to clipboard\", \"success\", {\n      clear: true,\n    });\n    setCopied(true);\n    setTimeout(() => setCopied(false), 2000);\n  };\n\n  const handleConnect = () => {\n    // Sending a message to Chrome extension to pop up the extension window\n    // This will open the extension window and attempt to connect with the API key\n    window.postMessage(\n      { type: POPUP_BROWSER_EXTENSION_EVENT, apiKey: connectionString },\n      \"*\"\n    );\n    showToast(\"Attempting to connect to browser extension...\", \"info\", {\n      clear: true,\n    });\n  };\n\n  return (\n    <tr\n      ref={rowRef}\n      className=\"bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10\"\n    >\n      <td scope=\"row\" className=\"px-6 py-2 whitespace-nowrap\">\n        <div className=\"flex items-center\">\n          <span className=\"mr-2 font-mono\">{connectionString}</span>\n          <div className=\"flex items-center space-x-2\">\n            <button\n              onClick={handleCopy}\n              data-tooltip-id=\"copy-connection-text\"\n              data-tooltip-content=\"Copy connection string\"\n              className=\"border-none text-theme-text-primary hover:text-theme-text-secondary transition-colors duration-200 p-1 rounded\"\n            >\n              {copied ? (\n                <Check className=\"h-4 w-4 text-green-500\" />\n              ) : (\n                <Copy className=\"h-4 w-4\" />\n              )}\n            </button>\n\n            <button\n              onClick={handleConnect}\n              data-tooltip-id=\"auto-connection\"\n              data-tooltip-content=\"Automatically connect to extension\"\n              className=\"border-none text-theme-text-primary hover:text-theme-text-secondary transition-colors duration-200 p-1 rounded\"\n            >\n              <Plug className=\"h-4 w-4\" />\n            </button>\n          </div>\n        </div>\n      </td>\n      {isMultiUser && (\n        <td className=\"px-6 py-2\">\n          {apiKey.user ? apiKey.user.username : \"N/A\"}\n        </td>\n      )}\n      <td className=\"px-6 py-2\">\n        {new Date(apiKey.createdAt).toLocaleString()}\n      </td>\n      <td className=\"px-6 py-2\">\n        <button\n          onClick={handleRevoke}\n          className=\"text-xs font-medium text-white/80 light:text-black/80 hover:light:text-red-500 hover:text-red-300 rounded-lg px-2 py-1 hover:bg-white hover:light:bg-red-50 hover:bg-opacity-10\"\n        >\n          <Trash className=\"h-4 w-4\" />\n        </button>\n      </td>\n    </tr>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/BrowserExtensionApiKey/NewBrowserExtensionApiKeyModal/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport BrowserExtensionApiKey from \"@/models/browserExtensionApiKey\";\nimport { fullApiUrl, POPUP_BROWSER_EXTENSION_EVENT } from \"@/utils/constants\";\n\nexport default function NewBrowserExtensionApiKeyModal({\n  closeModal,\n  onSuccess,\n  isMultiUser,\n}) {\n  const [apiKey, setApiKey] = useState(null);\n  const [error, setError] = useState(null);\n  const [copied, setCopied] = useState(false);\n\n  const handleCreate = async (e) => {\n    setError(null);\n    e.preventDefault();\n\n    const { apiKey: newApiKey, error } =\n      await BrowserExtensionApiKey.generateKey();\n    if (!!newApiKey) {\n      const fullApiKey = `${fullApiUrl()}|${newApiKey}`;\n      setApiKey(fullApiKey);\n      onSuccess();\n\n      window.postMessage(\n        { type: POPUP_BROWSER_EXTENSION_EVENT, apiKey: fullApiKey },\n        \"*\"\n      );\n    }\n    setError(error);\n  };\n\n  const copyApiKey = () => {\n    if (!apiKey) return false;\n    window.navigator.clipboard.writeText(apiKey);\n    setCopied(true);\n  };\n\n  useEffect(() => {\n    function resetStatus() {\n      if (!copied) return false;\n      setTimeout(() => {\n        setCopied(false);\n      }, 3000);\n    }\n    resetStatus();\n  }, [copied]);\n\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n      <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              New Browser Extension API Key\n            </h3>\n          </div>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div className=\"px-7 py-6\">\n          <form onSubmit={handleCreate}>\n            <div className=\"space-y-6 max-h-[60vh] overflow-y-auto pr-2\">\n              {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n              {apiKey && (\n                <input\n                  type=\"text\"\n                  defaultValue={apiKey}\n                  disabled={true}\n                  className=\"border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg block w-full p-2.5\"\n                />\n              )}\n              {isMultiUser && (\n                <p className=\"text-yellow-300 light:text-orange-500 text-xs md:text-sm font-semibold\">\n                  Warning: You are in multi-user mode, this API key will allow\n                  access to all workspaces associated with your account. Please\n                  share it cautiously.\n                </p>\n              )}\n              <p className=\"text-white text-opacity-60 text-xs md:text-sm\">\n                After clicking \"Create API Key\", AnythingLLM will attempt to\n                connect to your browser extension automatically.\n              </p>\n              <p className=\"text-white text-opacity-60 text-xs md:text-sm\">\n                If you see \"Connected to AnythingLLM\" in the extension, the\n                connection was successful. If not, please copy the connection\n                string and paste it into the extension manually.\n              </p>\n            </div>\n            <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border\">\n              {!apiKey ? (\n                <>\n                  <button\n                    onClick={closeModal}\n                    type=\"button\"\n                    className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n                  >\n                    Cancel\n                  </button>\n                  <button\n                    type=\"submit\"\n                    className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n                  >\n                    Create API Key\n                  </button>\n                </>\n              ) : (\n                <button\n                  onClick={copyApiKey}\n                  type=\"button\"\n                  disabled={copied}\n                  className=\"w-full transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm cursor-pointer\"\n                >\n                  {copied ? \"API Key Copied!\" : \"Copy API Key\"}\n                </button>\n              )}\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/BrowserExtensionApiKey/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport { PlusCircle } from \"@phosphor-icons/react\";\nimport BrowserExtensionApiKey from \"@/models/browserExtensionApiKey\";\nimport BrowserExtensionApiKeyRow from \"./BrowserExtensionApiKeyRow\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport NewBrowserExtensionApiKeyModal from \"./NewBrowserExtensionApiKeyModal\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { useModal } from \"@/hooks/useModal\";\nimport { fullApiUrl } from \"@/utils/constants\";\nimport { Tooltip } from \"react-tooltip\";\n\nexport default function BrowserExtensionApiKeys() {\n  const [loading, setLoading] = useState(true);\n  const [apiKeys, setApiKeys] = useState([]);\n  const [error, setError] = useState(null);\n  const { isOpen, openModal, closeModal } = useModal();\n  const [isMultiUser, setIsMultiUser] = useState(false);\n\n  useEffect(() => {\n    fetchExistingKeys();\n  }, []);\n\n  const fetchExistingKeys = async () => {\n    const result = await BrowserExtensionApiKey.getAll();\n    if (result.success) {\n      setApiKeys(result.apiKeys);\n      setIsMultiUser(result.apiKeys.some((key) => key.user !== null));\n    } else {\n      setError(result.error || \"Failed to fetch API keys\");\n    }\n    setLoading(false);\n  };\n\n  const removeApiKey = (id) => {\n    setApiKeys((prevKeys) => prevKeys.filter((apiKey) => apiKey.id !== id));\n  };\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2\">\n            <div className=\"items-center flex gap-x-4\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                Browser Extension API Keys\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary mt-2\">\n              Manage API keys for browser extensions connecting to your\n              AnythingLLM instance.\n            </p>\n          </div>\n          <div className=\"w-full justify-end flex\">\n            <CTAButton\n              onClick={openModal}\n              className=\"mt-3 mr-0 mb-4 md:-mb-14 z-10\"\n            >\n              <PlusCircle className=\"h-4 w-4\" weight=\"bold\" />\n              Generate New API Key\n            </CTAButton>\n          </div>\n          <div className=\"overflow-x-auto mt-6\">\n            {loading ? (\n              <Skeleton.default\n                height=\"80vh\"\n                width=\"100%\"\n                highlightColor=\"var(--theme-bg-primary)\"\n                baseColor=\"var(--theme-bg-secondary)\"\n                count={1}\n                className=\"w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm\"\n                containerClassName=\"flex w-full\"\n              />\n            ) : error ? (\n              <div className=\"text-red-500 mt-6\">Error: {error}</div>\n            ) : (\n              <table className=\"w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0 md:mt-6 mt-0\">\n                <thead className=\"text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b\">\n                  <tr>\n                    <th scope=\"col\" className=\"px-6 py-2 rounded-tl-lg\">\n                      Extension Connection String\n                    </th>\n                    {isMultiUser && (\n                      <th scope=\"col\" className=\"px-6 py-2\">\n                        Created By\n                      </th>\n                    )}\n                    <th scope=\"col\" className=\"px-6 py-2\">\n                      Created At\n                    </th>\n                    <th scope=\"col\" className=\"px-6 py-2 rounded-tr-lg\">\n                      Actions\n                    </th>\n                  </tr>\n                </thead>\n                <tbody>\n                  {apiKeys.length === 0 ? (\n                    <tr className=\"bg-transparent text-theme-text-secondary text-sm font-medium\">\n                      <td\n                        colSpan={isMultiUser ? \"4\" : \"3\"}\n                        className=\"px-6 py-4 text-center\"\n                      >\n                        No API keys found\n                      </td>\n                    </tr>\n                  ) : (\n                    apiKeys.map((apiKey) => (\n                      <BrowserExtensionApiKeyRow\n                        key={apiKey.id}\n                        apiKey={apiKey}\n                        removeApiKey={removeApiKey}\n                        connectionString={`${fullApiUrl()}|${apiKey.key}`}\n                        isMultiUser={isMultiUser}\n                      />\n                    ))\n                  )}\n                </tbody>\n              </table>\n            )}\n          </div>\n        </div>\n      </div>\n      <ModalWrapper isOpen={isOpen}>\n        <NewBrowserExtensionApiKeyModal\n          closeModal={closeModal}\n          onSuccess={fetchExistingKeys}\n          isMultiUser={isMultiUser}\n        />\n      </ModalWrapper>\n      <Tooltip\n        id=\"auto-connection\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"allm-tooltip !allm-text-xs\"\n      />\n      <Tooltip\n        id=\"copy-connection-text\"\n        place=\"bottom\"\n        delayShow={300}\n        className=\"allm-tooltip !allm-text-xs\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/ChatRow/index.jsx",
    "content": "import truncate from \"truncate\";\nimport { X } from \"@phosphor-icons/react\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { useModal } from \"@/hooks/useModal\";\nimport paths from \"@/utils/paths\";\nimport Embed from \"@/models/embed\";\nimport MarkdownRenderer from \"../MarkdownRenderer\";\nimport { safeJsonParse } from \"@/utils/request\";\n\nexport default function ChatRow({ chat, onDelete }) {\n  const {\n    isOpen: isPromptOpen,\n    openModal: openPromptModal,\n    closeModal: closePromptModal,\n  } = useModal();\n  const {\n    isOpen: isResponseOpen,\n    openModal: openResponseModal,\n    closeModal: closeResponseModal,\n  } = useModal();\n  const {\n    isOpen: isConnectionDetailsModalOpen,\n    openModal: openConnectionDetailsModal,\n    closeModal: closeConnectionDetailsModal,\n  } = useModal();\n\n  const handleDelete = async () => {\n    if (\n      !window.confirm(\n        `Are you sure you want to delete this chat?\\n\\nThis action is irreversible.`\n      )\n    )\n      return false;\n    await Embed.deleteChat(chat.id);\n    onDelete(chat.id);\n  };\n\n  return (\n    <>\n      <tr className=\"bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10\">\n        <td className=\"px-6 font-medium whitespace-nowrap text-white\">\n          <a\n            href={paths.settings.embedChatWidgets()}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"text-white flex items-center hover:underline\"\n          >\n            {chat.embed_config.workspace.name}\n          </a>\n        </td>\n        <td\n          onClick={openConnectionDetailsModal}\n          className=\"px-6 cursor-pointer hover:shadow-lg\"\n        >\n          <div className=\"flex flex-col\">\n            <p>{truncate(chat.session_id, 20)}</p>\n          </div>\n        </td>\n        <td\n          onClick={openPromptModal}\n          className=\"px-6 border-transparent cursor-pointer hover:shadow-lg\"\n        >\n          {truncate(chat.prompt, 40)}\n        </td>\n        <td\n          onClick={openResponseModal}\n          className=\"px-6 cursor-pointer hover:shadow-lg\"\n        >\n          {truncate(safeJsonParse(chat.response, {})?.text, 40)}\n        </td>\n        <td className=\"px-6\">{chat.createdAt}</td>\n        <td className=\"px-6 flex items-center gap-x-6 h-full mt-1\">\n          <button\n            onClick={handleDelete}\n            className=\"group text-xs font-medium text-theme-text-secondary px-2 py-1 rounded-lg hover:bg-theme-button-delete-hover-bg\"\n          >\n            <span className=\"group-hover:text-theme-button-delete-hover-text\">\n              Delete\n            </span>\n          </button>\n        </td>\n      </tr>\n      <ModalWrapper isOpen={isPromptOpen}>\n        <TextPreview text={chat.prompt} closeModal={closePromptModal} />\n      </ModalWrapper>\n      <ModalWrapper isOpen={isResponseOpen}>\n        <TextPreview\n          text={\n            <MarkdownRenderer\n              content={safeJsonParse(chat.response, {})?.text}\n            />\n          }\n          closeModal={closeResponseModal}\n        />\n      </ModalWrapper>\n      <ModalWrapper isOpen={isConnectionDetailsModalOpen}>\n        <TextPreview\n          text={\n            <ConnectionDetails\n              sessionId={chat.session_id}\n              verbose={true}\n              connection_information={chat.connection_information}\n            />\n          }\n          closeModal={closeConnectionDetailsModal}\n        />\n      </ModalWrapper>\n    </>\n  );\n}\n\nconst TextPreview = ({ text, closeModal }) => {\n  return (\n    <div className=\"relative w-full md:max-w-2xl max-h-full\">\n      <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden\">\n        <div className=\"flex items-center justify-between p-6 border-b rounded-t border-theme-modal-border\">\n          <h3 className=\"text-xl font-semibold text-white\">Viewing Text</h3>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X className=\"text-white text-lg\" />\n          </button>\n        </div>\n        <div className=\"w-full p-6\">\n          <div className=\"w-full h-[60vh] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-900 light:bg-theme-bg-secondary border border-gray-500 text-white text-sm\">\n            {text}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst ConnectionDetails = ({\n  sessionId,\n  verbose = false,\n  connection_information,\n}) => {\n  const details = safeJsonParse(connection_information, {});\n  if (Object.keys(details).length === 0) return null;\n\n  if (verbose) {\n    return (\n      <>\n        <p className=\"text-xs text-theme-text-secondary\">\n          sessionID: {sessionId}\n        </p>\n        {details.username && (\n          <p className=\"text-xs text-theme-text-secondary\">\n            username: {details.username}\n          </p>\n        )}\n        {details.ip && (\n          <p className=\"text-xs text-theme-text-secondary\">\n            client ip address: {details.ip}\n          </p>\n        )}\n        {details.host && (\n          <p className=\"text-xs text-theme-text-secondary\">\n            client host URL: {details.host}\n          </p>\n        )}\n      </>\n    );\n  }\n\n  return (\n    <>\n      {details.username && (\n        <p className=\"text-xs text-theme-text-secondary\">{details.username}</p>\n      )}\n      {details.ip && (\n        <p className=\"text-xs text-theme-text-secondary\">{details.ip}</p>\n      )}\n      {details.host && (\n        <p className=\"text-xs text-theme-text-secondary\">{details.host}</p>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/MarkdownRenderer.jsx",
    "content": "import { useState } from \"react\";\nimport MarkdownIt from \"markdown-it\";\nimport hljs from \"highlight.js\";\nimport { CaretDown } from \"@phosphor-icons/react\";\nimport \"highlight.js/styles/github-dark.css\";\nimport DOMPurify from \"@/utils/chat/purify\";\n\nconst md = new MarkdownIt({\n  html: true,\n  breaks: true,\n  highlight: function (str, lang) {\n    if (lang && hljs.getLanguage(lang)) {\n      try {\n        return hljs.highlight(str, { language: lang }).value;\n      } catch {}\n    }\n    return \"\"; // use external default escaping\n  },\n});\n\nconst ThoughtBubble = ({ thought }) => {\n  const [isExpanded, setIsExpanded] = useState(false);\n\n  if (!thought) return null;\n\n  const cleanThought = thought.replace(/<\\/?think>/g, \"\").trim();\n  if (!cleanThought) return null;\n\n  return (\n    <div className=\"mb-3\">\n      <div\n        onClick={() => setIsExpanded(!isExpanded)}\n        className=\"cursor-pointer flex items-center gap-x-2 text-theme-text-secondary hover:text-theme-text-primary transition-colors mb-2\"\n      >\n        <CaretDown\n          size={14}\n          weight=\"bold\"\n          className={`transition-transform ${isExpanded ? \"rotate-180\" : \"\"}`}\n        />\n        <span className=\"text-xs font-medium\">View thoughts</span>\n      </div>\n      {isExpanded && (\n        <div className=\"bg-theme-bg-chat-input rounded-md p-3 border-l-2 border-theme-text-secondary/30\">\n          <div className=\"text-xs text-theme-text-secondary font-mono whitespace-pre-wrap\">\n            {cleanThought}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nfunction parseContent(content) {\n  const parts = [];\n  let lastIndex = 0;\n  content.replace(/<think>([^]*?)<\\/think>/g, (match, thinkContent, offset) => {\n    if (offset > lastIndex) {\n      parts.push({ type: \"normal\", text: content.slice(lastIndex, offset) });\n    }\n    parts.push({ type: \"think\", text: thinkContent });\n    lastIndex = offset + match.length;\n  });\n  if (lastIndex < content.length) {\n    parts.push({ type: \"normal\", text: content.slice(lastIndex) });\n  }\n  return parts;\n}\n\nexport default function MarkdownRenderer({ content }) {\n  if (!content) return null;\n\n  const parts = parseContent(content);\n  return (\n    <div className=\"whitespace-normal\">\n      {parts.map((part, index) => {\n        const html = md.render(part.text);\n        if (part.type === \"think\")\n          return <ThoughtBubble key={index} thought={part.text} />;\n        return (\n          <div\n            key={index}\n            dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}\n          />\n        );\n      })}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/index.jsx",
    "content": "import { useEffect, useState, useRef } from \"react\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport useQuery from \"@/hooks/useQuery\";\nimport ChatRow from \"./ChatRow\";\nimport Embed from \"@/models/embed\";\nimport { useTranslation } from \"react-i18next\";\nimport { CaretDown, Download } from \"@phosphor-icons/react\";\nimport showToast from \"@/utils/toast\";\nimport { saveAs } from \"file-saver\";\nimport System from \"@/models/system\";\n\nconst exportOptions = {\n  csv: {\n    name: \"CSV\",\n    mimeType: \"text/csv\",\n    fileExtension: \"csv\",\n    filenameFunc: () => {\n      return `anythingllm-embed-chats-${new Date().toLocaleDateString()}`;\n    },\n  },\n  json: {\n    name: \"JSON\",\n    mimeType: \"application/json\",\n    fileExtension: \"json\",\n    filenameFunc: () => {\n      return `anythingllm-embed-chats-${new Date().toLocaleDateString()}`;\n    },\n  },\n  jsonl: {\n    name: \"JSONL\",\n    mimeType: \"application/jsonl\",\n    fileExtension: \"jsonl\",\n    filenameFunc: () => {\n      return `anythingllm-embed-chats-${new Date().toLocaleDateString()}-lines`;\n    },\n  },\n  jsonAlpaca: {\n    name: \"JSON (Alpaca)\",\n    mimeType: \"application/json\",\n    fileExtension: \"json\",\n    filenameFunc: () => {\n      return `anythingllm-embed-chats-${new Date().toLocaleDateString()}-alpaca`;\n    },\n  },\n};\n\nexport default function EmbedChatsView() {\n  const { t } = useTranslation();\n  const menuRef = useRef();\n  const query = useQuery();\n  const openMenuButton = useRef();\n  const [showMenu, setShowMenu] = useState(false);\n  const [loading, setLoading] = useState(true);\n  const [chats, setChats] = useState([]);\n  const [offset, setOffset] = useState(Number(query.get(\"offset\") || 0));\n  const [canNext, setCanNext] = useState(false);\n\n  const handleDumpChats = async (exportType) => {\n    const chats = await System.exportChats(exportType, \"embed\");\n    if (!!chats) {\n      const { name, mimeType, fileExtension, filenameFunc } =\n        exportOptions[exportType];\n      const blob = new Blob([chats], { type: mimeType });\n      saveAs(blob, `${filenameFunc()}.${fileExtension}`);\n      showToast(`Embed chats exported successfully as ${name}.`, \"success\");\n    } else {\n      showToast(\"Failed to export embed chats.\", \"error\");\n    }\n  };\n\n  const toggleMenu = () => {\n    setShowMenu(!showMenu);\n  };\n\n  useEffect(() => {\n    function handleClickOutside(event) {\n      if (\n        menuRef.current &&\n        !menuRef.current.contains(event.target) &&\n        !openMenuButton.current.contains(event.target)\n      ) {\n        setShowMenu(false);\n      }\n    }\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, []);\n\n  useEffect(() => {\n    async function fetchChats() {\n      setLoading(true);\n      await Embed.chats(offset)\n        .then(({ chats: _chats, hasPages = false }) => {\n          setChats(_chats);\n          setCanNext(hasPages);\n        })\n        .finally(() => {\n          setLoading(false);\n        });\n    }\n    fetchChats();\n  }, [offset]);\n\n  const handlePrevious = () => {\n    setOffset(Math.max(offset - 1, 0));\n  };\n\n  const handleNext = () => {\n    setOffset(offset + 1);\n  };\n\n  const handleDeleteChat = (chatId) => {\n    setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatId));\n  };\n\n  if (loading) {\n    return (\n      <Skeleton.default\n        height=\"80vh\"\n        width=\"100%\"\n        highlightColor=\"var(--theme-bg-primary)\"\n        baseColor=\"var(--theme-bg-secondary)\"\n        count={1}\n        className=\"w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm\"\n        containerClassName=\"flex w-full\"\n      />\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-full p-4 overflow-none\">\n      <div className=\"w-full flex flex-col gap-y-1\">\n        <div className=\"flex flex-wrap gap-4 items-center\">\n          <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n            {t(\"embed-chats.title\")}\n          </p>\n          <div className=\"relative\">\n            <button\n              ref={openMenuButton}\n              onClick={toggleMenu}\n              className=\"flex items-center gap-x-2 px-4 py-1 rounded-lg text-theme-bg-chat bg-primary-button hover:bg-secondary hover:text-white text-xs font-semibold h-[34px] w-fit\"\n            >\n              <Download size={18} weight=\"bold\" />\n              {t(\"embed-chats.export\")}\n              <CaretDown size={18} weight=\"bold\" />\n            </button>\n            <div\n              ref={menuRef}\n              className={`${\n                showMenu ? \"slide-down\" : \"slide-up hidden\"\n              } z-20 w-fit rounded-lg absolute top-full right-0 bg-secondary light:bg-theme-bg-secondary mt-2 shadow-md`}\n            >\n              <div className=\"py-2\">\n                {Object.entries(exportOptions).map(([key, data]) => (\n                  <button\n                    key={key}\n                    onClick={() => {\n                      handleDumpChats(key);\n                      setShowMenu(false);\n                    }}\n                    className=\"w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147] light:hover:bg-theme-sidebar-item-hover\"\n                  >\n                    {data.name}\n                  </button>\n                ))}\n              </div>\n            </div>\n          </div>\n        </div>\n        <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary mt-2\">\n          {t(\"embed-chats.description\")}\n        </p>\n      </div>\n      <div className=\"overflow-x-auto mt-6\">\n        <table className=\"w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0\">\n          <thead className=\"text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b\">\n            <tr>\n              <th scope=\"col\" className=\"px-6 py-3 rounded-tl-lg\">\n                {t(\"embed-chats.table.embed\")}\n              </th>\n              <th scope=\"col\" className=\"px-6 py-3\">\n                {t(\"embed-chats.table.sender\")}\n              </th>\n              <th scope=\"col\" className=\"px-6 py-3\">\n                {t(\"embed-chats.table.message\")}\n              </th>\n              <th scope=\"col\" className=\"px-6 py-3\">\n                {t(\"embed-chats.table.response\")}\n              </th>\n              <th scope=\"col\" className=\"px-6 py-3\">\n                {t(\"embed-chats.table.at\")}\n              </th>\n              <th scope=\"col\" className=\"px-6 py-3 rounded-tr-lg\">\n                {\" \"}\n              </th>\n            </tr>\n          </thead>\n          <tbody>\n            {chats.map((chat) => (\n              <ChatRow key={chat.id} chat={chat} onDelete={handleDeleteChat} />\n            ))}\n          </tbody>\n        </table>\n        {(offset > 0 || canNext) && (\n          <div className=\"flex items-center justify-end gap-2 mt-4 pb-6\">\n            <button\n              onClick={handlePrevious}\n              disabled={offset === 0}\n              className={`px-4 py-2 text-sm rounded-lg ${\n                offset === 0\n                  ? \"bg-theme-bg-secondary text-theme-text-disabled cursor-not-allowed\"\n                  : \"bg-theme-bg-secondary text-theme-text-primary hover:bg-theme-hover\"\n              }`}\n            >\n              {t(\"common.previous\")}\n            </button>\n            <button\n              onClick={handleNext}\n              disabled={!canNext}\n              className={`px-4 py-2 text-sm rounded-lg ${\n                !canNext\n                  ? \"bg-theme-bg-secondary text-theme-text-disabled cursor-not-allowed\"\n                  : \"bg-theme-bg-secondary text-theme-text-primary hover:bg-theme-hover\"\n              }`}\n            >\n              {t(\"common.next\")}\n            </button>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedConfigs/EmbedRow/CodeSnippetModal/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport { CheckCircle, CopySimple, X } from \"@phosphor-icons/react\";\nimport showToast from \"@/utils/toast\";\nimport hljs from \"highlight.js\";\nimport \"@/utils/chat/themes/github-dark.css\";\nimport \"@/utils/chat/themes/github.css\";\n\nexport default function CodeSnippetModal({ embed, closeModal }) {\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n      <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Copy your embed code\n            </h3>\n          </div>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div className=\"px-7 py-6\">\n          <div className=\"space-y-6 max-h-[60vh] overflow-y-auto pr-2\">\n            <ScriptTag embed={embed} />\n          </div>\n          <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border\">\n            <button\n              onClick={closeModal}\n              type=\"button\"\n              className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n            >\n              Close\n            </button>\n            <div hidden={true} />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction createScriptTagSnippet(embed, scriptHost, serverHost) {\n  return `<!--\nPaste this script at the bottom of your HTML before the </body> tag.\nSee more style and config options on our docs\nhttps://github.com/Mintplex-Labs/anythingllm-embed/blob/main/README.md\n-->\n<script\n  data-embed-id=\"${embed.uuid}\"\n  data-base-api-url=\"${serverHost}/api/embed\"\n  src=\"${scriptHost}/embed/anythingllm-chat-widget.min.js\">\n</script>\n<!-- AnythingLLM (https://anythingllm.com) -->\n`;\n}\n\nconst ScriptTag = ({ embed }) => {\n  const [copied, setCopied] = useState(false);\n  const scriptHost = import.meta.env.DEV\n    ? \"http://localhost:3000\"\n    : window.location.origin;\n  const serverHost = import.meta.env.DEV\n    ? \"http://localhost:3001\"\n    : window.location.origin;\n  const snippet = createScriptTagSnippet(embed, scriptHost, serverHost);\n  const theme =\n    window.localStorage.getItem(\"theme\") === \"light\" ? \"github\" : \"github-dark\";\n\n  const handleClick = () => {\n    window.navigator.clipboard.writeText(snippet);\n    setCopied(true);\n    setTimeout(() => {\n      setCopied(false);\n    }, 2500);\n    showToast(\"Snippet copied to clipboard!\", \"success\", { clear: true });\n  };\n\n  return (\n    <div>\n      <div className=\"flex flex-col mb-2\">\n        <label className=\"block text-sm font-medium text-white\">\n          HTML Script Tag Embed Code\n        </label>\n        <p className=\"text-theme-text-secondary text-xs\">\n          Have your workspace chat embed function like a help desk chat bottom\n          in the corner of your website.\n        </p>\n        <a\n          href=\"https://github.com/Mintplex-Labs/anythingllm-embed/blob/main/README.md\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"text-blue-300 light:text-blue-500 hover:underline\"\n        >\n          View all style and configuration options &rarr;\n        </a>\n      </div>\n      <button\n        disabled={copied}\n        onClick={handleClick}\n        className={`disabled:border disabled:border-green-300 disabled:light:border-green-600 border border-transparent relative w-full font-mono flex hljs ${theme} light:border light:border-gray-700 text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5 m-1`}\n      >\n        <div\n          className=\"flex w-full text-left flex-col gap-y-1 pr-6 pl-4 whitespace-pre-line\"\n          dangerouslySetInnerHTML={{\n            __html: hljs.highlight(snippet, {\n              language: \"html\",\n              ignoreIllegals: true,\n            }).value,\n          }}\n        />\n        {copied ? (\n          <CheckCircle\n            size={14}\n            className=\"text-green-300 light:text-green-600 absolute top-2 right-2\"\n          />\n        ) : (\n          <CopySimple size={14} className=\"absolute top-2 right-2\" />\n        )}\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedConfigs/EmbedRow/EditEmbedModal/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport {\n  BooleanInput,\n  ChatModeSelection,\n  NumberInput,\n  PermittedDomains,\n  WorkspaceSelection,\n  enforceSubmissionSchema,\n} from \"../../NewEmbedModal\";\nimport Embed from \"@/models/embed\";\nimport showToast from \"@/utils/toast\";\nimport { safeJsonParse } from \"@/utils/request\";\n\nexport default function EditEmbedModal({ embed, closeModal }) {\n  const [error, setError] = useState(null);\n\n  const handleUpdate = async (e) => {\n    setError(null);\n    e.preventDefault();\n    const form = new FormData(e.target);\n    const data = enforceSubmissionSchema(form);\n    const { success, error } = await Embed.updateEmbed(embed.id, data);\n    if (success) {\n      showToast(\"Embed updated successfully.\", \"success\", { clear: true });\n      setTimeout(() => {\n        window.location.reload();\n      }, 800);\n    }\n    setError(error);\n  };\n\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n      <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Update embed #{embed.id}\n            </h3>\n          </div>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div className=\"px-7 py-6\">\n          <form onSubmit={handleUpdate}>\n            <div className=\"space-y-6 max-h-[60vh] overflow-y-auto pr-2\">\n              <WorkspaceSelection defaultValue={embed.workspace.id} />\n              <ChatModeSelection defaultValue={embed.chat_mode} />\n              <PermittedDomains\n                defaultValue={\n                  safeJsonParse(embed.allowlist_domains, null) || []\n                }\n              />\n              <NumberInput\n                name=\"max_chats_per_day\"\n                title=\"Max chats per day\"\n                hint=\"Limit the amount of chats this embedded chat can process in a 24 hour period. Zero is unlimited.\"\n                defaultValue={embed.max_chats_per_day}\n              />\n              <NumberInput\n                name=\"max_chats_per_session\"\n                title=\"Max chats per session\"\n                hint=\"Limit the amount of chats a session user can send with this embed in a 24 hour period. Zero is unlimited.\"\n                defaultValue={embed.max_chats_per_session}\n              />\n              <NumberInput\n                name=\"message_limit\"\n                title=\"Message History Limit\"\n                hint=\"The number of previous messages to include in the chat context. Default is 20.\"\n                defaultValue={embed.message_limit}\n              />\n              <BooleanInput\n                name=\"allow_model_override\"\n                title=\"Enable dynamic model use\"\n                hint=\"Allow setting of the preferred LLM model to override the workspace default.\"\n                defaultValue={embed.allow_model_override}\n              />\n              <BooleanInput\n                name=\"allow_temperature_override\"\n                title=\"Enable dynamic LLM temperature\"\n                hint=\"Allow setting of the LLM temperature to override the workspace default.\"\n                defaultValue={embed.allow_temperature_override}\n              />\n              <BooleanInput\n                name=\"allow_prompt_override\"\n                title=\"Enable Prompt Override\"\n                hint=\"Allow setting of the system prompt to override the workspace default.\"\n                defaultValue={embed.allow_prompt_override}\n              />\n\n              {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n              <p className=\"text-white text-opacity-60 text-xs md:text-sm\">\n                After creating an embed you will be provided a link that you can\n                publish on your website with a simple\n                <code className=\"border-none bg-theme-settings-input-bg text-white mx-1 px-1 rounded-sm\">\n                  &lt;script&gt;\n                </code>{\" \"}\n                tag.\n              </p>\n            </div>\n            <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border\">\n              <button\n                onClick={closeModal}\n                type=\"button\"\n                className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n              >\n                Cancel\n              </button>\n              <button\n                type=\"submit\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                Update embed\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedConfigs/EmbedRow/index.jsx",
    "content": "import { useRef, useState } from \"react\";\nimport { DotsThreeOutline } from \"@phosphor-icons/react\";\nimport showToast from \"@/utils/toast\";\nimport { useModal } from \"@/hooks/useModal\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport Embed from \"@/models/embed\";\nimport paths from \"@/utils/paths\";\nimport { nFormatter } from \"@/utils/numbers\";\nimport EditEmbedModal from \"./EditEmbedModal\";\nimport CodeSnippetModal from \"./CodeSnippetModal\";\nimport moment from \"moment\";\nimport { safeJsonParse } from \"@/utils/request\";\n\nexport default function EmbedRow({ embed }) {\n  const rowRef = useRef(null);\n  const [enabled, setEnabled] = useState(Number(embed.enabled) === 1);\n  const {\n    isOpen: isSettingsOpen,\n    openModal: openSettingsModal,\n    closeModal: closeSettingsModal,\n  } = useModal();\n  const {\n    isOpen: isSnippetOpen,\n    openModal: openSnippetModal,\n    closeModal: closeSnippetModal,\n  } = useModal();\n\n  const handleSuspend = async () => {\n    if (\n      !window.confirm(\n        `Are you sure you want to disabled this embed?\\nOnce disabled the embed will no longer respond to any chat requests.`\n      )\n    )\n      return false;\n\n    const { success, error } = await Embed.updateEmbed(embed.id, {\n      enabled: !enabled,\n    });\n    if (!success) showToast(error, \"error\", { clear: true });\n    if (success) {\n      showToast(\n        `Embed ${enabled ? \"has been disabled\" : \"is active\"}.`,\n        \"success\",\n        { clear: true }\n      );\n      setEnabled(!enabled);\n    }\n  };\n  const handleDelete = async () => {\n    if (\n      !window.confirm(\n        `Are you sure you want to delete this embed?\\nOnce deleted this embed will no longer respond to chats or be active.\\n\\nThis action is irreversible.`\n      )\n    )\n      return false;\n    const { success, error } = await Embed.deleteEmbed(embed.id);\n    if (!success) showToast(error, \"error\", { clear: true });\n    if (success) {\n      rowRef?.current?.remove();\n      showToast(\"Embed deleted from system.\", \"success\", { clear: true });\n    }\n  };\n\n  return (\n    <>\n      <tr\n        ref={rowRef}\n        className=\"bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10\"\n      >\n        <th\n          scope=\"row\"\n          className=\"px-6 whitespace-nowrap flex item-center gap-x-1\"\n        >\n          <a\n            href={paths.workspace.chat(embed.workspace.slug)}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"text-white flex items-center hover:underline\"\n          >\n            {embed.workspace.name}\n          </a>\n        </th>\n        <th scope=\"row\" className=\"px-6 whitespace-nowrap\">\n          {nFormatter(embed._count.embed_chats)}\n        </th>\n        <th scope=\"row\" className=\"px-6 whitespace-nowrap\">\n          <ActiveDomains domainList={embed.allowlist_domains} />\n        </th>\n        <th\n          scope=\"row\"\n          className=\"px-6 whitespace-nowrap text-theme-text-secondary !font-normal\"\n        >\n          {\n            // If the embed was created more than a day ago, show the date, otherwise show the time ago\n            moment(embed.createdAt).diff(moment(), \"days\") > 0\n              ? moment(embed.createdAt).format(\"MMM D, YYYY\")\n              : moment(embed.createdAt).fromNow()\n          }\n        </th>\n        <td className=\"px-6 flex items-center gap-x-6 h-full mt-1\">\n          <button\n            onClick={openSnippetModal}\n            className=\"group text-xs font-medium text-theme-text-secondary px-2 py-1 rounded-lg hover:bg-theme-button-code-hover-bg\"\n          >\n            <span className=\"group-hover:text-theme-button-code-hover-text\">\n              Code\n            </span>\n          </button>\n          <button\n            onClick={handleSuspend}\n            className=\"group text-xs font-medium text-theme-text-secondary px-2 py-1 rounded-lg hover:bg-theme-button-disable-hover-bg\"\n          >\n            <span className=\"group-hover:text-theme-button-disable-hover-text\">\n              {enabled ? \"Disable\" : \"Enable\"}\n            </span>\n          </button>\n          <button\n            onClick={handleDelete}\n            className=\"group text-xs font-medium text-theme-text-secondary px-2 py-1 rounded-lg hover:bg-theme-button-delete-hover-bg\"\n          >\n            <span className=\"group-hover:text-theme-button-delete-hover-text\">\n              Delete\n            </span>\n          </button>\n          <button\n            onClick={openSettingsModal}\n            className=\"text-xs font-medium text-theme-button-text hover:text-theme-text-secondary hover:bg-theme-hover px-2 py-1 rounded-lg\"\n          >\n            <DotsThreeOutline weight=\"fill\" className=\"h-5 w-5\" />\n          </button>\n        </td>\n      </tr>\n      <ModalWrapper isOpen={isSettingsOpen}>\n        <EditEmbedModal embed={embed} closeModal={closeSettingsModal} />\n      </ModalWrapper>\n      <ModalWrapper isOpen={isSnippetOpen}>\n        <CodeSnippetModal embed={embed} closeModal={closeSnippetModal} />\n      </ModalWrapper>\n    </>\n  );\n}\n\nfunction ActiveDomains({ domainList }) {\n  const domains = safeJsonParse(domainList, []);\n  if (domains.length === 0) return <p>all</p>;\n  return (\n    <div className=\"flex flex-col gap-y-2\">\n      {domains.map((domain, index) => {\n        return (\n          <p key={index} className=\"font-mono !font-normal\">\n            {domain}\n          </p>\n        );\n      })}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedConfigs/NewEmbedModal/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { X } from \"@phosphor-icons/react\";\nimport Workspace from \"@/models/workspace\";\nimport { TagsInput } from \"react-tag-input-component\";\nimport Embed from \"@/models/embed\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nexport function enforceSubmissionSchema(form) {\n  const data = {};\n  for (var [key, value] of form.entries()) {\n    if (!value || value === null) continue;\n    data[key] = value;\n    if (value === \"on\") data[key] = true;\n  }\n\n  // Always set value on nullable keys since empty or off will not send anything from form element.\n  if (!data.hasOwnProperty(\"allowlist_domains\")) data.allowlist_domains = null;\n  if (!data.hasOwnProperty(\"allow_model_override\"))\n    data.allow_model_override = false;\n  if (!data.hasOwnProperty(\"allow_temperature_override\"))\n    data.allow_temperature_override = false;\n  if (!data.hasOwnProperty(\"allow_prompt_override\"))\n    data.allow_prompt_override = false;\n  if (!data.hasOwnProperty(\"message_limit\")) data.message_limit = 20;\n  return data;\n}\n\nexport default function NewEmbedModal({ closeModal }) {\n  const [error, setError] = useState(null);\n\n  const handleCreate = async (e) => {\n    setError(null);\n    e.preventDefault();\n    const form = new FormData(e.target);\n    const data = enforceSubmissionSchema(form);\n    const { embed, error } = await Embed.newEmbed(data);\n    if (!!embed) window.location.reload();\n    setError(error);\n  };\n\n  return (\n    <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n      <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n        <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"w-full flex gap-x-2 items-center\">\n            <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n              Create new embed for workspace\n            </h3>\n          </div>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        </div>\n        <div className=\"px-7 py-6\">\n          <form onSubmit={handleCreate}>\n            <div className=\"space-y-6 max-h-[60vh] overflow-y-auto pr-2\">\n              <WorkspaceSelection />\n              <ChatModeSelection />\n              <PermittedDomains />\n              <NumberInput\n                name=\"max_chats_per_day\"\n                title=\"Max chats per day\"\n                hint=\"Limit the amount of chats this embedded chat can process in a 24 hour period. Zero is unlimited.\"\n              />\n              <NumberInput\n                name=\"max_chats_per_session\"\n                title=\"Max chats per session\"\n                hint=\"Limit the amount of chats a session user can send with this embed in a 24 hour period. Zero is unlimited.\"\n              />\n              <NumberInput\n                name=\"message_limit\"\n                title=\"Message History Limit\"\n                hint=\"The number of previous messages to include in the chat context. Default is 20.\"\n                defaultValue={20}\n              />\n              <BooleanInput\n                name=\"allow_model_override\"\n                title=\"Enable dynamic model use\"\n                hint=\"Allow setting of the preferred LLM model to override the workspace default.\"\n              />\n              <BooleanInput\n                name=\"allow_temperature_override\"\n                title=\"Enable dynamic LLM temperature\"\n                hint=\"Allow setting of the LLM temperature to override the workspace default.\"\n              />\n              <BooleanInput\n                name=\"allow_prompt_override\"\n                title=\"Enable Prompt Override\"\n                hint=\"Allow setting of the system prompt to override the workspace default.\"\n              />\n\n              {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n              <p className=\"text-white text-opacity-60 text-xs md:text-sm\">\n                After creating an embed you will be provided a link that you can\n                publish on your website with a simple\n                <code className=\"light:bg-stone-300 bg-stone-900 text-white mx-1 px-1 rounded-sm\">\n                  &lt;script&gt;\n                </code>{\" \"}\n                tag.\n              </p>\n            </div>\n            <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border\">\n              <button\n                onClick={closeModal}\n                type=\"button\"\n                className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n              >\n                Cancel\n              </button>\n              <button\n                type=\"submit\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                Create embed\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport const WorkspaceSelection = ({ defaultValue = null }) => {\n  const [workspaces, setWorkspaces] = useState([]);\n  useEffect(() => {\n    async function fetchWorkspaces() {\n      const _workspaces = await Workspace.all();\n      setWorkspaces(_workspaces);\n    }\n    fetchWorkspaces();\n  }, []);\n\n  return (\n    <div>\n      <div className=\"flex flex-col mb-2\">\n        <label\n          htmlFor=\"workspace_id\"\n          className=\"block  text-sm font-medium text-white\"\n        >\n          Workspace\n        </label>\n        <p className=\"text-theme-text-secondary text-xs\">\n          This is the workspace your chat window will be based on. All defaults\n          will be inherited from the workspace unless overridden by this config.\n        </p>\n      </div>\n      <select\n        name=\"workspace_id\"\n        required={true}\n        defaultValue={defaultValue}\n        className=\"min-w-[15rem] rounded-lg bg-theme-settings-input-bg px-4 py-2 text-sm text-white focus:ring-blue-500 focus:border-blue-500\"\n      >\n        {workspaces.map((workspace) => {\n          return (\n            <option\n              key={workspace.id}\n              selected={defaultValue === workspace.id}\n              value={workspace.id}\n            >\n              {workspace.name}\n            </option>\n          );\n        })}\n      </select>\n    </div>\n  );\n};\n\nexport const ChatModeSelection = ({ defaultValue = null }) => {\n  const [chatMode, setChatMode] = useState(defaultValue ?? \"query\");\n\n  return (\n    <div>\n      <div className=\"flex flex-col mb-2\">\n        <label\n          className=\"block text-sm font-medium text-white\"\n          htmlFor=\"chat_mode\"\n        >\n          Allowed chat method\n        </label>\n        <p className=\"text-theme-text-secondary text-xs\">\n          Set how your chatbot should operate. Query means it will only respond\n          if a document helps answer the query.\n          <br />\n          Chat opens the chat to even general questions and can answer totally\n          unrelated queries to your workspace.\n        </p>\n      </div>\n      <div className=\"mt-2 gap-y-3 flex flex-col\">\n        <label\n          className={`transition-all duration-300 w-full h-11 p-2.5 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border ${\n            chatMode === \"chat\"\n              ? \"border-theme-sidebar-item-workspace-active bg-theme-bg-secondary\"\n              : \"border-theme-sidebar-border hover:border-theme-sidebar-border hover:bg-theme-bg-secondary\"\n          } `}\n        >\n          <input\n            type=\"radio\"\n            name=\"chat_mode\"\n            value={\"chat\"}\n            checked={chatMode === \"chat\"}\n            onChange={(e) => setChatMode(e.target.value)}\n            className=\"hidden\"\n          />\n          <div\n            className={`w-4 h-4 rounded-full border-2 border-theme-sidebar-border mr-2 ${\n              chatMode === \"chat\"\n                ? \"bg-[var(--theme-sidebar-item-workspace-active)]\"\n                : \"\"\n            }`}\n          ></div>\n          <div className=\"text-theme-text-primary text-sm font-medium font-['Plus Jakarta Sans'] leading-tight\">\n            Chat: Respond to all questions regardless of context\n          </div>\n        </label>\n        <label\n          className={`transition-all duration-300 w-full h-11 p-2.5 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border ${\n            chatMode === \"query\"\n              ? \"border-theme-sidebar-item-workspace-active bg-theme-bg-secondary\"\n              : \"border-theme-sidebar-border hover:border-theme-sidebar-border hover:bg-theme-bg-secondary\"\n          } `}\n        >\n          <input\n            type=\"radio\"\n            name=\"chat_mode\"\n            value={\"query\"}\n            checked={chatMode === \"query\"}\n            onChange={(e) => setChatMode(e.target.value)}\n            className=\"hidden\"\n          />\n          <div\n            className={`w-4 h-4 rounded-full border-2 border-theme-sidebar-border mr-2 ${\n              chatMode === \"query\"\n                ? \"bg-[var(--theme-sidebar-item-workspace-active)]\"\n                : \"\"\n            }`}\n          ></div>\n          <div className=\"text-theme-text-primary text-sm font-medium font-['Plus Jakarta Sans'] leading-tight\">\n            Query: Only respond to chats related to documents in workspace\n          </div>\n        </label>\n      </div>\n    </div>\n  );\n};\n\nexport const PermittedDomains = ({ defaultValue = [] }) => {\n  const [domains, setDomains] = useState(defaultValue);\n  const handleChange = (data) => {\n    const validDomains = data\n      .map((input) => {\n        let url = input;\n        if (!url.includes(\"http://\") && !url.includes(\"https://\"))\n          url = `https://${url}`;\n        try {\n          new URL(url);\n          return url;\n        } catch {\n          return null;\n        }\n      })\n      .filter((u) => !!u);\n    setDomains(validDomains);\n  };\n\n  const handleBlur = (event) => {\n    const currentInput = event.target.value;\n    if (!currentInput) return;\n\n    const validDomains = [...domains, currentInput].map((input) => {\n      let url = input;\n      if (!url.includes(\"http://\") && !url.includes(\"https://\"))\n        url = `https://${url}`;\n      try {\n        new URL(url);\n        return url;\n      } catch {\n        return null;\n      }\n    });\n    event.target.value = \"\";\n    setDomains(validDomains);\n  };\n\n  return (\n    <div>\n      <div className=\"flex flex-col mb-2\">\n        <label\n          htmlFor=\"allowlist_domains\"\n          className=\"block text-sm font-medium text-white\"\n        >\n          Restrict requests from domains\n        </label>\n        <p className=\"text-theme-text-secondary text-xs\">\n          This filter will block any requests that come from a domain other than\n          the list below.\n          <br />\n          Leaving this empty means anyone can use your embed on any site.\n        </p>\n      </div>\n      <input type=\"hidden\" name=\"allowlist_domains\" value={domains.join(\",\")} />\n      <TagsInput\n        value={domains}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        placeholder=\"https://mysite.com, https://anythingllm.com\"\n        classNames={{\n          tag: \"bg-theme-settings-input-bg light:bg-black/10 bg-blue-300/10 text-zinc-800\",\n          input:\n            \"flex p-1 !bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none\",\n        }}\n      />\n    </div>\n  );\n};\n\nexport const NumberInput = ({ name, title, hint, defaultValue = 0 }) => {\n  return (\n    <div>\n      <div className=\"flex flex-col mb-2\">\n        <label htmlFor={name} className=\"block text-sm font-medium text-white\">\n          {title}\n        </label>\n        <p className=\"text-theme-text-secondary text-xs\">{hint}</p>\n      </div>\n      <input\n        type=\"number\"\n        name={name}\n        className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-[15rem] p-2.5\"\n        min={0}\n        defaultValue={defaultValue}\n        onScroll={(e) => e.target.blur()}\n      />\n    </div>\n  );\n};\n\nexport const BooleanInput = ({ name, title, hint, defaultValue = null }) => {\n  const [status, setStatus] = useState(defaultValue ?? false);\n\n  return (\n    <Toggle\n      name={name}\n      size=\"md\"\n      variant=\"horizontal\"\n      label={title}\n      description={hint}\n      enabled={status}\n      onChange={(checked) => setStatus(checked)}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedConfigs/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport { CodeBlock } from \"@phosphor-icons/react\";\nimport EmbedRow from \"./EmbedRow\";\nimport NewEmbedModal from \"./NewEmbedModal\";\nimport { useModal } from \"@/hooks/useModal\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport Embed from \"@/models/embed\";\nimport CTAButton from \"@/components/lib/CTAButton\";\n\nexport default function EmbedConfigsView() {\n  const { isOpen, openModal, closeModal } = useModal();\n  const { t } = useTranslation();\n  const [loading, setLoading] = useState(true);\n  const [embeds, setEmbeds] = useState([]);\n\n  useEffect(() => {\n    async function fetchUsers() {\n      const _embeds = await Embed.embeds();\n      setEmbeds(_embeds);\n      setLoading(false);\n    }\n    fetchUsers();\n  }, []);\n\n  if (loading) {\n    return (\n      <Skeleton.default\n        height=\"80vh\"\n        width=\"100%\"\n        highlightColor=\"var(--theme-bg-primary)\"\n        baseColor=\"var(--theme-bg-secondary)\"\n        count={1}\n        className=\"w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm\"\n        containerClassName=\"flex w-full\"\n      />\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col w-full p-4\">\n      <div className=\"w-full flex flex-col gap-y-1 pb-6\">\n        <div className=\"items-center flex gap-x-4\">\n          <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n            {t(\"embeddable.title\")}\n          </p>\n        </div>\n\n        <div className=\"flex gap-x-10 mr-8\">\n          <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary mt-2\">\n            {t(\"embeddable.description\")}\n          </p>\n\n          <div>\n            <CTAButton onClick={openModal} className=\"text-theme-bg-chat\">\n              <CodeBlock className=\"h-4 w-4\" weight=\"bold\" />{\" \"}\n              {t(\"embeddable.create\")}\n            </CTAButton>\n          </div>\n        </div>\n      </div>\n      <div className=\"overflow-x-auto\">\n        <table className=\"w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0\">\n          <thead className=\"text-theme-text-secondary text-xs leading-[18px] uppercase border-white/10 border-b\">\n            <tr>\n              <th scope=\"col\" className=\"px-6 py-3\">\n                {t(\"embeddable.table.workspace\")}\n              </th>\n              <th scope=\"col\" className=\"px-6 py-3\">\n                {t(\"embeddable.table.chats\")}\n              </th>\n              <th scope=\"col\" className=\"px-6 py-3\">\n                {t(\"embeddable.table.active\")}\n              </th>\n              <th scope=\"col\" className=\"px-6 py-3\">\n                {t(\"embeddable.table.created\")}\n              </th>\n              <th scope=\"col\" className=\"px-6 py-3\">\n                {\" \"}\n              </th>\n            </tr>\n          </thead>\n          <tbody>\n            {embeds.map((embed) => (\n              <EmbedRow key={embed.id} embed={embed} />\n            ))}\n          </tbody>\n        </table>\n      </div>\n      <ModalWrapper isOpen={isOpen}>\n        <NewEmbedModal closeModal={closeModal} />\n      </ModalWrapper>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/ChatEmbedWidgets/index.jsx",
    "content": "import { useState } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport { CaretLeft, CaretRight } from \"@phosphor-icons/react\";\nimport EmbedConfigsView from \"./EmbedConfigs\";\nimport EmbedChatsView from \"./EmbedChats\";\n\nexport default function ChatEmbedWidgets() {\n  const [selectedView, setSelectedView] = useState(\"configs\");\n  const [showViewModal, setShowViewModal] = useState(false);\n\n  if (isMobile) {\n    return (\n      <WidgetLayout>\n        <div className=\"flex flex-col w-full p-4 mt-10\">\n          <div\n            hidden={showViewModal}\n            className=\"flex flex-col gap-y-[18px] overflow-y-scroll no-scroll\"\n          >\n            <div className=\"text-theme-text-primary flex items-center gap-x-2\">\n              <p className=\"text-lg font-medium\">Chat Embed</p>\n            </div>\n            <WidgetList\n              selectedView={selectedView}\n              handleClick={(view) => {\n                setSelectedView(view);\n                setShowViewModal(true);\n              }}\n            />\n          </div>\n          {showViewModal && (\n            <div className=\"fixed top-0 left-0 w-full h-full bg-sidebar z-30\">\n              <div className=\"flex flex-col h-full\">\n                <div className=\"flex items-center p-4\">\n                  <button\n                    type=\"button\"\n                    onClick={() => {\n                      setShowViewModal(false);\n                      setSelectedView(\"\");\n                    }}\n                    className=\"text-white/60 hover:text-white transition-colors duration-200\"\n                  >\n                    <div className=\"flex items-center text-sky-400\">\n                      <CaretLeft size={24} />\n                      <div>Back</div>\n                    </div>\n                  </button>\n                </div>\n                <div className=\"flex-1 overflow-y-auto p-4\">\n                  <div className=\"bg-theme-bg-secondary text-white rounded-xl p-4 overflow-y-scroll no-scroll\">\n                    {selectedView === \"configs\" ? (\n                      <EmbedConfigsView />\n                    ) : (\n                      <EmbedChatsView />\n                    )}\n                  </div>\n                </div>\n              </div>\n            </div>\n          )}\n        </div>\n      </WidgetLayout>\n    );\n  }\n\n  return (\n    <WidgetLayout>\n      <div className=\"flex-1 flex gap-x-6 p-4 mt-10\">\n        <div className=\"flex flex-col min-w-[360px] h-[calc(100vh-90px)]\">\n          <div className=\"flex-none mb-4\">\n            <div className=\"text-theme-text-primary flex items-center gap-x-2\">\n              <p className=\"text-lg font-medium\">Chat Embed</p>\n            </div>\n          </div>\n\n          <div className=\"flex-1 overflow-y-auto pr-2 pb-4\">\n            <div className=\"space-y-4\">\n              <WidgetList\n                selectedView={selectedView}\n                handleClick={setSelectedView}\n              />\n            </div>\n          </div>\n        </div>\n        <div className=\"flex-[2] flex flex-col gap-y-[18px] mt-10\">\n          <div className=\"bg-theme-bg-secondary text-white rounded-xl flex-1 p-4 overflow-y-scroll no-scroll\">\n            {selectedView === \"configs\" ? (\n              <EmbedConfigsView />\n            ) : (\n              <EmbedChatsView />\n            )}\n          </div>\n        </div>\n      </div>\n    </WidgetLayout>\n  );\n}\n\nfunction WidgetLayout({ children }) {\n  return (\n    <div\n      id=\"workspace-widget-settings-container\"\n      className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex md:mt-0 mt-6\"\n    >\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] w-full h-full flex\"\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n\nfunction WidgetList({ selectedView, handleClick }) {\n  const views = {\n    configs: {\n      title: \"Widgets\",\n    },\n    chats: {\n      title: \"History\",\n    },\n  };\n\n  return (\n    <div\n      className={`bg-theme-bg-secondary text-white rounded-xl ${isMobile ? \"w-full\" : \"min-w-[360px] w-fit\"}`}\n    >\n      {Object.entries(views).map(([view, settings], index) => (\n        <div\n          key={view}\n          className={`py-3 px-4 flex items-center justify-between ${\n            index === 0 ? \"rounded-t-xl\" : \"\"\n          } ${\n            index === Object.keys(views).length - 1\n              ? \"rounded-b-xl\"\n              : \"border-b border-white/10\"\n          } cursor-pointer transition-all duration-300 hover:bg-theme-bg-primary ${\n            selectedView === view ? \"bg-white/10 light:bg-theme-bg-sidebar\" : \"\"\n          }`}\n          onClick={() => handleClick?.(view)}\n        >\n          <div className=\"text-sm font-light\">{settings.title}</div>\n          <CaretRight\n            size={14}\n            weight=\"bold\"\n            className=\"text-theme-text-secondary\"\n          />\n        </div>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx",
    "content": "import truncate from \"truncate\";\nimport { X, Trash } from \"@phosphor-icons/react\";\nimport System from \"@/models/system\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { useModal } from \"@/hooks/useModal\";\nimport MarkdownRenderer from \"../MarkdownRenderer\";\nimport { safeJsonParse } from \"@/utils/request\";\n\nexport default function ChatRow({ chat, onDelete }) {\n  const {\n    isOpen: isPromptOpen,\n    openModal: openPromptModal,\n    closeModal: closePromptModal,\n  } = useModal();\n  const {\n    isOpen: isResponseOpen,\n    openModal: openResponseModal,\n    closeModal: closeResponseModal,\n  } = useModal();\n\n  const handleDelete = async () => {\n    if (\n      !window.confirm(\n        `Are you sure you want to delete this chat?\\n\\nThis action is irreversible.`\n      )\n    )\n      return false;\n    await System.deleteChat(chat.id);\n    onDelete(chat.id);\n  };\n\n  return (\n    <>\n      <tr className=\"bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10\">\n        <td className=\"px-6 font-medium whitespace-nowrap text-white\">\n          {chat.id}\n        </td>\n        <td className=\"px-6 font-medium whitespace-nowrap text-white\">\n          {chat.user?.username}\n        </td>\n        <td className=\"px-6\">{chat.workspace?.name}</td>\n        <td\n          onClick={openPromptModal}\n          className=\"px-6 border-transparent cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg\"\n        >\n          {truncate(chat.prompt, 40)}\n        </td>\n        <td\n          onClick={openResponseModal}\n          className=\"px-6 cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg\"\n        >\n          {truncate(safeJsonParse(chat.response, {})?.text, 40)}\n        </td>\n        <td className=\"px-6\">{chat.createdAt}</td>\n        <td className=\"px-6 flex items-center gap-x-6 h-full mt-1\">\n          <button\n            onClick={handleDelete}\n            className=\"text-xs font-medium text-white/80 light:text-black/80 hover:light:text-red-500 hover:text-red-300 rounded-lg px-2 py-1 hover:bg-white hover:light:bg-red-50 hover:bg-opacity-10\"\n          >\n            <Trash className=\"h-5 w-5\" />\n          </button>\n        </td>\n      </tr>\n      <ModalWrapper isOpen={isPromptOpen}>\n        <TextPreview text={chat.prompt} closeModal={closePromptModal} />\n      </ModalWrapper>\n      <ModalWrapper isOpen={isResponseOpen}>\n        <TextPreview\n          text={\n            <MarkdownRenderer\n              content={safeJsonParse(chat.response, {})?.text}\n            />\n          }\n          closeModal={closeResponseModal}\n        />\n      </ModalWrapper>\n    </>\n  );\n}\nconst TextPreview = ({ text, closeModal }) => {\n  return (\n    <div className=\"relative w-full md:max-w-2xl max-h-full\">\n      <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden\">\n        <div className=\"flex items-center justify-between p-6 border-b rounded-t border-theme-modal-border\">\n          <h3 className=\"text-xl font-semibold text-white\">Viewing Text</h3>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X className=\"text-white text-lg\" />\n          </button>\n        </div>\n        <div className=\"w-full p-6\">\n          <pre className=\"w-full h-[200px] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-900 light:bg-theme-bg-secondary border border-gray-500 text-white text-sm\">\n            {text}\n          </pre>\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Chats/MarkdownRenderer.jsx",
    "content": "import { useState } from \"react\";\nimport MarkdownIt from \"markdown-it\";\nimport hljs from \"highlight.js\";\nimport { CaretDown } from \"@phosphor-icons/react\";\nimport \"highlight.js/styles/github-dark.css\";\nimport DOMPurify from \"@/utils/chat/purify\";\n\nconst md = new MarkdownIt({\n  html: true,\n  breaks: true,\n  highlight: function (str, lang) {\n    if (lang && hljs.getLanguage(lang)) {\n      try {\n        return hljs.highlight(str, { language: lang }).value;\n      } catch {}\n    }\n    return \"\"; // use external default escaping\n  },\n});\n\nconst ThoughtBubble = ({ thought }) => {\n  const [isExpanded, setIsExpanded] = useState(false);\n\n  if (!thought) return null;\n\n  const cleanThought = thought.replace(/<\\/?think>/g, \"\").trim();\n  if (!cleanThought) return null;\n\n  return (\n    <div className=\"mb-3\">\n      <div\n        onClick={() => setIsExpanded(!isExpanded)}\n        className=\"cursor-pointer flex items-center gap-x-2 text-theme-text-secondary hover:text-theme-text-primary transition-colors mb-2\"\n      >\n        <CaretDown\n          size={14}\n          weight=\"bold\"\n          className={`transition-transform ${isExpanded ? \"rotate-180\" : \"\"}`}\n        />\n        <span className=\"text-xs font-medium\">View thoughts</span>\n      </div>\n      {isExpanded && (\n        <div className=\"bg-theme-bg-chat-input rounded-md p-3 border-l-2 border-theme-text-secondary/30\">\n          <div className=\"text-xs text-theme-text-secondary font-mono whitespace-pre-wrap\">\n            {cleanThought}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nfunction parseContent(content) {\n  const parts = [];\n  let lastIndex = 0;\n  content.replace(/<think>([^]*?)<\\/think>/g, (match, thinkContent, offset) => {\n    if (offset > lastIndex) {\n      parts.push({ type: \"normal\", text: content.slice(lastIndex, offset) });\n    }\n    parts.push({ type: \"think\", text: thinkContent });\n    lastIndex = offset + match.length;\n  });\n  if (lastIndex < content.length) {\n    parts.push({ type: \"normal\", text: content.slice(lastIndex) });\n  }\n  return parts;\n}\n\nexport default function MarkdownRenderer({ content }) {\n  if (!content) return null;\n\n  const parts = parseContent(content);\n  return (\n    <div className=\"whitespace-normal\">\n      {parts.map((part, index) => {\n        const html = md.render(part.text);\n        if (part.type === \"think\")\n          return <ThoughtBubble key={index} thought={part.text} />;\n        return (\n          <div\n            key={index}\n            dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}\n          />\n        );\n      })}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Chats/index.jsx",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport useQuery from \"@/hooks/useQuery\";\nimport ChatRow from \"./ChatRow\";\nimport showToast from \"@/utils/toast\";\nimport System from \"@/models/system\";\nimport { CaretDown, Download, Trash } from \"@phosphor-icons/react\";\nimport { saveAs } from \"file-saver\";\nimport { useTranslation } from \"react-i18next\";\nimport { CanViewChatHistory } from \"@/components/CanViewChatHistory\";\n\nconst exportOptions = {\n  csv: {\n    name: \"CSV\",\n    mimeType: \"text/csv\",\n    fileExtension: \"csv\",\n    filenameFunc: () => {\n      return `anythingllm-chats-${new Date().toLocaleDateString()}`;\n    },\n  },\n  json: {\n    name: \"JSON\",\n    mimeType: \"application/json\",\n    fileExtension: \"json\",\n    filenameFunc: () => {\n      return `anythingllm-chats-${new Date().toLocaleDateString()}`;\n    },\n  },\n  jsonl: {\n    name: \"JSONL\",\n    mimeType: \"application/jsonl\",\n    fileExtension: \"jsonl\",\n    filenameFunc: () => {\n      return `anythingllm-chats-${new Date().toLocaleDateString()}-lines`;\n    },\n  },\n  jsonAlpaca: {\n    name: \"JSON (Alpaca)\",\n    mimeType: \"application/json\",\n    fileExtension: \"json\",\n    filenameFunc: () => {\n      return `anythingllm-chats-${new Date().toLocaleDateString()}-alpaca`;\n    },\n  },\n};\n\nexport default function WorkspaceChats() {\n  const [showMenu, setShowMenu] = useState(false);\n  const menuRef = useRef();\n  const openMenuButton = useRef();\n  const query = useQuery();\n  const [loading, setLoading] = useState(true);\n  const [chats, setChats] = useState([]);\n  const [offset, setOffset] = useState(Number(query.get(\"offset\") || 0));\n  const [canNext, setCanNext] = useState(false);\n  const { t } = useTranslation();\n\n  const handleDumpChats = async (exportType) => {\n    const chats = await System.exportChats(exportType, \"workspace\");\n    if (!!chats) {\n      const { name, mimeType, fileExtension, filenameFunc } =\n        exportOptions[exportType];\n      const blob = new Blob([chats], { type: mimeType });\n      saveAs(blob, `${filenameFunc()}.${fileExtension}`);\n      showToast(`Chats exported successfully as ${name}.`, \"success\");\n    } else {\n      showToast(\"Failed to export chats.\", \"error\");\n    }\n  };\n\n  const handleClearAllChats = async () => {\n    if (\n      !window.confirm(\n        `Are you sure you want to clear all chats?\\n\\nThis action is irreversible.`\n      )\n    )\n      return false;\n    await System.deleteChat(-1);\n    setChats([]);\n    showToast(\"Cleared all chats.\", \"success\");\n  };\n\n  const toggleMenu = () => {\n    setShowMenu(!showMenu);\n  };\n\n  useEffect(() => {\n    function handleClickOutside(event) {\n      if (\n        menuRef.current &&\n        !menuRef.current.contains(event.target) &&\n        !openMenuButton.current.contains(event.target)\n      ) {\n        setShowMenu(false);\n      }\n    }\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, []);\n\n  useEffect(() => {\n    async function fetchChats() {\n      const { chats: _chats = [], hasPages = false } =\n        await System.chats(offset);\n      setChats(_chats);\n      setCanNext(hasPages);\n      setLoading(false);\n    }\n    fetchChats();\n  }, [offset]);\n\n  return (\n    <CanViewChatHistory>\n      <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n        <Sidebar />\n        <div\n          style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n          className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n        >\n          <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n            <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2\">\n              <div className=\"flex flex-wrap gap-4 items-center\">\n                <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                  {t(\"recorded.title\")}\n                </p>\n                <div className=\"relative\">\n                  <button\n                    ref={openMenuButton}\n                    onClick={toggleMenu}\n                    className=\"flex items-center gap-x-2 px-4 py-1 rounded-lg bg-primary-button hover:light:bg-theme-bg-primary hover:text-theme-text-primary text-xs font-semibold hover:bg-secondary shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit\"\n                  >\n                    <Download size={18} weight=\"bold\" />\n                    {t(\"recorded.export\")}\n                    <CaretDown size={18} weight=\"bold\" />\n                  </button>\n                  <div\n                    ref={menuRef}\n                    className={`${\n                      showMenu ? \"slide-down\" : \"slide-up hidden\"\n                    } z-20 w-fit rounded-lg absolute top-full right-0 bg-secondary light:bg-theme-bg-secondary mt-2 shadow-md`}\n                  >\n                    <div className=\"py-2\">\n                      {Object.entries(exportOptions).map(([key, data]) => (\n                        <button\n                          key={key}\n                          onClick={() => {\n                            handleDumpChats(key);\n                            setShowMenu(false);\n                          }}\n                          className=\"w-full text-left px-4 py-2 text-white text-sm hover:bg-[#3D4147] light:hover:bg-theme-sidebar-item-hover\"\n                        >\n                          {data.name}\n                        </button>\n                      ))}\n                    </div>\n                  </div>\n                </div>\n                {chats.length > 0 && (\n                  <button\n                    onClick={handleClearAllChats}\n                    className=\"flex items-center gap-x-2 px-4 py-1 border hover:border-transparent light:border-theme-sidebar-border border-white/40 text-white/40 light:text-theme-text-secondary rounded-lg bg-transparent hover:light:text-theme-bg-primary hover:text-theme-text-primary text-xs font-semibold hover:bg-red-500 shadow-[0_4px_14px_rgba(0,0,0,0.25)] h-[34px] w-fit\"\n                  >\n                    <Trash size={18} weight=\"bold\" />\n                    Clear Chats\n                  </button>\n                )}\n              </div>\n              <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary mt-2\">\n                {t(\"recorded.description\")}\n              </p>\n            </div>\n            <div className=\"overflow-x-auto\">\n              <ChatsContainer\n                loading={loading}\n                chats={chats}\n                setChats={setChats}\n                offset={offset}\n                setOffset={setOffset}\n                canNext={canNext}\n                t={t}\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n    </CanViewChatHistory>\n  );\n}\n\nfunction ChatsContainer({\n  loading,\n  chats,\n  setChats,\n  offset,\n  setOffset,\n  canNext,\n  t,\n}) {\n  const handlePrevious = () => {\n    setOffset(Math.max(offset - 1, 0));\n  };\n  const handleNext = () => {\n    setOffset(offset + 1);\n  };\n\n  const handleDeleteChat = async (chatId) => {\n    await System.deleteChat(chatId);\n    setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatId));\n  };\n\n  if (loading) {\n    return (\n      <Skeleton.default\n        height=\"80vh\"\n        width=\"100%\"\n        highlightColor=\"var(--theme-bg-primary)\"\n        baseColor=\"var(--theme-bg-secondary)\"\n        count={1}\n        className=\"w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm\"\n        containerClassName=\"flex w-full\"\n      />\n    );\n  }\n\n  return (\n    <>\n      <table className=\"w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0\">\n        <thead className=\"text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b\">\n          <tr>\n            <th scope=\"col\" className=\"px-6 py-3 rounded-tl-lg\">\n              {t(\"recorded.table.id\")}\n            </th>\n            <th scope=\"col\" className=\"px-6 py-3\">\n              {t(\"recorded.table.by\")}\n            </th>\n            <th scope=\"col\" className=\"px-6 py-3\">\n              {t(\"recorded.table.workspace\")}\n            </th>\n            <th scope=\"col\" className=\"px-6 py-3\">\n              {t(\"recorded.table.prompt\")}\n            </th>\n            <th scope=\"col\" className=\"px-6 py-3\">\n              {t(\"recorded.table.response\")}\n            </th>\n            <th scope=\"col\" className=\"px-6 py-3\">\n              {t(\"recorded.table.at\")}\n            </th>\n            <th scope=\"col\" className=\"px-6 py-3 rounded-tr-lg\">\n              {\" \"}\n            </th>\n          </tr>\n        </thead>\n        <tbody>\n          {!!chats &&\n            chats.map((chat) => (\n              <ChatRow key={chat.id} chat={chat} onDelete={handleDeleteChat} />\n            ))}\n        </tbody>\n      </table>\n      <div className=\"flex w-full justify-between items-center mt-6\">\n        <button\n          onClick={handlePrevious}\n          className=\"px-4 py-2 rounded-lg border border-theme-text-secondary text-theme-text-secondary text-sm items-center flex gap-x-2 hover:bg-theme-text-secondary hover:text-theme-bg-secondary disabled:invisible\"\n          disabled={offset === 0}\n        >\n          {\" \"}\n          Previous Page\n        </button>\n        <button\n          onClick={handleNext}\n          className=\"px-4 py-2 rounded-lg border border-slate-200 text-slate-200 light:text-theme-text-secondary light:border-theme-sidebar-border text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:invisible\"\n          disabled={!canNext}\n        >\n          Next Page\n        </button>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/Authentication/UserItems/index.jsx",
    "content": "import paths from \"@/utils/paths\";\nimport HubItemCard from \"../../Trending/HubItems/HubItemCard\";\nimport { useUserItems } from \"../useUserItems\";\nimport { HubItemCardSkeleton } from \"../../Trending/HubItems\";\nimport { readableType } from \"../../utils\";\n\nexport default function UserItems({ connectionKey }) {\n  const { loading, userItems } = useUserItems({ connectionKey });\n  const { createdByMe = {}, teamItems = [] } = userItems || {};\n\n  if (loading) return <HubItemCardSkeleton />;\n  const hasItems = (items) => {\n    return Object.values(items).some((category) => category?.items?.length > 0);\n  };\n\n  return (\n    <div className=\"flex flex-col gap-y-8\">\n      {/* Created By Me Section */}\n      <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10\">\n        <div className=\"flex items-center justify-between\">\n          <p className=\"text-lg leading-6 font-bold text-white\">\n            Created by me\n          </p>\n          <a\n            href={paths.communityHub.noPrivateItems()}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"text-primary-button hover:text-primary-button/80 text-sm\"\n          >\n            Why can't I see my private items?\n          </a>\n        </div>\n        <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n          Items you have created and shared publicly on the AnythingLLM\n          Community Hub.\n        </p>\n        <div className=\"flex flex-col gap-4 mt-4\">\n          {Object.keys(createdByMe).map((type) => {\n            if (!createdByMe[type]?.items?.length) return null;\n            return (\n              <div key={type} className=\"rounded-lg w-full\">\n                <h3 className=\"text-white capitalize font-medium mb-3\">\n                  {readableType(type)}\n                </h3>\n                <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2\">\n                  {createdByMe[type].items.map((item) => (\n                    <HubItemCard key={item.id} type={type} item={item} />\n                  ))}\n                </div>\n              </div>\n            );\n          })}\n          {!hasItems(createdByMe) && (\n            <p className=\"text-white/60 text-xs text-center mt-4\">\n              You haven&apos;t created any items yet.\n            </p>\n          )}\n        </div>\n      </div>\n\n      {/* Team Items Section */}\n      <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10\">\n        <div className=\"items-center\">\n          <p className=\"text-lg leading-6 font-bold text-white\">\n            Items by team\n          </p>\n        </div>\n        <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n          Public and private items shared with teams you belong to.\n        </p>\n        <div className=\"flex flex-col gap-4 mt-4\">\n          {teamItems.map((team) => (\n            <div key={team.teamId} className=\"flex flex-col gap-y-4\">\n              <h3 className=\"text-white text-sm font-medium\">\n                {team.teamName}\n              </h3>\n              {Object.keys(team.items).map((type) => {\n                if (!team.items[type]?.items?.length) return null;\n                return (\n                  <div key={type} className=\"rounded-lg w-full\">\n                    <h3 className=\"text-white capitalize font-medium mb-3\">\n                      {readableType(type)}\n                    </h3>\n                    <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2\">\n                      {team.items[type].items.map((item) => (\n                        <HubItemCard key={item.id} type={type} item={item} />\n                      ))}\n                    </div>\n                  </div>\n                );\n              })}\n              {!hasItems(team.items) && (\n                <p className=\"text-white/60 text-xs text-center mt-4\">\n                  No items shared with this team yet.\n                </p>\n              )}\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/Authentication/index.jsx",
    "content": "import Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport { useEffect, useState } from \"react\";\nimport CommunityHub from \"@/models/communityHub\";\nimport ContextualSaveBar from \"@/components/ContextualSaveBar\";\nimport showToast from \"@/utils/toast\";\nimport { FullScreenLoader } from \"@/components/Preloader\";\nimport paths from \"@/utils/paths\";\nimport { Info } from \"@phosphor-icons/react\";\nimport UserItems from \"./UserItems\";\n\nfunction useCommunityHubAuthentication() {\n  const [originalConnectionKey, setOriginalConnectionKey] = useState(\"\");\n  const [hasChanges, setHasChanges] = useState(false);\n  const [connectionKey, setConnectionKey] = useState(\"\");\n  const [loading, setLoading] = useState(true);\n\n  async function resetChanges() {\n    setConnectionKey(originalConnectionKey);\n    setHasChanges(false);\n  }\n\n  async function onConnectionKeyChange(e) {\n    const newConnectionKey = e.target.value;\n    setConnectionKey(newConnectionKey);\n    setHasChanges(true);\n  }\n\n  async function updateConnectionKey() {\n    if (connectionKey === originalConnectionKey) return;\n    setLoading(true);\n    try {\n      const response = await CommunityHub.updateSettings({\n        hub_api_key: connectionKey,\n      });\n      if (!response.success)\n        return showToast(\"Failed to save API key\", \"error\");\n      setHasChanges(false);\n      showToast(\"API key saved successfully\", \"success\");\n      setOriginalConnectionKey(connectionKey);\n    } catch (error) {\n      console.error(error);\n      showToast(\"Failed to save API key\", \"error\");\n    } finally {\n      setLoading(false);\n    }\n  }\n\n  async function disconnectHub() {\n    setLoading(true);\n    try {\n      const response = await CommunityHub.updateSettings({\n        hub_api_key: \"\",\n      });\n      if (!response.success)\n        return showToast(\"Failed to disconnect from hub\", \"error\");\n      setHasChanges(false);\n      showToast(\"Disconnected from AnythingLLM Community Hub\", \"success\");\n      setOriginalConnectionKey(\"\");\n      setConnectionKey(\"\");\n    } catch (error) {\n      console.error(error);\n      showToast(\"Failed to disconnect from hub\", \"error\");\n    } finally {\n      setLoading(false);\n    }\n  }\n\n  useEffect(() => {\n    const fetchData = async () => {\n      setLoading(true);\n      try {\n        const { connectionKey } = await CommunityHub.getSettings();\n        setOriginalConnectionKey(connectionKey || \"\");\n        setConnectionKey(connectionKey || \"\");\n      } catch (error) {\n        console.error(\"Error fetching data:\", error);\n      } finally {\n        setLoading(false);\n      }\n    };\n    fetchData();\n  }, []);\n\n  return {\n    connectionKey,\n    originalConnectionKey,\n    loading,\n    onConnectionKeyChange,\n    updateConnectionKey,\n    hasChanges,\n    resetChanges,\n    disconnectHub,\n  };\n}\n\nexport default function CommunityHubAuthentication() {\n  const {\n    connectionKey,\n    originalConnectionKey,\n    loading,\n    onConnectionKeyChange,\n    updateConnectionKey,\n    hasChanges,\n    resetChanges,\n    disconnectHub,\n  } = useCommunityHubAuthentication();\n  if (loading) return <FullScreenLoader />;\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <ContextualSaveBar\n        showing={hasChanges}\n        onSave={updateConnectionKey}\n        onCancel={resetChanges}\n      />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n            <div className=\"items-center\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                Your AnythingLLM Community Hub Account\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary\">\n              Connecting your AnythingLLM Community Hub account allows you to\n              access your <b>private</b> AnythingLLM Community Hub items as well\n              as upload your own items to the AnythingLLM Community Hub.\n            </p>\n          </div>\n\n          {!connectionKey && (\n            <div className=\"border border-theme-border my-2 flex flex-col md:flex-row md:items-center gap-x-2 text-theme-text-primary mb-4 bg-theme-settings-input-bg w-1/2 rounded-lg px-4 py-2\">\n              <div className=\"flex flex-col gap-y-2\">\n                <div className=\"gap-x-2 flex items-center\">\n                  <Info size={25} />\n                  <h1 className=\"text-lg font-semibold\">\n                    Why connect my AnythingLLM Community Hub account?\n                  </h1>\n                </div>\n                <p className=\"text-sm text-theme-text-secondary\">\n                  Connecting your AnythingLLM Community Hub account allows you\n                  to pull in your <b>private</b> items from the AnythingLLM\n                  Community Hub as well as upload your own items to the\n                  AnythingLLM Community Hub.\n                  <br />\n                  <br />\n                  <i>\n                    You do not need to connect your AnythingLLM Community Hub\n                    account to pull in public items from the AnythingLLM\n                    Community Hub.\n                  </i>\n                </p>\n              </div>\n            </div>\n          )}\n\n          {/* API Key Section */}\n          <div className=\"mt-6 mb-12\">\n            <div className=\"flex flex-col w-full max-w-[400px]\">\n              <label className=\"text-theme-text-primary text-sm font-semibold block mb-2\">\n                AnythingLLM Hub API Key\n              </label>\n              <input\n                type=\"password\"\n                value={connectionKey || \"\"}\n                onChange={onConnectionKeyChange}\n                className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                placeholder=\"Enter your AnythingLLM Hub API key\"\n              />\n              <div className=\"flex items-center justify-between mt-2\">\n                <p className=\"text-theme-text-secondary text-xs\">\n                  You can get your API key from your{\" \"}\n                  <a\n                    href={paths.communityHub.profile()}\n                    className=\"underline text-primary-button\"\n                  >\n                    AnythingLLM Community Hub profile page\n                  </a>\n                  .\n                </p>\n                {!!originalConnectionKey && (\n                  <button\n                    onClick={disconnectHub}\n                    className=\"border-none text-red-500 hover:text-red-600 text-sm font-medium transition-colors duration-200\"\n                  >\n                    Disconnect\n                  </button>\n                )}\n              </div>\n            </div>\n          </div>\n\n          {!!originalConnectionKey && (\n            <div className=\"mt-6\">\n              <UserItems connectionKey={originalConnectionKey} />\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/Authentication/useUserItems.js",
    "content": "import { useState, useEffect } from \"react\";\nimport CommunityHub from \"@/models/communityHub\";\n\nconst DEFAULT_USER_ITEMS = {\n  createdByMe: {\n    agentSkills: { items: [] },\n    systemPrompts: { items: [] },\n    slashCommands: { items: [] },\n    agentFlows: { items: [] },\n  },\n  teamItems: [],\n};\n\nexport function useUserItems({ connectionKey }) {\n  const [loading, setLoading] = useState(true);\n  const [userItems, setUserItems] = useState(DEFAULT_USER_ITEMS);\n\n  useEffect(() => {\n    const fetchData = async () => {\n      console.log(\"fetching user items\", connectionKey);\n      if (!connectionKey) return;\n      setLoading(true);\n      try {\n        const { success, createdByMe, teamItems } =\n          await CommunityHub.fetchUserItems();\n        if (success) {\n          setUserItems({ createdByMe, teamItems });\n        }\n      } catch (error) {\n        console.error(\"Error fetching user items:\", error);\n      } finally {\n        setLoading(false);\n      }\n    };\n\n    fetchData();\n  }, [connectionKey]);\n\n  return { loading, userItems };\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/Completed/index.jsx",
    "content": "import CommunityHubImportItemSteps from \"..\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport { Link } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\n\nexport default function Completed({ settings, setSettings, setStep }) {\n  return (\n    <div className=\"flex-[2] flex flex-col gap-y-[18px] mt-10\">\n      <div className=\"bg-theme-bg-secondary rounded-xl flex-1 p-6\">\n        <div className=\"w-full flex flex-col gap-y-2 max-w-[700px]\">\n          <h2 className=\"text-base text-theme-text-primary font-semibold\">\n            Community Hub Item Imported\n          </h2>\n          <div className=\"flex flex-col gap-y-[25px] text-theme-text-secondary text-sm\">\n            <p>\n              The \"{settings.item.name}\" {settings.item.itemType} has been\n              imported successfully! It is now available in your AnythingLLM\n              instance.\n            </p>\n            {settings.item.itemType === \"agent-flow\" && (\n              <Link\n                to={paths.settings.agentSkills()}\n                className=\"text-theme-text-primary hover:text-blue-500 hover:underline\"\n              >\n                View \"{settings.item.name}\" in Agent Skills\n              </Link>\n            )}\n            <p>\n              Any changes you make to this {settings.item.itemType} will not be\n              reflected in the community hub. You can now modify as needed.\n            </p>\n          </div>\n          <CTAButton\n            className=\"text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent\"\n            onClick={() => {\n              setSettings({ item: null, itemId: null });\n              setStep(CommunityHubImportItemSteps.itemId.key);\n            }}\n          >\n            Import another item\n          </CTAButton>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/Introduction/index.jsx",
    "content": "import CommunityHubImportItemSteps from \"..\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport paths from \"@/utils/paths\";\nimport showToast from \"@/utils/toast\";\nimport { useState } from \"react\";\n\nexport default function Introduction({ settings, setSettings, setStep }) {\n  const [itemId, setItemId] = useState(settings.itemId);\n  const handleContinue = () => {\n    if (!itemId) return showToast(\"Please enter an item ID\", \"error\");\n    setSettings((prev) => ({ ...prev, itemId }));\n    setStep(CommunityHubImportItemSteps.itemId.next());\n  };\n\n  return (\n    <div className=\"flex-[2] flex flex-col gap-y-[18px] mt-10\">\n      <div className=\"bg-theme-bg-secondary rounded-xl flex-1 p-6\">\n        <div className=\"w-full flex flex-col gap-y-2 max-w-[700px]\">\n          <h2 className=\"text-base text-theme-text-primary font-semibold\">\n            Import an item from the community hub\n          </h2>\n          <div className=\"flex flex-col gap-y-[25px] text-theme-text-secondary text-sm\">\n            <p>\n              The community hub is a place where you can find, share, and import\n              agent-skills, system prompts, slash commands, and more!\n            </p>\n            <p>\n              These items are created by the AnythingLLM team and community, and\n              are a great way to get started with AnythingLLM as well as extend\n              AnythingLLM in a way that is customized to your needs.\n            </p>\n            <p>\n              There are both <b>private</b> and <b>public</b> items in the\n              community hub. Private items are only visible to you, while public\n              items are visible to everyone.\n            </p>\n\n            <p className=\"p-4 bg-yellow-800/30 light:bg-orange-100 light:text-orange-500 light:border-orange-500 rounded-lg border border-yellow-500 text-yellow-500\">\n              If you are pulling in a private item, make sure it is{\" \"}\n              <b>shared with a team</b> you belong to, and you have added a{\" \"}\n              <a\n                href={paths.communityHub.authentication()}\n                className=\"underline text-yellow-100 light:text-orange-500 font-semibold\"\n              >\n                Connection Key.\n              </a>\n            </p>\n          </div>\n\n          <div className=\"flex flex-col gap-y-2 mt-4\">\n            <div className=\"w-full flex flex-col gap-y-4\">\n              <div className=\"flex flex-col w-full\">\n                <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n                  Community Hub Item Import ID\n                </label>\n                <input\n                  type=\"text\"\n                  value={itemId}\n                  onChange={(e) => setItemId(e.target.value)}\n                  placeholder=\"allm-community-id:agent-skill:1234567890\"\n                  className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                />\n              </div>\n            </div>\n          </div>\n          <CTAButton\n            className=\"text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent\"\n            onClick={handleContinue}\n          >\n            Continue with import &rarr;\n          </CTAButton>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/HubItem/AgentFlow.jsx",
    "content": "import CTAButton from \"@/components/lib/CTAButton\";\nimport CommunityHubImportItemSteps from \"../..\";\nimport showToast from \"@/utils/toast\";\nimport paths from \"@/utils/paths\";\nimport { CircleNotch } from \"@phosphor-icons/react\";\nimport { useState } from \"react\";\nimport AgentFlows from \"@/models/agentFlows\";\nimport { safeJsonParse } from \"@/utils/request\";\n\nexport default function AgentFlow({ item, setStep }) {\n  const flowInfo = safeJsonParse(item.flow, { steps: [] });\n  const [loading, setLoading] = useState(false);\n\n  async function importAgentFlow() {\n    try {\n      setLoading(true);\n      const { success, error, flow } = await AgentFlows.saveFlow(\n        item.name,\n        flowInfo\n      );\n      if (!success) throw new Error(error);\n      if (!!flow?.uuid) await AgentFlows.toggleFlow(flow.uuid, true); // Enable the flow automatically after import\n\n      showToast(`Agent flow imported successfully!`, \"success\");\n      setStep(CommunityHubImportItemSteps.completed.key);\n    } catch (e) {\n      console.error(e);\n      showToast(`Failed to import agent flow. ${e.message}`, \"error\");\n    } finally {\n      setLoading(false);\n    }\n  }\n\n  return (\n    <div className=\"flex flex-col mt-4 gap-y-4\">\n      <div className=\"flex flex-col gap-y-1\">\n        <h2 className=\"text-base text-theme-text-primary font-semibold\">\n          Import Agent Flow &quot;{item.name}&quot;\n        </h2>\n        {item.creatorUsername && (\n          <p className=\"text-white/60 light:text-theme-text-secondary text-xs font-mono\">\n            Created by{\" \"}\n            <a\n              href={paths.communityHub.profile(item.creatorUsername)}\n              target=\"_blank\"\n              className=\"hover:text-blue-500 hover:underline\"\n              rel=\"noreferrer\"\n            >\n              @{item.creatorUsername}\n            </a>\n          </p>\n        )}\n      </div>\n      <div className=\"flex flex-col gap-y-[25px] text-white/80 light:text-theme-text-secondary text-sm\">\n        <p>\n          Agent flows allow you to create reusable sequences of actions that can\n          be triggered by your agent.\n        </p>\n        <div className=\"flex flex-col gap-y-2\">\n          <p className=\"font-semibold\">Flow Details:</p>\n          <p>Description: {item.description}</p>\n          <p className=\"font-semibold\">Steps ({flowInfo.steps.length}):</p>\n          <ul className=\"list-disc pl-6\">\n            {flowInfo.steps.map((step, index) => (\n              <li key={index}>{step.type}</li>\n            ))}\n          </ul>\n        </div>\n      </div>\n      <CTAButton\n        disabled={loading}\n        className=\"text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent\"\n        onClick={importAgentFlow}\n      >\n        {loading ? <CircleNotch size={16} className=\"animate-spin\" /> : null}\n        {loading ? \"Importing...\" : \"Import agent flow\"}\n      </CTAButton>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/HubItem/AgentSkill.jsx",
    "content": "import CTAButton from \"@/components/lib/CTAButton\";\nimport CommunityHubImportItemSteps from \"../..\";\nimport showToast from \"@/utils/toast\";\nimport paths from \"@/utils/paths\";\nimport {\n  CaretLeft,\n  CaretRight,\n  CircleNotch,\n  Warning,\n} from \"@phosphor-icons/react\";\nimport { useEffect, useState } from \"react\";\nimport renderMarkdown from \"@/utils/chat/markdown\";\nimport DOMPurify from \"dompurify\";\nimport CommunityHub from \"@/models/communityHub\";\nimport { setEventDelegatorForCodeSnippets } from \"@/components/WorkspaceChat\";\n\nexport default function AgentSkill({ item, settings, setStep }) {\n  const [loading, setLoading] = useState(false);\n  async function importAgentSkill() {\n    try {\n      setLoading(true);\n      const { error } = await CommunityHub.importBundleItem(settings.itemId);\n      if (error) throw new Error(error);\n      showToast(`Agent skill imported successfully!`, \"success\");\n      setStep(CommunityHubImportItemSteps.completed.key);\n    } catch (e) {\n      console.error(e);\n      showToast(`Failed to import agent skill. ${e.message}`, \"error\");\n    } finally {\n      setLoading(false);\n    }\n  }\n\n  useEffect(() => {\n    setEventDelegatorForCodeSnippets();\n  }, []);\n\n  return (\n    <div className=\"flex flex-col mt-4 gap-y-4\">\n      <div className=\"border border-white/10 light:border-orange-500/20 my-2 flex flex-col md:flex-row md:items-center gap-x-2 text-theme-text-primary light:text-orange-600 mb-4 bg-orange-800/30 light:bg-orange-500/10 rounded-lg px-4 py-2\">\n        <div className=\"flex flex-col gap-y-2\">\n          <div className=\"gap-x-2 flex items-center\">\n            <Warning size={25} />\n            <h1 className=\"text-lg font-semibold\">\n              {\" \"}\n              Only import agent skills you trust{\" \"}\n            </h1>\n          </div>\n          <p className=\"text-sm\">\n            Agent skills can execute code on your AnythingLLM instance, so only\n            import agent skills from sources you trust. You should also review\n            the code before importing. If you are unsure about what a skill does\n            - don't import it!\n          </p>\n        </div>\n      </div>\n\n      <div className=\"flex flex-col gap-y-1\">\n        <h2 className=\"text-base text-theme-text-primary font-semibold\">\n          Review Agent Skill \"{item.name}\"\n        </h2>\n        {item.creatorUsername && (\n          <p className=\"text-white/60 light:text-theme-text-secondary text-xs font-mono\">\n            Created by{\" \"}\n            <a\n              href={paths.communityHub.profile(item.creatorUsername)}\n              target=\"_blank\"\n              className=\"hover:text-blue-500 hover:underline\"\n              rel=\"noreferrer\"\n            >\n              @{item.creatorUsername}\n            </a>\n          </p>\n        )}\n        <div className=\"flex gap-x-1\">\n          {item.verified ? (\n            <p className=\"text-green-500 text-xs font-mono\">Verified code</p>\n          ) : (\n            <p className=\"text-red-500 text-xs font-mono\">\n              This skill is not verified.\n            </p>\n          )}\n          <a\n            href=\"https://docs.anythingllm.com/community-hub/faq#verification\"\n            target=\"_blank\"\n            className=\"text-xs font-mono text-blue-500 hover:underline\"\n            rel=\"noreferrer\"\n          >\n            Learn more &rarr;\n          </a>\n        </div>\n      </div>\n      <div className=\"flex flex-col gap-y-[25px] text-white/80 light:text-theme-text-secondary text-sm\">\n        <p>\n          Agent skills unlock new capabilities for your AnythingLLM workspace\n          via{\" \"}\n          <code className=\"font-mono bg-zinc-900 light:bg-slate-200 px-1 py-0.5 rounded-md text-sm\">\n            @agent\n          </code>{\" \"}\n          skills that can do specific tasks when invoked.\n        </p>\n      </div>\n      <FileReview item={item} />\n      <CTAButton\n        disabled={loading}\n        className=\"text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent\"\n        onClick={importAgentSkill}\n      >\n        {loading ? <CircleNotch size={16} className=\"animate-spin\" /> : null}\n        {loading ? \"Importing...\" : \"Import agent skill\"}\n      </CTAButton>\n    </div>\n  );\n}\n\nfunction FileReview({ item }) {\n  const files = item.manifest.files || [];\n  const [index, setIndex] = useState(0);\n  const [file, setFile] = useState(files[index]);\n  function handlePrevious() {\n    if (index > 0) setIndex(index - 1);\n  }\n\n  function handleNext() {\n    if (index < files.length - 1) setIndex(index + 1);\n  }\n\n  function fileMarkup(file) {\n    const extension = file.name.split(\".\").pop();\n    switch (extension) {\n      case \"js\":\n        return \"javascript\";\n      case \"json\":\n        return \"json\";\n      case \"md\":\n        return \"markdown\";\n      default:\n        return \"text\";\n    }\n  }\n\n  useEffect(() => {\n    if (files.length > 0) setFile(files?.[index] || files[0]);\n  }, [index]);\n\n  if (!file) return null;\n  return (\n    <div className=\"flex flex-col gap-y-2\">\n      <div className=\"flex flex-col gap-y-2\">\n        <div className=\"flex justify-between items-center\">\n          <button\n            type=\"button\"\n            className={`border-none bg-black/70 light:bg-slate-200 rounded-md p-1 text-white/60 light:text-theme-text-secondary text-xs font-mono ${\n              index === 0 ? \"opacity-50 cursor-not-allowed\" : \"\"\n            }`}\n            onClick={handlePrevious}\n          >\n            <CaretLeft size={16} />\n          </button>\n          <p className=\"text-white/60 light:text-theme-text-secondary text-xs font-mono\">\n            {file.name} ({index + 1} of {files.length} files)\n          </p>\n          <button\n            type=\"button\"\n            className={`border-none bg-black/70 light:bg-slate-200 rounded-md p-1 text-white/60 light:text-theme-text-secondary text-xs font-mono ${\n              index === files.length - 1 ? \"opacity-50 cursor-not-allowed\" : \"\"\n            }`}\n            onClick={handleNext}\n          >\n            <CaretRight size={16} />\n          </button>\n        </div>\n        <span\n          className=\"whitespace-pre-line flex flex-col gap-y-1 text-sm leading-[20px] max-h-[500px] overflow-y-auto hljs text-theme-text-primary\"\n          dangerouslySetInnerHTML={{\n            __html: DOMPurify.sanitize(\n              renderMarkdown(\n                `\\`\\`\\`${fileMarkup(file)}\\n${\n                  fileMarkup(file) === \"markdown\"\n                    ? file.content.replace(/```/g, \"~~~\") // Escape triple backticks in markdown\n                    : file.content\n                }\\n\\`\\`\\``\n              )\n            ),\n          }}\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/HubItem/SlashCommand.jsx",
    "content": "import CTAButton from \"@/components/lib/CTAButton\";\nimport CommunityHubImportItemSteps from \"../..\";\nimport showToast from \"@/utils/toast\";\nimport paths from \"@/utils/paths\";\nimport CommunityHub from \"@/models/communityHub\";\n\nexport default function SlashCommand({ item, setStep }) {\n  async function handleSubmit() {\n    try {\n      const { error } = await CommunityHub.applyItem(item.importId);\n      if (error) throw new Error(error);\n      showToast(\n        `Slash command ${item.command} imported successfully!`,\n        \"success\"\n      );\n      setStep(CommunityHubImportItemSteps.completed.key);\n    } catch (e) {\n      console.error(e);\n      showToast(`Failed to import slash command. ${e.message}`, \"error\");\n    }\n  }\n\n  return (\n    <div className=\"flex flex-col mt-4 gap-y-4\">\n      <div className=\"flex flex-col gap-y-1\">\n        <h2 className=\"text-base text-theme-text-primary font-semibold\">\n          Review Slash Command \"{item.name}\"\n        </h2>\n        {item.creatorUsername && (\n          <p className=\"text-white/60 text-xs font-mono\">\n            Created by{\" \"}\n            <a\n              href={paths.communityHub.profile(item.creatorUsername)}\n              target=\"_blank\"\n              className=\"hover:text-blue-500 hover:underline\"\n              rel=\"noreferrer\"\n            >\n              @{item.creatorUsername}\n            </a>\n          </p>\n        )}\n      </div>\n      <div className=\"flex flex-col gap-y-[25px] text-white/80 light:text-theme-text-secondary text-sm\">\n        <p>\n          Slash commands are used to prefill information into a prompt while\n          chatting with a AnythingLLM workspace.\n          <br />\n          <br />\n          The slash command will be available during chatting by simply invoking\n          it with{\" \"}\n          <code className=\"font-mono bg-zinc-900 light:bg-slate-200 px-1 py-0.5 rounded-md text-sm\">\n            {item.command}\n          </code>{\" \"}\n          like you would any other command.\n        </p>\n\n        <div className=\"flex flex-col gap-y-2 mt-2\">\n          <div className=\"w-full text-theme-text-primary text-md gap-x-2 flex items-center\">\n            <p className=\"text-white/60 light:text-theme-text-secondary w-fit font-mono bg-zinc-900 light:bg-slate-200 px-2 py-1 rounded-md text-sm whitespace-pre-line\">\n              {item.command}\n            </p>\n          </div>\n\n          <div className=\"w-full text-theme-text-primary text-md flex flex-col gap-y-2\">\n            <p className=\"text-white/60 light:text-theme-text-secondary font-mono bg-zinc-900 light:bg-slate-200 p-4 rounded-md text-sm whitespace-pre-line max-h-[calc(200px)] overflow-y-auto\">\n              {item.prompt}\n            </p>\n          </div>\n        </div>\n      </div>\n      <CTAButton\n        className=\"text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent\"\n        onClick={handleSubmit}\n      >\n        Import slash command\n      </CTAButton>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/HubItem/SystemPrompt.jsx",
    "content": "import CTAButton from \"@/components/lib/CTAButton\";\nimport CommunityHubImportItemSteps from \"../..\";\nimport { useEffect, useState } from \"react\";\nimport Workspace from \"@/models/workspace\";\nimport showToast from \"@/utils/toast\";\nimport paths from \"@/utils/paths\";\nimport CommunityHub from \"@/models/communityHub\";\n\nexport default function SystemPrompt({ item, setStep }) {\n  const [destinationWorkspaceSlug, setDestinationWorkspaceSlug] =\n    useState(null);\n  const [workspaces, setWorkspaces] = useState([]);\n  useEffect(() => {\n    async function getWorkspaces() {\n      const workspaces = await Workspace.all();\n      setWorkspaces(workspaces);\n      setDestinationWorkspaceSlug(workspaces[0].slug);\n    }\n    getWorkspaces();\n  }, []);\n\n  async function handleSubmit() {\n    showToast(\"Applying system prompt to workspace...\", \"info\");\n    const { error } = await CommunityHub.applyItem(item.importId, {\n      workspaceSlug: destinationWorkspaceSlug,\n    });\n    if (error) {\n      return showToast(`Failed to apply system prompt. ${error}`, \"error\", {\n        clear: true,\n      });\n    }\n\n    showToast(\"System prompt applied to workspace.\", \"success\", {\n      clear: true,\n    });\n    setStep(CommunityHubImportItemSteps.completed.key);\n  }\n\n  return (\n    <div className=\"flex flex-col mt-4 gap-y-4\">\n      <div className=\"flex flex-col gap-y-1\">\n        <h2 className=\"text-base text-theme-text-primary font-semibold\">\n          Review System Prompt \"{item.name}\"\n        </h2>\n        {item.creatorUsername && (\n          <p className=\"text-white/60 light:text-theme-text-secondary text-xs font-mono\">\n            Created by{\" \"}\n            <a\n              href={paths.communityHub.profile(item.creatorUsername)}\n              target=\"_blank\"\n              className=\"hover:text-blue-500 hover:underline\"\n              rel=\"noreferrer\"\n            >\n              @{item.creatorUsername}\n            </a>\n          </p>\n        )}\n      </div>\n      <div className=\"flex flex-col gap-y-[25px] text-white/80 light:text-theme-text-secondary text-sm\">\n        <p>\n          System prompts are used to guide the behavior of the AI agents and can\n          be applied to any existing workspace.\n        </p>\n\n        <div className=\"flex flex-col gap-y-2\">\n          <p className=\"text-white/60 light:text-theme-text-secondary font-semibold\">\n            Provided system prompt:\n          </p>\n          <div className=\"w-full text-theme-text-primary text-md flex flex-col max-h-[calc(300px)] overflow-y-auto\">\n            <p className=\"text-white/60 light:text-theme-text-secondary font-mono bg-zinc-900 light:bg-slate-200 px-2 py-1 rounded-md text-sm whitespace-pre-line\">\n              {item.prompt}\n            </p>\n          </div>\n        </div>\n\n        <div className=\"flex flex-col w-60\">\n          <label className=\"text-theme-text-primary text-sm font-semibold block mb-3\">\n            Apply to Workspace\n          </label>\n          <select\n            name=\"destinationWorkspaceSlug\"\n            required={true}\n            onChange={(e) => setDestinationWorkspaceSlug(e.target.value)}\n            className=\"border-none bg-theme-settings-input-bg border-gray-500 text-white text-sm rounded-lg block w-full p-2.5\"\n          >\n            <optgroup label=\"Available workspaces\">\n              {workspaces.map((workspace) => (\n                <option key={workspace.id} value={workspace.slug}>\n                  {workspace.name}\n                </option>\n              ))}\n            </optgroup>\n          </select>\n        </div>\n      </div>\n      {destinationWorkspaceSlug && (\n        <CTAButton\n          className=\"text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent\"\n          onClick={handleSubmit}\n        >\n          Apply system prompt to workspace\n        </CTAButton>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/HubItem/Unknown.jsx",
    "content": "import CTAButton from \"@/components/lib/CTAButton\";\nimport CommunityHubImportItemSteps from \"../..\";\nimport { Warning } from \"@phosphor-icons/react\";\n\nexport default function UnknownItem({ item, setSettings, setStep }) {\n  return (\n    <div className=\"flex flex-col mt-4 gap-y-4\">\n      <div className=\"w-full flex items-center gap-x-2\">\n        <Warning size={24} className=\"text-red-500\" />\n        <h2 className=\"text-base text-red-500 font-semibold\">\n          Unsupported item\n        </h2>\n      </div>\n      <div className=\"flex flex-col gap-y-[25px] text-white/80 text-sm\">\n        <p>\n          We found an item in the community hub, but we don't know what it is or\n          it is not yet supported for import into AnythingLLM.\n        </p>\n        <p>\n          The item ID is: <b>{item.id}</b>\n          <br />\n          The item type is: <b>{item.itemType}</b>\n        </p>\n        <p>\n          Please contact support via email if you need help importing this item.\n        </p>\n      </div>\n      <CTAButton\n        className=\"text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent\"\n        onClick={() => {\n          setSettings({ itemId: null, item: null });\n          setStep(CommunityHubImportItemSteps.itemId.key);\n        }}\n      >\n        Try another item\n      </CTAButton>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/HubItem/index.js",
    "content": "import SystemPrompt from \"./SystemPrompt\";\nimport SlashCommand from \"./SlashCommand\";\nimport UnknownItem from \"./Unknown\";\nimport AgentSkill from \"./AgentSkill\";\nimport AgentFlow from \"./AgentFlow\";\n\nconst HubItemComponent = {\n  \"agent-skill\": AgentSkill,\n  \"system-prompt\": SystemPrompt,\n  \"slash-command\": SlashCommand,\n  \"agent-flow\": AgentFlow,\n  unknown: UnknownItem,\n};\n\nexport default HubItemComponent;\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/index.jsx",
    "content": "import CommunityHub from \"@/models/communityHub\";\nimport CommunityHubImportItemSteps from \"..\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport { useEffect, useState } from \"react\";\nimport HubItemComponent from \"./HubItem\";\n\nfunction useGetCommunityHubItem({ importId, updateSettings }) {\n  const [item, setItem] = useState(null);\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState(null);\n\n  useEffect(() => {\n    async function fetchItem() {\n      if (!importId) return;\n      setLoading(true);\n      await new Promise((resolve) => setTimeout(resolve, 2000));\n      const { error, item } = await CommunityHub.getItemFromImportId(importId);\n      if (error) setError(error);\n      setItem(item);\n      updateSettings((prev) => ({ ...prev, item }));\n      setLoading(false);\n    }\n    fetchItem();\n  }, [importId]);\n\n  return { item, loading, error };\n}\n\nexport default function PullAndReview({ settings, setSettings, setStep }) {\n  const { item, loading, error } = useGetCommunityHubItem({\n    importId: settings.itemId,\n    updateSettings: setSettings,\n  });\n  const ItemComponent =\n    HubItemComponent[item?.itemType] || HubItemComponent[\"unknown\"];\n\n  return (\n    <div className=\"flex-[2] flex flex-col gap-y-[18px] mt-10\">\n      <div className=\"bg-theme-bg-secondary rounded-xl flex-1 p-6\">\n        <div className=\"w-full flex flex-col gap-y-2 max-w-[700px]\">\n          <h2 className=\"text-base text-theme-text-primary font-semibold\">\n            Review item\n          </h2>\n\n          {loading && (\n            <div className=\"flex h-[200px] min-w-[746px] rounded-lg animate-pulse\">\n              <div className=\"w-full h-full flex items-center justify-center\">\n                <p className=\"text-sm text-theme-text-secondary\">\n                  Pulling item details from community hub...\n                </p>\n              </div>\n            </div>\n          )}\n          {!loading && error && (\n            <>\n              <div className=\"flex flex-col gap-y-2 mt-8\">\n                <p className=\"text-red-500\">\n                  An error occurred while fetching the item. Please try again\n                  later.\n                </p>\n                <p className=\"text-red-500/80 text-sm font-mono\">{error}</p>\n              </div>\n              <CTAButton\n                className=\"text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent\"\n                onClick={() => {\n                  setSettings({ itemId: null, item: null });\n                  setStep(CommunityHubImportItemSteps.itemId.key);\n                }}\n              >\n                Try another item\n              </CTAButton>\n            </>\n          )}\n          {!loading && !error && item && (\n            <ItemComponent\n              item={item}\n              settings={settings}\n              setSettings={setSettings}\n              setStep={setStep}\n            />\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/index.jsx",
    "content": "import { isMobile } from \"react-device-detect\";\nimport { useEffect, useState } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport Introduction from \"./Introduction\";\nimport PullAndReview from \"./PullAndReview\";\nimport Completed from \"./Completed\";\nimport useQuery from \"@/hooks/useQuery\";\n\nconst CommunityHubImportItemSteps = {\n  itemId: {\n    key: \"itemId\",\n    name: \"1. Paste in Item ID\",\n    next: () => \"validation\",\n    component: ({ settings, setSettings, setStep }) => (\n      <Introduction\n        settings={settings}\n        setSettings={setSettings}\n        setStep={setStep}\n      />\n    ),\n  },\n  validation: {\n    key: \"validation\",\n    name: \"2. Review item\",\n    next: () => \"completed\",\n    component: ({ settings, setSettings, setStep }) => (\n      <PullAndReview\n        settings={settings}\n        setSettings={setSettings}\n        setStep={setStep}\n      />\n    ),\n  },\n  completed: {\n    key: \"completed\",\n    name: \"3. Completed\",\n    component: ({ settings, setSettings, setStep }) => (\n      <Completed\n        settings={settings}\n        setSettings={setSettings}\n        setStep={setStep}\n      />\n    ),\n  },\n};\n\nexport function CommunityHubImportItemLayout({ setStep, children }) {\n  const query = useQuery();\n  const [settings, setSettings] = useState({\n    itemId: null,\n    item: null,\n  });\n\n  useEffect(() => {\n    function autoForward() {\n      if (query.get(\"id\")) {\n        setSettings({ itemId: query.get(\"id\") });\n        setStep(CommunityHubImportItemSteps.itemId.next());\n      }\n    }\n    autoForward();\n  }, []);\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex md:mt-0 mt-6\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] w-full h-full flex\"\n      >\n        {children(settings, setSettings, setStep)}\n      </div>\n    </div>\n  );\n}\n\nexport default CommunityHubImportItemSteps;\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport { isMobile } from \"react-device-detect\";\nimport CommunityHubImportItemSteps, {\n  CommunityHubImportItemLayout,\n} from \"./Steps\";\n\nfunction SideBarSelection({ setStep, currentStep }) {\n  const currentIndex = Object.keys(CommunityHubImportItemSteps).indexOf(\n    currentStep\n  );\n  return (\n    <div\n      className={`bg-white/5 light:bg-white text-theme-text-primary rounded-xl py-1 px-4 shadow-lg ${\n        isMobile ? \"w-full\" : \"min-w-[360px] w-fit\"\n      }`}\n    >\n      {Object.entries(CommunityHubImportItemSteps).map(\n        ([stepKey, props], index) => {\n          const isSelected = currentStep === stepKey;\n          const isLast =\n            index === Object.keys(CommunityHubImportItemSteps).length - 1;\n          const isDone =\n            currentIndex ===\n              Object.keys(CommunityHubImportItemSteps).length - 1 ||\n            index < currentIndex;\n          return (\n            <div\n              key={stepKey}\n              className={[\n                \"py-3 flex items-center justify-between transition-all duration-300\",\n                isSelected ? \"rounded-t-xl\" : \"\",\n                isLast\n                  ? \"\"\n                  : \"border-b border-white/10 light:border-[#026AA2]/10\",\n              ].join(\" \")}\n            >\n              {isDone || isSelected ? (\n                <button\n                  onClick={() => setStep(stepKey)}\n                  className=\"border-none hover:underline text-sm font-medium text-theme-text-primary\"\n                >\n                  {props.name}\n                </button>\n              ) : (\n                <div className=\"text-sm text-theme-text-secondary font-medium\">\n                  {props.name}\n                </div>\n              )}\n              <div className=\"flex items-center gap-x-2\">\n                {isDone ? (\n                  <div className=\"w-[14px] h-[14px] rounded-full border border-[#32D583] flex items-center justify-center\">\n                    <div className=\"w-[5.6px] h-[5.6px] rounded-full bg-[#6CE9A6]\"></div>\n                  </div>\n                ) : (\n                  <div\n                    className={`w-[14px] h-[14px] rounded-full border border-theme-text-primary ${\n                      isSelected ? \"animate-pulse\" : \"opacity-50\"\n                    }`}\n                  />\n                )}\n              </div>\n            </div>\n          );\n        }\n      )}\n    </div>\n  );\n}\n\nexport default function CommunityHubImportItemFlow() {\n  const [step, setStep] = useState(\"itemId\");\n\n  const StepPage = CommunityHubImportItemSteps.hasOwnProperty(step)\n    ? CommunityHubImportItemSteps[step]\n    : CommunityHubImportItemSteps.itemId;\n\n  return (\n    <CommunityHubImportItemLayout setStep={setStep}>\n      {(settings, setSettings, setStep) => (\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n            <div className=\"items-center\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                Import a Community Item\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary\">\n              Import items from the AnythingLLM Community Hub to enhance your\n              instance with community-created prompts, skills, and commands.\n            </p>\n          </div>\n          <div className=\"flex-1 flex h-full\">\n            <div className=\"flex flex-col gap-y-[18px] mt-10 w-[360px] flex-shrink-0\">\n              <SideBarSelection setStep={setStep} currentStep={step} />\n            </div>\n            <div className=\"overflow-y-auto pb-[200px] h-screen no-scroll\">\n              <div className=\"ml-8\">\n                {StepPage.component({ settings, setSettings, setStep })}\n              </div>\n            </div>\n          </div>\n        </div>\n      )}\n    </CommunityHubImportItemLayout>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/HubItemCard/agentFlow.jsx",
    "content": "import { Link } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\nimport { VisibilityIcon } from \"./generic\";\nimport { safeJsonParse } from \"@/utils/request\";\n\nexport default function AgentFlowHubCard({ item }) {\n  const flow = safeJsonParse(item.flow, { steps: [] });\n  return (\n    <Link\n      to={paths.communityHub.importItem(item.importId)}\n      className=\"bg-black/70 light:bg-slate-100 rounded-lg p-3 hover:bg-black/60 light:hover:bg-slate-200 transition-all duration-200 cursor-pointer group border border-transparent hover:border-slate-400 flex flex-col h-full\"\n    >\n      <div className=\"flex gap-x-2 items-center\">\n        <p className=\"text-white text-sm font-medium\">{item.name}</p>\n        <VisibilityIcon visibility={item.visibility} />\n      </div>\n      <div className=\"flex flex-col gap-2 flex-1\">\n        <p className=\"text-white/60 text-xs mt-1\">{item.description}</p>\n        <label className=\"text-white/60 text-xs font-semibold mt-4\">\n          Steps ({flow.steps.length}):\n        </label>\n        <p className=\"text-white/60 text-xs bg-zinc-900 light:bg-slate-200 px-2 py-1 rounded-md font-mono border border-slate-800 light:border-slate-300\">\n          <ul className=\"list-disc pl-4\">\n            {flow.steps.map((step, index) => (\n              <li key={index}>{step.type}</li>\n            ))}\n          </ul>\n        </p>\n      </div>\n      <div className=\"flex justify-end mt-2\">\n        <Link\n          to={paths.communityHub.importItem(item.importId)}\n          className=\"text-primary-button hover:text-primary-button/80 text-sm font-medium px-3 py-1.5 rounded-md bg-black/30 light:bg-slate-200 group-hover:bg-black/50 light:group-hover:bg-slate-300 transition-all\"\n        >\n          Import →\n        </Link>\n      </div>\n    </Link>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/HubItemCard/agentSkill.jsx",
    "content": "import { Link } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\nimport pluralize from \"pluralize\";\nimport { VisibilityIcon } from \"./generic\";\n\nexport default function AgentSkillHubCard({ item }) {\n  return (\n    <>\n      <Link\n        key={item.id}\n        to={paths.communityHub.importItem(item.importId)}\n        className=\"bg-black/70 light:bg-slate-100 rounded-lg p-3 hover:bg-black/60 light:hover:bg-slate-200 transition-all duration-200 cursor-pointer group border border-transparent hover:border-slate-400\"\n      >\n        <div className=\"flex gap-x-2 items-center\">\n          <p className=\"text-white text-sm font-medium\">{item.name}</p>\n          <VisibilityIcon visibility={item.visibility} />\n        </div>\n        <div className=\"flex flex-col gap-2\">\n          <p className=\"text-white/60 text-xs mt-1\">{item.description}</p>\n\n          <p className=\"font-mono text-xs mt-1 text-white/60\">\n            {item.verified ? (\n              <span className=\"text-green-500\">Verified</span>\n            ) : (\n              <span className=\"text-red-500\">Unverified</span>\n            )}{\" \"}\n            Skill\n          </p>\n          <p className=\"font-mono text-xs mt-1 text-white/60\">\n            {item.manifest.files?.length || 0}{\" \"}\n            {pluralize(\"file\", item.manifest.files?.length || 0)} found\n          </p>\n        </div>\n        <div className=\"flex justify-end mt-2\">\n          <Link\n            to={paths.communityHub.importItem(item.importId)}\n            className=\"text-primary-button hover:text-primary-button/80 text-sm font-medium px-3 py-1.5 rounded-md bg-black/30 light:bg-slate-200 group-hover:bg-black/50 light:group-hover:bg-slate-300 transition-all\"\n          >\n            Import →\n          </Link>\n        </div>\n      </Link>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/HubItemCard/generic.jsx",
    "content": "import paths from \"@/utils/paths\";\nimport { Eye, LockSimple } from \"@phosphor-icons/react\";\nimport { Link } from \"react-router-dom\";\nimport { Tooltip } from \"react-tooltip\";\n\nexport default function GenericHubCard({ item }) {\n  return (\n    <div\n      key={item.id}\n      className=\"bg-zinc-800 light:bg-slate-100 rounded-lg p-3 hover:bg-zinc-700 light:hover:bg-slate-200 transition-all duration-200\"\n    >\n      <p className=\"text-white text-sm font-medium\">{item.name}</p>\n      <p className=\"text-white/60 text-xs mt-1\">{item.description}</p>\n      <div className=\"flex justify-end mt-2\">\n        <Link\n          className=\"text-primary-button hover:text-primary-button/80 text-xs\"\n          to={paths.communityHub.importItem(item.importId)}\n        >\n          Import →\n        </Link>\n      </div>\n    </div>\n  );\n}\n\nexport function VisibilityIcon({ visibility = \"public\" }) {\n  const Icon = visibility === \"private\" ? LockSimple : Eye;\n\n  return (\n    <>\n      <div\n        data-tooltip-id=\"visibility-icon\"\n        data-tooltip-content={`This item is ${visibility === \"private\" ? \"private\" : \"public\"}`}\n      >\n        <Icon className=\"w-4 h-4 text-white/60\" />\n      </div>\n      <Tooltip\n        id=\"visibility-icon\"\n        place=\"top\"\n        delayShow={300}\n        className=\"allm-tooltip !allm-text-xs\"\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/HubItemCard/index.jsx",
    "content": "import GenericHubCard from \"./generic\";\nimport SystemPromptHubCard from \"./systemPrompt\";\nimport SlashCommandHubCard from \"./slashCommand\";\nimport AgentSkillHubCard from \"./agentSkill\";\nimport AgentFlowHubCard from \"./agentFlow\";\n\nexport default function HubItemCard({ type, item }) {\n  switch (type) {\n    case \"systemPrompts\":\n      return <SystemPromptHubCard item={item} />;\n    case \"slashCommands\":\n      return <SlashCommandHubCard item={item} />;\n    case \"agentSkills\":\n      return <AgentSkillHubCard item={item} />;\n    case \"agentFlows\":\n      return <AgentFlowHubCard item={item} />;\n    default:\n      return <GenericHubCard item={item} />;\n  }\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/HubItemCard/slashCommand.jsx",
    "content": "import truncate from \"truncate\";\nimport { Link } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\nimport { VisibilityIcon } from \"./generic\";\n\nexport default function SlashCommandHubCard({ item }) {\n  return (\n    <>\n      <Link\n        key={item.id}\n        to={paths.communityHub.importItem(item.importId)}\n        className=\"bg-black/70 light:bg-slate-100 rounded-lg p-3 hover:bg-black/60 light:hover:bg-slate-200 transition-all duration-200 cursor-pointer group border border-transparent hover:border-slate-400\"\n      >\n        <div className=\"flex gap-x-2 items-center\">\n          <p className=\"text-white text-sm font-medium\">{item.name}</p>\n          <VisibilityIcon visibility={item.visibility} />\n        </div>\n        <div className=\"flex flex-col gap-2\">\n          <p className=\"text-white/60 text-xs mt-1\">{item.description}</p>\n          <label className=\"text-white/60 text-xs font-semibold mt-4\">\n            Command\n          </label>\n          <p className=\"text-white/60 text-xs bg-zinc-900 light:bg-slate-200 px-2 py-1 rounded-md font-mono border border-slate-800 light:border-slate-300\">\n            {item.command}\n          </p>\n\n          <label className=\"text-white/60 text-xs font-semibold mt-4\">\n            Prompt\n          </label>\n          <p className=\"text-white/60 text-xs bg-zinc-900 light:bg-slate-200 px-2 py-1 rounded-md font-mono border border-slate-800 light:border-slate-300\">\n            {truncate(item.prompt, 90)}\n          </p>\n        </div>\n        <div className=\"flex justify-end mt-2\">\n          <Link\n            to={paths.communityHub.importItem(item.importId)}\n            className=\"text-primary-button hover:text-primary-button/80 text-sm font-medium px-3 py-1.5 rounded-md bg-black/30 light:bg-slate-200 group-hover:bg-black/50 light:group-hover:bg-slate-300 transition-all\"\n          >\n            Import →\n          </Link>\n        </div>\n      </Link>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/HubItemCard/systemPrompt.jsx",
    "content": "import truncate from \"truncate\";\nimport { Link } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\nimport { VisibilityIcon } from \"./generic\";\n\nexport default function SystemPromptHubCard({ item }) {\n  return (\n    <>\n      <Link\n        key={item.id}\n        to={paths.communityHub.importItem(item.importId)}\n        className=\"bg-black/70 light:bg-slate-100 rounded-lg p-3 hover:bg-black/60 light:hover:bg-slate-200 transition-all duration-200 cursor-pointer group border border-transparent hover:border-slate-400\"\n      >\n        <div className=\"flex gap-x-2 items-center\">\n          <p className=\"text-white text-sm font-medium\">{item.name}</p>\n          <VisibilityIcon visibility={item.visibility} />\n        </div>\n        <div className=\"flex flex-col gap-2\">\n          <p className=\"text-white/60 text-xs mt-1\">{item.description}</p>\n          <label className=\"text-white/60 text-xs font-semibold mt-4\">\n            Prompt\n          </label>\n          <p className=\"text-white/60 text-xs bg-zinc-900 light:bg-slate-200 px-2 py-1 rounded-md font-mono border border-slate-800 light:border-slate-300\">\n            {truncate(item.prompt, 90)}\n          </p>\n        </div>\n        <div className=\"flex justify-end mt-2\">\n          <Link\n            to={paths.communityHub.importItem(item.importId)}\n            className=\"text-primary-button hover:text-primary-button/80 text-sm font-medium px-3 py-1.5 rounded-md bg-black/30 light:bg-slate-200 group-hover:bg-black/50 light:group-hover:bg-slate-300 transition-all\"\n          >\n            Import →\n          </Link>\n        </div>\n      </Link>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport CommunityHub from \"@/models/communityHub\";\nimport paths from \"@/utils/paths\";\nimport HubItemCard from \"./HubItemCard\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport { readableType, typeToPath } from \"../../utils\";\n\nconst DEFAULT_EXPLORE_ITEMS = {\n  agentSkills: { items: [], hasMore: false, totalCount: 0 },\n  systemPrompts: { items: [], hasMore: false, totalCount: 0 },\n  slashCommands: { items: [], hasMore: false, totalCount: 0 },\n};\n\nfunction useCommunityHubExploreItems() {\n  const [loading, setLoading] = useState(true);\n  const [exploreItems, setExploreItems] = useState(DEFAULT_EXPLORE_ITEMS);\n  useEffect(() => {\n    const fetchData = async () => {\n      setLoading(true);\n      try {\n        const { success, result } = await CommunityHub.fetchExploreItems();\n        if (success) setExploreItems(result || DEFAULT_EXPLORE_ITEMS);\n      } catch (error) {\n        console.error(\"Error fetching data:\", error);\n      } finally {\n        setLoading(false);\n      }\n    };\n\n    fetchData();\n  }, []);\n\n  return { loading, exploreItems };\n}\n\nexport default function HubItems() {\n  const { loading, exploreItems } = useCommunityHubExploreItems();\n  return (\n    <div className=\"w-full flex flex-col gap-y-1 pb-6 pt-6\">\n      <div className=\"flex flex-col gap-y-2 mb-4\">\n        <p className=\"text-base font-semibold text-theme-text-primary\">\n          Recently Added on AnythingLLM Community Hub\n        </p>\n        <p className=\"text-xs text-theme-text-secondary\">\n          Explore the latest additions to the AnythingLLM Community Hub\n        </p>\n      </div>\n      <HubCategory loading={loading} exploreItems={exploreItems} />\n    </div>\n  );\n}\n\nfunction HubCategory({ loading, exploreItems }) {\n  if (loading) return <HubItemCardSkeleton />;\n  return (\n    <div className=\"flex flex-col gap-4\">\n      {Object.keys(exploreItems).map((type) => {\n        const path = typeToPath(type);\n        if (exploreItems[type].items.length === 0) return null;\n        return (\n          <div key={type} className=\"rounded-lg w-full\">\n            <div className=\"flex justify-between items-center\">\n              <h3 className=\"text-theme-text-primary capitalize font-medium mb-3\">\n                {readableType(type)}\n              </h3>\n              {exploreItems[type].hasMore && (\n                <a\n                  href={paths.communityHub.viewMoreOfType(path)}\n                  target=\"_blank\"\n                  rel=\"noopener noreferrer\"\n                  className=\"text-primary-button hover:text-primary-button/80 text-sm\"\n                >\n                  Explore More →\n                </a>\n              )}\n            </div>\n            <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2\">\n              {exploreItems[type].items.map((item) => (\n                <HubItemCard key={item.id} type={type} item={item} />\n              ))}\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\nexport function HubItemCardSkeleton() {\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <div className=\"rounded-lg w-full\">\n        <div className=\"flex justify-between items-center\">\n          <Skeleton.default\n            height=\"40px\"\n            width=\"300px\"\n            highlightColor=\"var(--theme-settings-input-active)\"\n            baseColor=\"var(--theme-settings-input-bg)\"\n            count={1}\n          />\n        </div>\n        <Skeleton.default\n          height=\"200px\"\n          width=\"300px\"\n          highlightColor=\"var(--theme-settings-input-active)\"\n          baseColor=\"var(--theme-settings-input-bg)\"\n          count={4}\n          className=\"rounded-lg\"\n          containerClassName=\"flex flex-wrap gap-2 mt-1\"\n        />\n      </div>\n      <div className=\"rounded-lg w-full\">\n        <div className=\"flex justify-between items-center\">\n          <Skeleton.default\n            height=\"40px\"\n            width=\"300px\"\n            highlightColor=\"var(--theme-settings-input-active)\"\n            baseColor=\"var(--theme-settings-input-bg)\"\n            count={1}\n          />\n        </div>\n        <Skeleton.default\n          height=\"200px\"\n          width=\"300px\"\n          highlightColor=\"var(--theme-settings-input-active)\"\n          baseColor=\"var(--theme-settings-input-bg)\"\n          count={4}\n          className=\"rounded-lg\"\n          containerClassName=\"flex flex-wrap gap-2 mt-1\"\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/Trending/index.jsx",
    "content": "import Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport HubItems from \"./HubItems\";\n\nexport default function CommunityHub() {\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n            <div className=\"items-center\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                Community Hub\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary\">\n              Share and collaborate with the AnythingLLM community.\n            </p>\n          </div>\n          <HubItems />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/CommunityHub/utils.js",
    "content": "/**\n * Convert a type to a readable string for the community hub.\n * @param {(\"agentSkills\" | \"agentSkill\" | \"systemPrompts\" | \"systemPrompt\" | \"slashCommands\" | \"slashCommand\" | \"agentFlows\" | \"agentFlow\")} type\n * @returns {string}\n */\nexport function readableType(type) {\n  switch (type) {\n    case \"agentSkills\":\n    case \"agentSkill\":\n      return \"Agent Skills\";\n    case \"systemPrompt\":\n    case \"systemPrompts\":\n      return \"System Prompts\";\n    case \"slashCommand\":\n    case \"slashCommands\":\n      return \"Slash Commands\";\n    case \"agentFlows\":\n    case \"agentFlow\":\n      return \"Agent Flows\";\n  }\n}\n\n/**\n * Convert a type to a path for the community hub.\n * @param {(\"agentSkill\" | \"agentSkills\" | \"systemPrompt\" | \"systemPrompts\" | \"slashCommand\" | \"slashCommands\" | \"agentFlow\" | \"agentFlows\")} type\n * @returns {string}\n */\nexport function typeToPath(type) {\n  switch (type) {\n    case \"agentSkill\":\n    case \"agentSkills\":\n      return \"agent-skills\";\n    case \"systemPrompt\":\n    case \"systemPrompts\":\n      return \"system-prompts\";\n    case \"slashCommand\":\n    case \"slashCommands\":\n      return \"slash-commands\";\n    case \"agentFlow\":\n    case \"agentFlows\":\n      return \"agent-flows\";\n  }\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport AnythingLLMIcon from \"@/media/logo/anything-llm-icon.png\";\nimport OpenAiLogo from \"@/media/llmprovider/openai.png\";\nimport AzureOpenAiLogo from \"@/media/llmprovider/azure.png\";\nimport GeminiAiLogo from \"@/media/llmprovider/gemini.png\";\nimport LocalAiLogo from \"@/media/llmprovider/localai.png\";\nimport OllamaLogo from \"@/media/llmprovider/ollama.png\";\nimport LMStudioLogo from \"@/media/llmprovider/lmstudio.png\";\nimport CohereLogo from \"@/media/llmprovider/cohere.png\";\nimport VoyageAiLogo from \"@/media/embeddingprovider/voyageai.png\";\nimport LiteLLMLogo from \"@/media/llmprovider/litellm.png\";\nimport GenericOpenAiLogo from \"@/media/llmprovider/generic-openai.png\";\nimport MistralAiLogo from \"@/media/llmprovider/mistral.jpeg\";\nimport OpenRouterLogo from \"@/media/llmprovider/openrouter.jpeg\";\nimport LemonadeLogo from \"@/media/llmprovider/lemonade.png\";\n\nimport PreLoader from \"@/components/Preloader\";\nimport ChangeWarningModal from \"@/components/ChangeWarning\";\nimport OpenAiOptions from \"@/components/EmbeddingSelection/OpenAiOptions\";\nimport AzureAiOptions from \"@/components/EmbeddingSelection/AzureAiOptions\";\nimport GeminiOptions from \"@/components/EmbeddingSelection/GeminiOptions\";\nimport LocalAiOptions from \"@/components/EmbeddingSelection/LocalAiOptions\";\nimport NativeEmbeddingOptions from \"@/components/EmbeddingSelection/NativeEmbeddingOptions\";\nimport OllamaEmbeddingOptions from \"@/components/EmbeddingSelection/OllamaOptions\";\nimport LMStudioEmbeddingOptions from \"@/components/EmbeddingSelection/LMStudioOptions\";\nimport CohereEmbeddingOptions from \"@/components/EmbeddingSelection/CohereOptions\";\nimport VoyageAiOptions from \"@/components/EmbeddingSelection/VoyageAiOptions\";\nimport LiteLLMOptions from \"@/components/EmbeddingSelection/LiteLLMOptions\";\nimport GenericOpenAiEmbeddingOptions from \"@/components/EmbeddingSelection/GenericOpenAiOptions\";\nimport OpenRouterOptions from \"@/components/EmbeddingSelection/OpenRouterOptions\";\nimport MistralAiOptions from \"@/components/EmbeddingSelection/MistralAiOptions\";\nimport LemonadeOptions from \"@/components/EmbeddingSelection/LemonadeOptions\";\n\nimport EmbedderItem from \"@/components/EmbeddingSelection/EmbedderItem\";\nimport { CaretUpDown, MagnifyingGlass, X } from \"@phosphor-icons/react\";\nimport { useModal } from \"@/hooks/useModal\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport { useTranslation } from \"react-i18next\";\n\nconst EMBEDDERS = [\n  {\n    name: \"AnythingLLM Embedder\",\n    value: \"native\",\n    logo: AnythingLLMIcon,\n    options: (settings) => <NativeEmbeddingOptions settings={settings} />,\n    description:\n      \"Use the built-in embedding provider for AnythingLLM. Zero setup!\",\n  },\n  {\n    name: \"OpenAI\",\n    value: \"openai\",\n    logo: OpenAiLogo,\n    options: (settings) => <OpenAiOptions settings={settings} />,\n    description: \"The standard option for most non-commercial use.\",\n  },\n  {\n    name: \"Azure OpenAI\",\n    value: \"azure\",\n    logo: AzureOpenAiLogo,\n    options: (settings) => <AzureAiOptions settings={settings} />,\n    description: \"The enterprise option of OpenAI hosted on Azure services.\",\n  },\n  {\n    name: \"Gemini\",\n    value: \"gemini\",\n    logo: GeminiAiLogo,\n    options: (settings) => <GeminiOptions settings={settings} />,\n    description: \"Run powerful embedding models from Google AI.\",\n  },\n  {\n    name: \"Local AI\",\n    value: \"localai\",\n    logo: LocalAiLogo,\n    options: (settings) => <LocalAiOptions settings={settings} />,\n    description: \"Run embedding models locally on your own machine.\",\n  },\n  {\n    name: \"Ollama\",\n    value: \"ollama\",\n    logo: OllamaLogo,\n    options: (settings) => <OllamaEmbeddingOptions settings={settings} />,\n    description: \"Run embedding models locally on your own machine.\",\n  },\n  {\n    name: \"LM Studio\",\n    value: \"lmstudio\",\n    logo: LMStudioLogo,\n    options: (settings) => <LMStudioEmbeddingOptions settings={settings} />,\n    description:\n      \"Discover, download, and run thousands of cutting edge LLMs in a few clicks.\",\n  },\n  {\n    name: \"Lemonade\",\n    value: \"lemonade\",\n    logo: LemonadeLogo,\n    options: (settings) => <LemonadeOptions settings={settings} />,\n    description:\n      \"Run embedding models locally on your own machine using Lemonade.\",\n  },\n  {\n    name: \"OpenRouter\",\n    value: \"openrouter\",\n    logo: OpenRouterLogo,\n    options: (settings) => <OpenRouterOptions settings={settings} />,\n    description: \"Run embedding models from OpenRouter.\",\n  },\n  {\n    name: \"LiteLLM\",\n    value: \"litellm\",\n    logo: LiteLLMLogo,\n    options: (settings) => <LiteLLMOptions settings={settings} />,\n    description: \"Run powerful embedding models from LiteLLM.\",\n  },\n  {\n    name: \"Cohere\",\n    value: \"cohere\",\n    logo: CohereLogo,\n    options: (settings) => <CohereEmbeddingOptions settings={settings} />,\n    description: \"Run powerful embedding models from Cohere.\",\n  },\n  {\n    name: \"Voyage AI\",\n    value: \"voyageai\",\n    logo: VoyageAiLogo,\n    options: (settings) => <VoyageAiOptions settings={settings} />,\n    description: \"Run powerful embedding models from Voyage AI.\",\n  },\n  {\n    name: \"Mistral AI\",\n    value: \"mistral\",\n    logo: MistralAiLogo,\n    options: (settings) => <MistralAiOptions settings={settings} />,\n    description: \"Run powerful embedding models from Mistral AI.\",\n  },\n  {\n    name: \"Generic OpenAI\",\n    value: \"generic-openai\",\n    logo: GenericOpenAiLogo,\n    options: (settings) => (\n      <GenericOpenAiEmbeddingOptions settings={settings} />\n    ),\n    description: \"Run embedding models from any OpenAI compatible API service.\",\n  },\n];\n\nexport default function GeneralEmbeddingPreference() {\n  const [saving, setSaving] = useState(false);\n  const [hasChanges, setHasChanges] = useState(false);\n  const [hasEmbeddings, setHasEmbeddings] = useState(false);\n  const [hasCachedEmbeddings, setHasCachedEmbeddings] = useState(false);\n  const [settings, setSettings] = useState(null);\n  const [loading, setLoading] = useState(true);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [filteredEmbedders, setFilteredEmbedders] = useState([]);\n  const [selectedEmbedder, setSelectedEmbedder] = useState(null);\n  const [searchMenuOpen, setSearchMenuOpen] = useState(false);\n  const searchInputRef = useRef(null);\n  const { isOpen, openModal, closeModal } = useModal();\n  const { t } = useTranslation();\n\n  function embedderModelChanged(formEl) {\n    try {\n      const newModel = new FormData(formEl).get(\"EmbeddingModelPref\") ?? null;\n      if (newModel === null) return false;\n      return settings?.EmbeddingModelPref !== newModel;\n    } catch (error) {\n      console.error(error);\n    }\n    return false;\n  }\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    if (\n      (selectedEmbedder !== settings?.EmbeddingEngine ||\n        embedderModelChanged(e.target)) &&\n      hasChanges &&\n      (hasEmbeddings || hasCachedEmbeddings)\n    ) {\n      openModal();\n    } else {\n      await handleSaveSettings();\n    }\n  };\n\n  const handleSaveSettings = async () => {\n    setSaving(true);\n    const form = document.getElementById(\"embedding-form\");\n    const settingsData = {};\n    const formData = new FormData(form);\n    settingsData.EmbeddingEngine = selectedEmbedder;\n    for (var [key, value] of formData.entries()) settingsData[key] = value;\n\n    const { error } = await System.updateSystem(settingsData);\n    if (error) {\n      showToast(`Failed to save embedding settings: ${error}`, \"error\");\n      setHasChanges(true);\n    } else {\n      showToast(\"Embedding preferences saved successfully.\", \"success\");\n      setHasChanges(false);\n    }\n    setSaving(false);\n    closeModal();\n  };\n\n  const updateChoice = (selection) => {\n    setSearchQuery(\"\");\n    setSelectedEmbedder(selection);\n    setSearchMenuOpen(false);\n    setHasChanges(true);\n  };\n\n  const handleXButton = () => {\n    if (searchQuery.length > 0) {\n      setSearchQuery(\"\");\n      if (searchInputRef.current) searchInputRef.current.value = \"\";\n    } else {\n      setSearchMenuOpen(!searchMenuOpen);\n    }\n  };\n\n  useEffect(() => {\n    async function fetchKeys() {\n      const _settings = await System.keys();\n      setSettings(_settings);\n      setSelectedEmbedder(_settings?.EmbeddingEngine || \"native\");\n      setHasEmbeddings(_settings?.HasExistingEmbeddings || false);\n      setHasCachedEmbeddings(_settings?.HasCachedEmbeddings || false);\n      setLoading(false);\n    }\n    fetchKeys();\n  }, []);\n\n  useEffect(() => {\n    const filtered = EMBEDDERS.filter((embedder) =>\n      embedder.name.toLowerCase().includes(searchQuery.toLowerCase())\n    );\n    setFilteredEmbedders(filtered);\n  }, [searchQuery, selectedEmbedder]);\n\n  const selectedEmbedderObject = EMBEDDERS.find(\n    (embedder) => embedder.value === selectedEmbedder\n  );\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      {loading ? (\n        <div\n          style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n          className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n        >\n          <div className=\"w-full h-full flex justify-center items-center\">\n            <PreLoader />\n          </div>\n        </div>\n      ) : (\n        <div\n          style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n          className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n        >\n          <form\n            id=\"embedding-form\"\n            onSubmit={handleSubmit}\n            className=\"flex w-full\"\n          >\n            <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] py-16 md:py-6\">\n              <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n                <div className=\"flex gap-x-4 items-center\">\n                  <p className=\"text-lg leading-6 font-bold text-white\">\n                    {t(\"embedding.title\")}\n                  </p>\n                </div>\n                <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n                  {t(\"embedding.desc-start\")}\n                  <br />\n                  {t(\"embedding.desc-end\")}\n                </p>\n              </div>\n              <div className=\"w-full justify-end flex\">\n                {hasChanges && (\n                  <CTAButton\n                    onClick={() => handleSubmit()}\n                    className=\"mt-3 mr-0 -mb-14 z-10\"\n                  >\n                    {saving ? t(\"common.saving\") : t(\"common.save\")}\n                  </CTAButton>\n                )}\n              </div>\n              <div className=\"text-base font-bold text-white mt-6 mb-4\">\n                {t(\"embedding.provider.title\")}\n              </div>\n              <div className=\"relative\">\n                {searchMenuOpen && (\n                  <div\n                    className=\"fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10\"\n                    onClick={() => setSearchMenuOpen(false)}\n                  />\n                )}\n                {searchMenuOpen ? (\n                  <div className=\"absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] min-h-[64px] bg-theme-settings-input-bg rounded-lg flex flex-col justify-between cursor-pointer border-2 border-primary-button z-20\">\n                    <div className=\"w-full flex flex-col gap-y-1\">\n                      <div className=\"flex items-center sticky top-0 z-10 border-b border-[#9CA3AF] mx-4 bg-theme-settings-input-bg\">\n                        <MagnifyingGlass\n                          size={20}\n                          weight=\"bold\"\n                          className=\"absolute left-4 z-30 text-theme-text-primary -ml-4 my-2\"\n                        />\n                        <input\n                          type=\"text\"\n                          name=\"embedder-search\"\n                          autoComplete=\"off\"\n                          placeholder=\"Search all embedding providers\"\n                          className=\"border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none text-theme-text-primary placeholder:text-theme-text-primary placeholder:font-medium\"\n                          onChange={(e) => setSearchQuery(e.target.value)}\n                          ref={searchInputRef}\n                          onKeyDown={(e) => {\n                            if (e.key === \"Enter\") e.preventDefault();\n                          }}\n                        />\n                        <X\n                          size={20}\n                          weight=\"bold\"\n                          className=\"cursor-pointer text-white hover:text-x-button\"\n                          onClick={handleXButton}\n                        />\n                      </div>\n                      <div className=\"flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4 max-h-[245px]\">\n                        {filteredEmbedders.map((embedder) => (\n                          <EmbedderItem\n                            key={embedder.name}\n                            name={embedder.name}\n                            value={embedder.value}\n                            image={embedder.logo}\n                            description={embedder.description}\n                            checked={selectedEmbedder === embedder.value}\n                            onClick={() => updateChoice(embedder.value)}\n                          />\n                        ))}\n                      </div>\n                    </div>\n                  </div>\n                ) : (\n                  <button\n                    className=\"w-full max-w-[640px] h-[64px] bg-theme-settings-input-bg rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-primary-button transition-all duration-300\"\n                    type=\"button\"\n                    onClick={() => setSearchMenuOpen(true)}\n                  >\n                    <div className=\"flex gap-x-4 items-center\">\n                      <img\n                        src={selectedEmbedderObject.logo}\n                        alt={`${selectedEmbedderObject.name} logo`}\n                        className=\"w-10 h-10 rounded-md\"\n                      />\n                      <div className=\"flex flex-col text-left\">\n                        <div className=\"text-sm font-semibold text-white\">\n                          {selectedEmbedderObject.name}\n                        </div>\n                        <div className=\"mt-1 text-xs text-description\">\n                          {selectedEmbedderObject.description}\n                        </div>\n                      </div>\n                    </div>\n                    <CaretUpDown\n                      size={24}\n                      weight=\"bold\"\n                      className=\"text-white\"\n                    />\n                  </button>\n                )}\n              </div>\n              <div\n                onChange={() => setHasChanges(true)}\n                className=\"mt-4 flex flex-col gap-y-1\"\n              >\n                {selectedEmbedder &&\n                  EMBEDDERS.find(\n                    (embedder) => embedder.value === selectedEmbedder\n                  )?.options(settings)}\n              </div>\n            </div>\n          </form>\n        </div>\n      )}\n      <ModalWrapper isOpen={isOpen}>\n        <ChangeWarningModal\n          warningText=\"Switching the embedding model will reset all previously embedded documents in all workspaces.\\n\\nConfirming will clear all embeddings from your vector database and remove all documents from your workspaces. Your uploaded documents will not be deleted, they will be available for re-embedding.\"\n          onClose={closeModal}\n          onConfirm={handleSaveSettings}\n        />\n      </ModalWrapper>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/EmbeddingTextSplitterPreference/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport PreLoader from \"@/components/Preloader\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport Admin from \"@/models/admin\";\nimport showToast from \"@/utils/toast\";\nimport { numberWithCommas } from \"@/utils/numbers\";\nimport { useTranslation } from \"react-i18next\";\nimport { useModal } from \"@/hooks/useModal\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport ChangeWarningModal from \"@/components/ChangeWarning\";\n\nfunction isNullOrNaN(value) {\n  if (value === null) return true;\n  return isNaN(value);\n}\n\nexport default function EmbeddingTextSplitterPreference() {\n  const [settings, setSettings] = useState({});\n  const [loading, setLoading] = useState(true);\n  const [saving, setSaving] = useState(false);\n  const [hasChanges, setHasChanges] = useState(false);\n  const { isOpen, openModal, closeModal } = useModal();\n  const { t } = useTranslation();\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = new FormData(e.target);\n\n    if (\n      Number(form.get(\"text_splitter_chunk_overlap\")) >=\n      Number(form.get(\"text_splitter_chunk_size\"))\n    ) {\n      showToast(\n        \"Chunk overlap cannot be larger or equal to chunk size.\",\n        \"error\"\n      );\n      return;\n    }\n\n    openModal();\n  };\n\n  const handleSaveSettings = async () => {\n    setSaving(true);\n    try {\n      const form = new FormData(\n        document.getElementById(\"text-splitter-chunking-form\")\n      );\n      await Admin.updateSystemPreferences({\n        text_splitter_chunk_size: isNullOrNaN(\n          form.get(\"text_splitter_chunk_size\")\n        )\n          ? 1000\n          : Number(form.get(\"text_splitter_chunk_size\")),\n        text_splitter_chunk_overlap: isNullOrNaN(\n          form.get(\"text_splitter_chunk_overlap\")\n        )\n          ? 1000\n          : Number(form.get(\"text_splitter_chunk_overlap\")),\n      });\n      setHasChanges(false);\n      closeModal();\n      showToast(\"Text chunking strategy settings saved.\", \"success\");\n    } catch {\n      showToast(\"Failed to save text chunking strategy settings.\", \"error\");\n    } finally {\n      setSaving(false);\n    }\n  };\n\n  useEffect(() => {\n    async function fetchSettings() {\n      const _settings = (\n        await Admin.systemPreferencesByFields([\n          \"text_splitter_chunk_size\",\n          \"text_splitter_chunk_overlap\",\n          \"max_embed_chunk_size\",\n        ])\n      )?.settings;\n      setSettings(_settings ?? {});\n      setLoading(false);\n    }\n    fetchSettings();\n  }, []);\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      {loading ? (\n        <div\n          style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n          className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n        >\n          <div className=\"w-full h-full flex justify-center items-center\">\n            <PreLoader />\n          </div>\n        </div>\n      ) : (\n        <div\n          style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n          className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n        >\n          <form\n            onSubmit={handleSubmit}\n            onChange={() => setHasChanges(true)}\n            className=\"flex w-full\"\n            id=\"text-splitter-chunking-form\"\n          >\n            <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n              <div className=\"w-full flex flex-col gap-y-1 pb-4 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n                <div className=\"flex gap-x-4 items-center\">\n                  <p className=\"text-lg leading-6 font-bold text-white\">\n                    {t(\"text.title\")}\n                  </p>\n                </div>\n                <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n                  {t(\"text.desc-start\")} <br />\n                  {t(\"text.desc-end\")}\n                </p>\n              </div>\n              <div className=\"w-full justify-end flex\">\n                {hasChanges && (\n                  <CTAButton className=\"mt-3 mr-0 -mb-14 z-10\">\n                    {saving ? t(\"common.saving\") : t(\"common.save\")}\n                  </CTAButton>\n                )}\n              </div>\n\n              <div className=\"flex flex-col gap-y-4 mt-8\">\n                <div className=\"flex flex-col max-w-[300px]\">\n                  <div className=\"flex flex-col gap-y-2 mb-4\">\n                    <label className=\"text-white text-sm font-semibold block\">\n                      {t(\"text.size.title\")}\n                    </label>\n                    <p className=\"text-xs text-white/60\">\n                      {t(\"text.size.description\")}\n                    </p>\n                  </div>\n                  <input\n                    type=\"number\"\n                    name=\"text_splitter_chunk_size\"\n                    min={1}\n                    max={settings?.max_embed_chunk_size || 1000}\n                    onWheel={(e) => e?.currentTarget?.blur()}\n                    className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                    placeholder=\"maximum length of vectorized text\"\n                    defaultValue={\n                      isNullOrNaN(settings?.text_splitter_chunk_size)\n                        ? 1000\n                        : Number(settings?.text_splitter_chunk_size)\n                    }\n                    required={true}\n                    autoComplete=\"off\"\n                  />\n                  <p className=\"text-xs text-white/40 mt-2\">\n                    {t(\"text.size.recommend\")}{\" \"}\n                    {numberWithCommas(settings?.max_embed_chunk_size || 1000)}.\n                  </p>\n                </div>\n              </div>\n\n              <div className=\"flex flex-col gap-y-4 mt-8\">\n                <div className=\"flex flex-col max-w-[300px]\">\n                  <div className=\"flex flex-col gap-y-2 mb-4\">\n                    <label className=\"text-white text-sm font-semibold block\">\n                      {t(\"text.overlap.title\")}\n                    </label>\n                    <p className=\"text-xs text-white/60\">\n                      {t(\"text.overlap.description\")}\n                    </p>\n                  </div>\n                  <input\n                    type=\"number\"\n                    name=\"text_splitter_chunk_overlap\"\n                    min={0}\n                    onWheel={(e) => e?.currentTarget?.blur()}\n                    className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                    placeholder=\"maximum length of vectorized text\"\n                    defaultValue={\n                      isNullOrNaN(settings?.text_splitter_chunk_overlap)\n                        ? 20\n                        : Number(settings?.text_splitter_chunk_overlap)\n                    }\n                    required={true}\n                    autoComplete=\"off\"\n                  />\n                </div>\n              </div>\n            </div>\n          </form>\n        </div>\n      )}\n\n      <ModalWrapper isOpen={isOpen}>\n        <ChangeWarningModal\n          warningText=\"Changing text splitter settings will clear any previously cached documents.\\n\\nThese new settings will be applied to all documents when embedding them into a workspace.\"\n          onClose={closeModal}\n          onConfirm={handleSaveSettings}\n        />\n      </ModalWrapper>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/LLMPreference/index.jsx",
    "content": "import React, { useEffect, useRef, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport AnythingLLMIcon from \"@/media/logo/anything-llm-icon.png\";\nimport OpenAiLogo from \"@/media/llmprovider/openai.png\";\nimport GenericOpenAiLogo from \"@/media/llmprovider/generic-openai.png\";\nimport AzureOpenAiLogo from \"@/media/llmprovider/azure.png\";\nimport AnthropicLogo from \"@/media/llmprovider/anthropic.png\";\nimport GeminiLogo from \"@/media/llmprovider/gemini.png\";\nimport OllamaLogo from \"@/media/llmprovider/ollama.png\";\nimport NovitaLogo from \"@/media/llmprovider/novita.png\";\nimport LMStudioLogo from \"@/media/llmprovider/lmstudio.png\";\nimport LocalAiLogo from \"@/media/llmprovider/localai.png\";\nimport TogetherAILogo from \"@/media/llmprovider/togetherai.png\";\nimport FireworksAILogo from \"@/media/llmprovider/fireworksai.jpeg\";\nimport MistralLogo from \"@/media/llmprovider/mistral.jpeg\";\nimport HuggingFaceLogo from \"@/media/llmprovider/huggingface.png\";\nimport PerplexityLogo from \"@/media/llmprovider/perplexity.png\";\nimport OpenRouterLogo from \"@/media/llmprovider/openrouter.jpeg\";\nimport GroqLogo from \"@/media/llmprovider/groq.png\";\nimport KoboldCPPLogo from \"@/media/llmprovider/koboldcpp.png\";\nimport TextGenWebUILogo from \"@/media/llmprovider/text-generation-webui.png\";\nimport CohereLogo from \"@/media/llmprovider/cohere.png\";\nimport LiteLLMLogo from \"@/media/llmprovider/litellm.png\";\nimport AWSBedrockLogo from \"@/media/llmprovider/bedrock.png\";\nimport DeepSeekLogo from \"@/media/llmprovider/deepseek.png\";\nimport APIPieLogo from \"@/media/llmprovider/apipie.png\";\nimport XAILogo from \"@/media/llmprovider/xai.png\";\nimport ZAiLogo from \"@/media/llmprovider/zai.png\";\nimport NvidiaNimLogo from \"@/media/llmprovider/nvidia-nim.png\";\nimport PPIOLogo from \"@/media/llmprovider/ppio.png\";\nimport DellProAiStudioLogo from \"@/media/llmprovider/dpais.png\";\nimport MoonshotAiLogo from \"@/media/llmprovider/moonshotai.png\";\nimport CometApiLogo from \"@/media/llmprovider/cometapi.png\";\nimport FoundryLogo from \"@/media/llmprovider/foundry-local.png\";\nimport GiteeAILogo from \"@/media/llmprovider/giteeai.png\";\nimport DockerModelRunnerLogo from \"@/media/llmprovider/docker-model-runner.png\";\nimport PrivateModeLogo from \"@/media/llmprovider/privatemode.png\";\nimport SambaNovaLogo from \"@/media/llmprovider/sambanova.png\";\nimport LemonadeLogo from \"@/media/llmprovider/lemonade.png\";\n\nimport PreLoader from \"@/components/Preloader\";\nimport OpenAiOptions from \"@/components/LLMSelection/OpenAiOptions\";\nimport GenericOpenAiOptions from \"@/components/LLMSelection/GenericOpenAiOptions\";\nimport AzureAiOptions from \"@/components/LLMSelection/AzureAiOptions\";\nimport AnthropicAiOptions from \"@/components/LLMSelection/AnthropicAiOptions\";\nimport LMStudioOptions from \"@/components/LLMSelection/LMStudioOptions\";\nimport LocalAiOptions from \"@/components/LLMSelection/LocalAiOptions\";\nimport GeminiLLMOptions from \"@/components/LLMSelection/GeminiLLMOptions\";\nimport OllamaLLMOptions from \"@/components/LLMSelection/OllamaLLMOptions\";\nimport NovitaLLMOptions from \"@/components/LLMSelection/NovitaLLMOptions\";\nimport CometApiLLMOptions from \"@/components/LLMSelection/CometApiLLMOptions\";\nimport TogetherAiOptions from \"@/components/LLMSelection/TogetherAiOptions\";\nimport FireworksAiOptions from \"@/components/LLMSelection/FireworksAiOptions\";\nimport MistralOptions from \"@/components/LLMSelection/MistralOptions\";\nimport HuggingFaceOptions from \"@/components/LLMSelection/HuggingFaceOptions\";\nimport PerplexityOptions from \"@/components/LLMSelection/PerplexityOptions\";\nimport OpenRouterOptions from \"@/components/LLMSelection/OpenRouterOptions\";\nimport GroqAiOptions from \"@/components/LLMSelection/GroqAiOptions\";\nimport CohereAiOptions from \"@/components/LLMSelection/CohereAiOptions\";\nimport KoboldCPPOptions from \"@/components/LLMSelection/KoboldCPPOptions\";\nimport TextGenWebUIOptions from \"@/components/LLMSelection/TextGenWebUIOptions\";\nimport LiteLLMOptions from \"@/components/LLMSelection/LiteLLMOptions\";\nimport AWSBedrockLLMOptions from \"@/components/LLMSelection/AwsBedrockLLMOptions\";\nimport DeepSeekOptions from \"@/components/LLMSelection/DeepSeekOptions\";\nimport ApiPieLLMOptions from \"@/components/LLMSelection/ApiPieOptions\";\nimport XAILLMOptions from \"@/components/LLMSelection/XAiLLMOptions\";\nimport ZAiLLMOptions from \"@/components/LLMSelection/ZAiLLMOptions\";\nimport NvidiaNimOptions from \"@/components/LLMSelection/NvidiaNimOptions\";\nimport PPIOLLMOptions from \"@/components/LLMSelection/PPIOLLMOptions\";\nimport DellProAiStudioOptions from \"@/components/LLMSelection/DPAISOptions\";\nimport MoonshotAiOptions from \"@/components/LLMSelection/MoonshotAiOptions\";\nimport FoundryOptions from \"@/components/LLMSelection/FoundryOptions\";\nimport GiteeAIOptions from \"@/components/LLMSelection/GiteeAIOptions/index.jsx\";\nimport DockerModelRunnerOptions from \"@/components/LLMSelection/DockerModelRunnerOptions\";\nimport PrivateModeOptions from \"@/components/LLMSelection/PrivateModeOptions\";\nimport SambaNovaOptions from \"@/components/LLMSelection/SambaNovaOptions\";\nimport LemonadeOptions from \"@/components/LLMSelection/LemonadeOptions\";\n\nimport LLMItem from \"@/components/LLMSelection/LLMItem\";\nimport { CaretUpDown, MagnifyingGlass, X } from \"@phosphor-icons/react\";\nimport CTAButton from \"@/components/lib/CTAButton\";\n\nexport const AVAILABLE_LLM_PROVIDERS = [\n  {\n    name: \"OpenAI\",\n    value: \"openai\",\n    logo: OpenAiLogo,\n    options: (settings) => <OpenAiOptions settings={settings} />,\n    description: \"The standard option for most non-commercial use.\",\n    requiredConfig: [\"OpenAiKey\"],\n  },\n  {\n    name: \"Azure OpenAI\",\n    value: \"azure\",\n    logo: AzureOpenAiLogo,\n    options: (settings) => <AzureAiOptions settings={settings} />,\n    description: \"The enterprise option of OpenAI hosted on Azure services.\",\n    requiredConfig: [\"AzureOpenAiEndpoint\"],\n  },\n  {\n    name: \"Anthropic\",\n    value: \"anthropic\",\n    logo: AnthropicLogo,\n    options: (settings) => <AnthropicAiOptions settings={settings} />,\n    description: \"A friendly AI Assistant hosted by Anthropic.\",\n    requiredConfig: [\"AnthropicApiKey\"],\n  },\n  {\n    name: \"Gemini\",\n    value: \"gemini\",\n    logo: GeminiLogo,\n    options: (settings) => <GeminiLLMOptions settings={settings} />,\n    description: \"Google's largest and most capable AI model\",\n    requiredConfig: [\"GeminiLLMApiKey\"],\n  },\n  {\n    name: \"NVIDIA NIM\",\n    value: \"nvidia-nim\",\n    logo: NvidiaNimLogo,\n    options: (settings) => <NvidiaNimOptions settings={settings} />,\n    description:\n      \"Run full parameter LLMs directly on your NVIDIA RTX GPU using NVIDIA NIM.\",\n    requiredConfig: [\"NvidiaNimLLMBasePath\"],\n  },\n  {\n    name: \"HuggingFace\",\n    value: \"huggingface\",\n    logo: HuggingFaceLogo,\n    options: (settings) => <HuggingFaceOptions settings={settings} />,\n    description:\n      \"Access 150,000+ open-source LLMs and the world's AI community\",\n    requiredConfig: [\n      \"HuggingFaceLLMEndpoint\",\n      \"HuggingFaceLLMAccessToken\",\n      \"HuggingFaceLLMTokenLimit\",\n    ],\n  },\n  {\n    name: \"Ollama\",\n    value: \"ollama\",\n    logo: OllamaLogo,\n    options: (settings) => <OllamaLLMOptions settings={settings} />,\n    description: \"Run LLMs locally on your own machine.\",\n    requiredConfig: [\"OllamaLLMBasePath\"],\n  },\n  {\n    name: \"Dell Pro AI Studio\",\n    value: \"dpais\",\n    logo: DellProAiStudioLogo,\n    options: (settings) => <DellProAiStudioOptions settings={settings} />,\n    description:\n      \"Run powerful LLMs quickly on NPU powered by Dell Pro AI Studio.\",\n    requiredConfig: [\n      \"DellProAiStudioBasePath\",\n      \"DellProAiStudioModelPref\",\n      \"DellProAiStudioTokenLimit\",\n    ],\n  },\n  {\n    name: \"LM Studio\",\n    value: \"lmstudio\",\n    logo: LMStudioLogo,\n    options: (settings) => <LMStudioOptions settings={settings} />,\n    description:\n      \"Discover, download, and run thousands of cutting edge LLMs in a few clicks.\",\n    requiredConfig: [\"LMStudioBasePath\"],\n  },\n  {\n    name: \"Docker Model Runner\",\n    value: \"docker-model-runner\",\n    logo: DockerModelRunnerLogo,\n    options: (settings) => <DockerModelRunnerOptions settings={settings} />,\n    description: \"Run LLMs using Docker Model Runner.\",\n    requiredConfig: [\n      \"DockerModelRunnerBasePath\",\n      \"DockerModelRunnerModelPref\",\n      \"DockerModelRunnerModelTokenLimit\",\n    ],\n  },\n  {\n    name: \"Lemonade\",\n    value: \"lemonade\",\n    logo: LemonadeLogo,\n    options: (settings) => <LemonadeOptions settings={settings} />,\n    description:\n      \"Run local LLMs, ASR, TTS, and more in a single unified AI runtime.\",\n    requiredConfig: [\"LemonadeLLMBasePath\"],\n  },\n  {\n    name: \"SambaNova\",\n    value: \"sambanova\",\n    logo: SambaNovaLogo,\n    options: (settings) => <SambaNovaOptions settings={settings} />,\n    description: \"Run open source models from SambaNova.\",\n    requiredConfig: [\"SambaNovaLLMApiKey\"],\n  },\n  {\n    name: \"Local AI\",\n    value: \"localai\",\n    logo: LocalAiLogo,\n    options: (settings) => <LocalAiOptions settings={settings} />,\n    description: \"Run LLMs locally on your own machine.\",\n    requiredConfig: [\"LocalAiApiKey\", \"LocalAiBasePath\", \"LocalAiTokenLimit\"],\n  },\n  {\n    name: \"Together AI\",\n    value: \"togetherai\",\n    logo: TogetherAILogo,\n    options: (settings) => <TogetherAiOptions settings={settings} />,\n    description: \"Run open source models from Together AI.\",\n    requiredConfig: [\"TogetherAiApiKey\"],\n  },\n\n  {\n    name: \"Fireworks AI\",\n    value: \"fireworksai\",\n    logo: FireworksAILogo,\n    options: (settings) => <FireworksAiOptions settings={settings} />,\n    description:\n      \"The fastest and most efficient inference engine to build production-ready, compound AI systems.\",\n    requiredConfig: [\"FireworksAiLLMApiKey\"],\n  },\n  {\n    name: \"Mistral\",\n    value: \"mistral\",\n    logo: MistralLogo,\n    options: (settings) => <MistralOptions settings={settings} />,\n    description: \"Run open source models from Mistral AI.\",\n    requiredConfig: [\"MistralApiKey\"],\n  },\n  {\n    name: \"Perplexity AI\",\n    value: \"perplexity\",\n    logo: PerplexityLogo,\n    options: (settings) => <PerplexityOptions settings={settings} />,\n    description:\n      \"Run powerful and internet-connected models hosted by Perplexity AI.\",\n    requiredConfig: [\"PerplexityApiKey\"],\n  },\n  {\n    name: \"OpenRouter\",\n    value: \"openrouter\",\n    logo: OpenRouterLogo,\n    options: (settings) => <OpenRouterOptions settings={settings} />,\n    description: \"A unified interface for LLMs.\",\n    requiredConfig: [\"OpenRouterApiKey\"],\n  },\n  {\n    name: \"Groq\",\n    value: \"groq\",\n    logo: GroqLogo,\n    options: (settings) => <GroqAiOptions settings={settings} />,\n    description:\n      \"The fastest LLM inferencing available for real-time AI applications.\",\n    requiredConfig: [\"GroqApiKey\"],\n  },\n  {\n    name: \"KoboldCPP\",\n    value: \"koboldcpp\",\n    logo: KoboldCPPLogo,\n    options: (settings) => <KoboldCPPOptions settings={settings} />,\n    description: \"Run local LLMs using koboldcpp.\",\n    requiredConfig: [\n      \"KoboldCPPModelPref\",\n      \"KoboldCPPBasePath\",\n      \"KoboldCPPTokenLimit\",\n    ],\n  },\n  {\n    name: \"Oobabooga Web UI\",\n    value: \"textgenwebui\",\n    logo: TextGenWebUILogo,\n    options: (settings) => <TextGenWebUIOptions settings={settings} />,\n    description: \"Run local LLMs using Oobabooga's Text Generation Web UI.\",\n    requiredConfig: [\"TextGenWebUIBasePath\", \"TextGenWebUITokenLimit\"],\n  },\n  {\n    name: \"Cohere\",\n    value: \"cohere\",\n    logo: CohereLogo,\n    options: (settings) => <CohereAiOptions settings={settings} />,\n    description: \"Run Cohere's powerful Command models.\",\n    requiredConfig: [\"CohereApiKey\"],\n  },\n  {\n    name: \"LiteLLM\",\n    value: \"litellm\",\n    logo: LiteLLMLogo,\n    options: (settings) => <LiteLLMOptions settings={settings} />,\n    description: \"Run LiteLLM's OpenAI compatible proxy for various LLMs.\",\n    requiredConfig: [\"LiteLLMBasePath\"],\n  },\n  {\n    name: \"DeepSeek\",\n    value: \"deepseek\",\n    logo: DeepSeekLogo,\n    options: (settings) => <DeepSeekOptions settings={settings} />,\n    description: \"Run DeepSeek's powerful LLMs.\",\n    requiredConfig: [\"DeepSeekApiKey\"],\n  },\n  {\n    name: \"PPIO\",\n    value: \"ppio\",\n    logo: PPIOLogo,\n    options: (settings) => <PPIOLLMOptions settings={settings} />,\n    description:\n      \"Run stable and cost-efficient open-source LLM APIs, such as DeepSeek, Llama, Qwen etc.\",\n    requiredConfig: [\"PPIOApiKey\"],\n  },\n  {\n    name: \"AWS Bedrock\",\n    value: \"bedrock\",\n    logo: AWSBedrockLogo,\n    options: (settings) => <AWSBedrockLLMOptions settings={settings} />,\n    description: \"Run powerful foundation models privately with AWS Bedrock.\",\n    requiredConfig: [\n      \"AwsBedrockLLMAccessKeyId\",\n      \"AwsBedrockLLMAccessKey\",\n      \"AwsBedrockLLMRegion\",\n      \"AwsBedrockLLMModel\",\n    ],\n  },\n  {\n    name: \"APIpie\",\n    value: \"apipie\",\n    logo: APIPieLogo,\n    options: (settings) => <ApiPieLLMOptions settings={settings} />,\n    description: \"A unified API of AI services from leading providers\",\n    requiredConfig: [\"ApipieLLMApiKey\", \"ApipieLLMModelPref\"],\n  },\n  {\n    name: \"Moonshot AI\",\n    value: \"moonshotai\",\n    logo: MoonshotAiLogo,\n    options: (settings) => <MoonshotAiOptions settings={settings} />,\n    description: \"Run Moonshot AI's powerful LLMs.\",\n    requiredConfig: [\"MoonshotAiApiKey\"],\n  },\n  {\n    name: \"Privatemode\",\n    value: \"privatemode\",\n    logo: PrivateModeLogo,\n    options: (settings) => <PrivateModeOptions settings={settings} />,\n    description: \"Run LLMs with end-to-end encryption.\",\n    requiredConfig: [\"PrivateModeBasePath\"],\n  },\n  {\n    name: \"Novita AI\",\n    value: \"novita\",\n    logo: NovitaLogo,\n    options: (settings) => <NovitaLLMOptions settings={settings} />,\n    description:\n      \"Reliable, Scalable, and Cost-Effective for LLMs from Novita AI\",\n    requiredConfig: [\"NovitaLLMApiKey\"],\n  },\n  {\n    name: \"CometAPI\",\n    value: \"cometapi\",\n    logo: CometApiLogo,\n    options: (settings) => <CometApiLLMOptions settings={settings} />,\n    description: \"500+ AI Models all in one API.\",\n    requiredConfig: [\"CometApiLLMApiKey\"],\n  },\n  {\n    name: \"Microsoft Foundry Local\",\n    value: \"foundry\",\n    logo: FoundryLogo,\n    options: (settings) => <FoundryOptions settings={settings} />,\n    description: \"Run Microsoft's Foundry models locally.\",\n    requiredConfig: [\n      \"FoundryBasePath\",\n      \"FoundryModelPref\",\n      \"FoundryModelTokenLimit\",\n    ],\n  },\n  {\n    name: \"xAI\",\n    value: \"xai\",\n    logo: XAILogo,\n    options: (settings) => <XAILLMOptions settings={settings} />,\n    description: \"Run xAI's powerful LLMs like Grok-2 and more.\",\n    requiredConfig: [\"XAIApiKey\", \"XAIModelPref\"],\n  },\n  {\n    name: \"Z.AI\",\n    value: \"zai\",\n    logo: ZAiLogo,\n    options: (settings) => <ZAiLLMOptions settings={settings} />,\n    description: \"Run Z.AI's powerful GLM models.\",\n    requiredConfig: [\"ZAiApiKey\"],\n  },\n  {\n    name: \"GiteeAI\",\n    value: \"giteeai\",\n    logo: GiteeAILogo,\n    options: (settings) => <GiteeAIOptions settings={settings} />,\n    description: \"Run GiteeAI's powerful LLMs.\",\n    requiredConfig: [\"GiteeAIApiKey\"],\n  },\n  {\n    name: \"Generic OpenAI\",\n    value: \"generic-openai\",\n    logo: GenericOpenAiLogo,\n    options: (settings) => <GenericOpenAiOptions settings={settings} />,\n    description:\n      \"Connect to any OpenAi-compatible service via a custom configuration\",\n    requiredConfig: [\n      \"GenericOpenAiBasePath\",\n      \"GenericOpenAiModelPref\",\n      \"GenericOpenAiTokenLimit\",\n      \"GenericOpenAiKey\",\n    ],\n  },\n];\n\nexport const LLM_PREFERENCE_CHANGED_EVENT = \"llm-preference-changed\";\nexport default function GeneralLLMPreference() {\n  const [saving, setSaving] = useState(false);\n  const [hasChanges, setHasChanges] = useState(false);\n  const [settings, setSettings] = useState(null);\n  const [loading, setLoading] = useState(true);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [filteredLLMs, setFilteredLLMs] = useState([]);\n  const [selectedLLM, setSelectedLLM] = useState(null);\n  const [searchMenuOpen, setSearchMenuOpen] = useState(false);\n  const searchInputRef = useRef(null);\n  const { t } = useTranslation();\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = e.target;\n    const data = { LLMProvider: selectedLLM };\n    const formData = new FormData(form);\n\n    for (var [key, value] of formData.entries()) data[key] = value;\n    const { error } = await System.updateSystem(data);\n    setSaving(true);\n\n    if (error) {\n      showToast(`Failed to save LLM settings: ${error}`, \"error\");\n    } else {\n      showToast(\"LLM preferences saved successfully.\", \"success\");\n    }\n    setSaving(false);\n    setHasChanges(!!error);\n  };\n\n  const updateLLMChoice = (selection) => {\n    setSearchQuery(\"\");\n    setSelectedLLM(selection);\n    setSearchMenuOpen(false);\n    setHasChanges(true);\n  };\n\n  const handleXButton = () => {\n    if (searchQuery.length > 0) {\n      setSearchQuery(\"\");\n      if (searchInputRef.current) searchInputRef.current.value = \"\";\n    } else {\n      setSearchMenuOpen(!searchMenuOpen);\n    }\n  };\n\n  useEffect(() => {\n    async function fetchKeys() {\n      const _settings = await System.keys();\n      setSettings(_settings);\n      setSelectedLLM(_settings?.LLMProvider);\n      setLoading(false);\n    }\n    fetchKeys();\n  }, []);\n\n  // Some more complex LLM options do not bubble up the change event, so we need to listen to the custom event\n  // we can emit from the LLM options component using window.dispatchEvent(new Event(LLM_PREFERENCE_CHANGED_EVENT));\n  useEffect(() => {\n    function updateHasChanges() {\n      setHasChanges(true);\n    }\n    window.addEventListener(LLM_PREFERENCE_CHANGED_EVENT, updateHasChanges);\n    return () => {\n      window.removeEventListener(\n        LLM_PREFERENCE_CHANGED_EVENT,\n        updateHasChanges\n      );\n    };\n  }, []);\n\n  useEffect(() => {\n    const filtered = AVAILABLE_LLM_PROVIDERS.filter((llm) =>\n      llm.name.toLowerCase().includes(searchQuery.toLowerCase())\n    );\n    setFilteredLLMs(filtered);\n  }, [searchQuery, selectedLLM]);\n\n  const selectedLLMObject = AVAILABLE_LLM_PROVIDERS.find(\n    (llm) => llm.value === selectedLLM\n  );\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      {loading ? (\n        <div\n          style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n          className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n        >\n          <div className=\"w-full h-full flex justify-center items-center\">\n            <PreLoader />\n          </div>\n        </div>\n      ) : (\n        <div\n          style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n          className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n        >\n          <form onSubmit={handleSubmit} className=\"flex w-full\">\n            <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n              <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n                <div className=\"flex gap-x-4 items-center\">\n                  <p className=\"text-lg leading-6 font-bold text-white\">\n                    {t(\"llm.title\")}\n                  </p>\n                </div>\n                <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n                  {t(\"llm.description\")}\n                </p>\n              </div>\n              <div className=\"w-full justify-end flex\">\n                {hasChanges && (\n                  <CTAButton\n                    onClick={() => handleSubmit()}\n                    className=\"mt-3 mr-0 -mb-14 z-10\"\n                  >\n                    {saving ? \"Saving...\" : \"Save changes\"}\n                  </CTAButton>\n                )}\n              </div>\n              <div className=\"text-base font-bold text-white mt-6 mb-4\">\n                {t(\"llm.provider\")}\n              </div>\n              <div className=\"relative\">\n                {searchMenuOpen && (\n                  <div\n                    className=\"fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10\"\n                    onClick={() => setSearchMenuOpen(false)}\n                  />\n                )}\n                {searchMenuOpen ? (\n                  <div className=\"absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] min-h-[64px] bg-theme-settings-input-bg rounded-lg flex flex-col justify-between cursor-pointer border-2 border-primary-button z-20\">\n                    <div className=\"w-full flex flex-col gap-y-1\">\n                      <div className=\"flex items-center sticky top-0 z-10 border-b border-[#9CA3AF] mx-4 bg-theme-settings-input-bg\">\n                        <MagnifyingGlass\n                          size={20}\n                          weight=\"bold\"\n                          className=\"absolute left-4 z-30 text-theme-text-primary -ml-4 my-2\"\n                        />\n                        <input\n                          type=\"text\"\n                          name=\"llm-search\"\n                          autoComplete=\"off\"\n                          placeholder=\"Search all LLM providers\"\n                          className=\"border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none text-theme-text-primary placeholder:text-theme-text-primary placeholder:font-medium\"\n                          onChange={(e) => setSearchQuery(e.target.value)}\n                          ref={searchInputRef}\n                          onKeyDown={(e) => {\n                            if (e.key === \"Enter\") e.preventDefault();\n                          }}\n                        />\n                        <X\n                          size={20}\n                          weight=\"bold\"\n                          className=\"cursor-pointer text-white hover:text-x-button\"\n                          onClick={handleXButton}\n                        />\n                      </div>\n                      <div className=\"flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4 max-h-[245px]\">\n                        {filteredLLMs.map((llm) => {\n                          return (\n                            <LLMItem\n                              key={llm.name}\n                              name={llm.name}\n                              value={llm.value}\n                              image={llm.logo}\n                              description={llm.description}\n                              checked={selectedLLM === llm.value}\n                              onClick={() => updateLLMChoice(llm.value)}\n                            />\n                          );\n                        })}\n                      </div>\n                    </div>\n                  </div>\n                ) : (\n                  <button\n                    className=\"w-full max-w-[640px] h-[64px] bg-theme-settings-input-bg rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-primary-button transition-all duration-300\"\n                    type=\"button\"\n                    onClick={() => setSearchMenuOpen(true)}\n                  >\n                    <div className=\"flex gap-x-4 items-center\">\n                      <img\n                        src={selectedLLMObject?.logo || AnythingLLMIcon}\n                        alt={`${selectedLLMObject?.name} logo`}\n                        className=\"w-10 h-10 rounded-md\"\n                      />\n                      <div className=\"flex flex-col text-left\">\n                        <div className=\"text-sm font-semibold text-white\">\n                          {selectedLLMObject?.name || \"None selected\"}\n                        </div>\n                        <div className=\"mt-1 text-xs text-description\">\n                          {selectedLLMObject?.description ||\n                            \"You need to select an LLM\"}\n                        </div>\n                      </div>\n                    </div>\n                    <CaretUpDown\n                      size={24}\n                      weight=\"bold\"\n                      className=\"text-white\"\n                    />\n                  </button>\n                )}\n              </div>\n              <div\n                onChange={() => setHasChanges(true)}\n                className=\"mt-4 flex flex-col gap-y-1\"\n              >\n                {selectedLLM &&\n                  AVAILABLE_LLM_PROVIDERS.find(\n                    (llm) => llm.value === selectedLLM\n                  )?.options?.(settings)}\n              </div>\n            </div>\n          </form>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/MobileConnections/ConnectionModal/index.jsx",
    "content": "import { X } from \"@phosphor-icons/react\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport BG from \"./bg.png\";\nimport { QRCodeSVG } from \"qrcode.react\";\nimport { Link } from \"react-router-dom\";\nimport { useEffect, useState } from \"react\";\nimport MobileConnection from \"@/models/mobile\";\nimport PreLoader from \"@/components/Preloader\";\nimport Logo from \"@/media/logo/anything-llm-infinity.png\";\nimport paths from \"@/utils/paths\";\nimport GetOnGooglePlay from \"./gplay-badge.svg\";\n\nexport default function MobileConnectModal({ isOpen, onClose }) {\n  return (\n    <ModalWrapper isOpen={isOpen}>\n      <div\n        className=\"relative w-full rounded-lg shadow\"\n        style={{\n          minHeight: \"60vh\",\n          maxWidth: \"70vw\",\n          backgroundImage: `url(${BG})`,\n          backgroundSize: \"cover\",\n          backgroundPosition: \"center\",\n        }}\n      >\n        <button\n          onClick={onClose}\n          type=\"button\"\n          className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n        >\n          <X size={24} weight=\"bold\" className=\"text-[#FFF]\" />\n        </button>\n\n        <div className=\"flex w-full h-full justify-between p-[35px]\">\n          {/* left column */}\n          <div className=\"flex flex-col w-1/2 gap-y-[16px]\">\n            <p className=\"text-[#FFF] text-xl font-bold\">\n              Go mobile. Stay local. AnythingLLM Mobile.\n            </p>\n            <p className=\"text-[#FFF] text-lg\">\n              AnythingLLM for mobile allows you to connect to your workspace's\n              chats, threads, tools, and documents for you to use on the go.\n              <br />\n              <br />\n              Run with local models on your phone privately or relay chats\n              directly to this instance seamlessly.\n            </p>\n            <Link\n              to=\"https://play.google.com/store/apps/details?id=com.anythingllm\"\n              target=\"_blank\"\n            >\n              <img\n                src={GetOnGooglePlay}\n                alt=\"Get on Google Play\"\n                className=\"w-[150px] h-auto\"\n              />\n            </Link>\n          </div>\n\n          {/* right column */}\n          <div className=\"flex flex-col items-center justify-center shrink-0 w-1/2 gap-y-[16px]\">\n            <div className=\"bg-white/10 rounded-lg p-[40px] w-[300px] h-[300px] flex flex-col gap-y-[16px] items-center justify-center\">\n              <ConnectionQrCode isOpen={isOpen} />\n            </div>\n            <p className=\"text-[#FFF] text-sm w-[300px] text-center\">\n              Scan the QR code with the AnythingLLM Mobile app to enable live\n              sync of your workspaces, chats, threads and documents.\n              <br />\n              <Link\n                to={paths.documentation.mobileIntroduction()}\n                className=\"text-cta-button font-semibold\"\n              >\n                Learn more\n              </Link>\n            </p>\n          </div>\n        </div>\n      </div>\n    </ModalWrapper>\n  );\n}\n\n/**\n * Process the connection url to make it absolute if it is a relative path\n * @param {string} url\n * @returns {string}\n */\nfunction processConnectionUrl(url) {\n  /*\n   * In dev mode, the connectionURL() method uses the `ip` module\n   * see server/models/mobileDevice.js `connectionURL()` method.\n   *\n   * In prod mode, this method returns the absolute path since we will always want to use\n   * the real instance hostname. If the domain changes, we should be able to inherit it from the client side\n   * since the backend has no knowledge of the domain since typically it is run behind a reverse proxy or in a container - or both.\n   * So `ip` is useless in prod mode since it would only resolve to the internal IP address of the container or if non-containerized,\n   * the local IP address may not be the preferred instance access point (eg: using custom domain)\n   *\n   * If the url does not start with http, we assume it is a relative path and add the origin to it.\n   * Then we check if the hostname is localhost, 127.0.0.1, or 0.0.0.0. If it is, we throw an error since that is not\n   * a LAN resolvable address that other devices can use to connect to the instance.\n   */\n  if (url.startsWith(\"http\")) return new URL(url);\n  const connectionUrl = new URL(`${window.location.origin}${url}`);\n  if ([\"localhost\", \"127.0.0.1\", \"0.0.0.0\"].includes(connectionUrl.hostname))\n    throw new Error(\n      \"Please open this page via your machines private IP address or custom domain. Localhost URLs will not work with the mobile app.\"\n    );\n  return connectionUrl.toString();\n}\n\nconst ConnectionQrCode = ({ isOpen }) => {\n  const [connectionInfo, setConnectionInfo] = useState(null);\n  const [isLoading, setIsLoading] = useState(false);\n  const [error, setError] = useState(null);\n\n  useEffect(() => {\n    if (!isOpen) return;\n    setIsLoading(true);\n    MobileConnection.getConnectionInfo()\n      .then((res) => {\n        if (res.error) throw new Error(res.error);\n        const url = processConnectionUrl(res.connectionUrl);\n        setConnectionInfo(url);\n      })\n      .catch((err) => {\n        setError(err.message);\n      })\n      .finally(() => {\n        setIsLoading(false);\n      });\n  }, [isOpen]);\n\n  if (isLoading) return <PreLoader size=\"[100px]\" />;\n  if (error)\n    return (\n      <p className=\"text-red-500 text-sm w-[300px] p-4 text-center\">{error}</p>\n    );\n\n  const size = {\n    width: 35 * 1.5,\n    height: 22 * 1.5,\n  };\n  return (\n    <QRCodeSVG\n      value={connectionInfo}\n      size={300}\n      bgColor=\"transparent\"\n      fgColor=\"white\"\n      level=\"L\"\n      imageSettings={{\n        src: Logo,\n        x: 300 / 2 - size.width / 2,\n        y: 300 / 2 - size.height / 2,\n        height: size.height,\n        width: size.width,\n        excavate: true,\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/MobileConnections/DeviceRow/index.jsx",
    "content": "import showToast from \"@/utils/toast\";\nimport MobileConnection from \"@/models/mobile\";\nimport { useState } from \"react\";\nimport moment from \"moment\";\nimport { BugDroid, AppleLogo } from \"@phosphor-icons/react\";\nimport { Link } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\n\nexport default function DeviceRow({ device, removeDevice }) {\n  const [status, setStatus] = useState(device.approved);\n\n  const handleApprove = async () => {\n    await MobileConnection.updateDevice(device.id, { approved: true });\n    showToast(\"Device access granted\", \"info\");\n    setStatus(true);\n  };\n\n  const handleDeny = async () => {\n    await MobileConnection.deleteDevice(device.id);\n    showToast(\"Device access denied\", \"info\");\n    setStatus(false);\n    removeDevice(device.id);\n  };\n\n  return (\n    <>\n      <tr className=\"bg-transparent text-white text-opacity-80 text-xs font-medium border-b border-white/10 h-10\">\n        <td scope=\"row\" className=\"px-6 whitespace-nowrap\">\n          <div className=\"flex items-center gap-x-2\">\n            {device.deviceOs === \"ios\" ? (\n              <AppleLogo\n                weight=\"fill\"\n                size={16}\n                className=\"fill-theme-text-primary\"\n              />\n            ) : (\n              <BugDroid\n                weight=\"fill\"\n                size={16}\n                className=\"fill-theme-text-primary\"\n              />\n            )}\n            <span className=\"text-sm\">{device.deviceName}</span>\n          </div>\n        </td>\n        <td className=\"px-6\">\n          <div className=\"flex items-center gap-x-2\">\n            {moment(device.createdAt).format(\"lll\")}\n            {device.user && (\n              <div className=\"flex items-center gap-x-1\">\n                <span className=\"text-xs text-theme-text-secondary\">by</span>\n                <Link\n                  to={paths.settings.users()}\n                  className=\"text-xs text-theme-text-secondary hover:underline hover:text-cta-button\"\n                >\n                  {device.user.username}\n                </Link>\n              </div>\n            )}\n          </div>\n        </td>\n        <td className=\"px-6 flex items-center gap-x-6 h-full mt-1\">\n          {status ? (\n            <button\n              onClick={handleDeny}\n              className={`border-none flex items-center justify-center text-xs font-medium text-white/80 light:text-black/80 rounded-lg p-1 hover:bg-white hover:light:bg-red-50 hover:bg-opacity-10`}\n            >\n              Revoke\n            </button>\n          ) : (\n            <>\n              <button\n                onClick={handleApprove}\n                className={`border-none flex items-center justify-center text-xs font-medium text-white/80 light:text-black/80 rounded-lg p-1 hover:bg-white hover:bg-opacity-10 hover:light:bg-green-50 hover:light:text-green-500 hover:text-green-300`}\n              >\n                Approve Access\n              </button>\n              <button\n                onClick={handleDeny}\n                className={`border-none flex items-center justify-center text-xs font-medium text-white/80 light:text-black/80 rounded-lg p-1 hover:bg-white hover:bg-opacity-10 hover:light:bg-red-50 hover:light:text-red-500 hover:text-red-300`}\n              >\n                Deny\n              </button>\n            </>\n          )}\n        </td>\n      </tr>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/MobileConnections/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport { QrCode } from \"@phosphor-icons/react\";\nimport { useModal } from \"@/hooks/useModal\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport MobileConnection from \"@/models/mobile\";\nimport ConnectionModal from \"./ConnectionModal\";\nimport DeviceRow from \"./DeviceRow\";\nimport { isMobile } from \"react-device-detect\";\n\nexport default function MobileDevices() {\n  const { isOpen, openModal, closeModal } = useModal();\n  const [loading, setLoading] = useState(true);\n  const [devices, setDevices] = useState([]);\n\n  const fetchDevices = async () => {\n    const foundDevices = await MobileConnection.getDevices();\n    setDevices(foundDevices);\n    if (foundDevices.length !== 0 && !isOpen) closeModal();\n    return foundDevices;\n  };\n\n  useEffect(() => {\n    fetchDevices()\n      .then((devices) => {\n        if (devices.length === 0) openModal();\n        return devices;\n      })\n      .finally(() => {\n        setLoading(false);\n      });\n\n    const interval = setInterval(fetchDevices, 5_000);\n    return () => clearInterval(interval);\n  }, []);\n\n  const removeDevice = (id) => {\n    setDevices((prevDevices) =>\n      prevDevices.filter((device) => device.id !== id)\n    );\n  };\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex md:mt-0 mt-6\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2\">\n            <div className=\"items-center flex gap-x-4\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                Connected Mobile Devices\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary mt-2\">\n              These are the devices that are connected to your desktop\n              application to sync chats, workspaces, and more.\n            </p>\n          </div>\n          <div className=\"w-full justify-end flex\">\n            <CTAButton\n              onClick={openModal}\n              className=\"mt-3 mr-0 mb-4 md:-mb-14 z-10\"\n            >\n              <QrCode className=\"h-4 w-4\" weight=\"bold\" /> Register New Device\n            </CTAButton>\n          </div>\n          <div className=\"overflow-x-auto mt-6\">\n            {loading ? (\n              <Skeleton.default\n                height=\"80vh\"\n                width=\"100%\"\n                highlightColor=\"var(--theme-bg-primary)\"\n                baseColor=\"var(--theme-bg-secondary)\"\n                count={1}\n                className=\"w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm\"\n                containerClassName=\"flex w-full\"\n              />\n            ) : (\n              <table className=\"w-full text-xs text-left rounded-lg min-w-[640px] border-spacing-0\">\n                <thead className=\"text-theme-text-secondary text-xs leading-[18px] font-bold uppercase border-white/10 border-b\">\n                  <tr>\n                    <th scope=\"col\" className=\"px-6 py-3\">\n                      Device Name\n                    </th>\n                    <th scope=\"col\" className=\"px-6 py-3\">\n                      Registered\n                    </th>\n                    <th scope=\"col\" className=\"px-6 py-3\">\n                      {\" \"}\n                    </th>\n                  </tr>\n                </thead>\n                <tbody>\n                  {devices.length === 0 ? (\n                    <tr className=\"bg-transparent text-theme-text-secondary text-sm font-medium\">\n                      <td colSpan=\"4\" className=\"px-6 py-4 text-center\">\n                        No devices found\n                      </td>\n                    </tr>\n                  ) : (\n                    devices.map((device) => (\n                      <DeviceRow\n                        key={device.id}\n                        device={device}\n                        removeDevice={removeDevice}\n                      />\n                    ))\n                  )}\n                </tbody>\n              </table>\n            )}\n          </div>\n        </div>\n      </div>\n      <ConnectionModal isOpen={isOpen} onClose={closeModal} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/PrivacyAndData/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport showToast from \"@/utils/toast\";\nimport System from \"@/models/system\";\nimport PreLoader from \"@/components/Preloader\";\nimport { useTranslation } from \"react-i18next\";\nimport ProviderPrivacy from \"@/components/ProviderPrivacy\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nexport default function PrivacyAndDataHandling() {\n  const [settings, setSettings] = useState({});\n  const [loading, setLoading] = useState(true);\n  const { t } = useTranslation();\n  useEffect(() => {\n    async function fetchSettings() {\n      setLoading(true);\n      const settings = await System.keys();\n      setSettings(settings);\n      setLoading(false);\n    }\n    fetchSettings();\n  }, []);\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] light:border light:border-theme-sidebar-border bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white/10 border-b-2\">\n            <div className=\"items-center flex gap-x-4\">\n              <p className=\"text-lg leading-6 font-bold text-theme-text-primary\">\n                {t(\"privacy.title\")}\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-theme-text-secondary\">\n              {t(\"privacy.description\")}\n            </p>\n          </div>\n          {loading ? (\n            <div className=\"h-1/2 transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] p-[18px] h-full overflow-y-scroll\">\n              <div className=\"w-full h-full flex justify-center items-center\">\n                <PreLoader />\n              </div>\n            </div>\n          ) : (\n            <div className=\"overflow-x-auto flex flex-col gap-y-6 pt-6\">\n              <ProviderPrivacy />\n              <TelemetryLogs settings={settings} />\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction TelemetryLogs({ settings }) {\n  const [telemetry, setTelemetry] = useState(\n    settings?.DisableTelemetry !== \"true\"\n  );\n  const { t } = useTranslation();\n  async function toggleTelemetry() {\n    await System.updateSystem({\n      DisableTelemetry: !telemetry ? \"false\" : \"true\",\n    });\n    setTelemetry(!telemetry);\n    showToast(\n      `Anonymous Telemetry has been ${!telemetry ? \"enabled\" : \"disabled\"}.`,\n      \"info\",\n      { clear: true }\n    );\n  }\n\n  return (\n    <div className=\"relative w-full max-h-full\">\n      <div className=\"relative rounded-lg\">\n        <div className=\"space-y-6 flex h-full w-full\">\n          <div className=\"w-full flex flex-col gap-y-4\">\n            <div className=\"\">\n              <Toggle\n                size=\"lg\"\n                className=\"mb-4\"\n                label={t(\"privacy.anonymous\")}\n                enabled={telemetry}\n                onChange={toggleTelemetry}\n              />\n            </div>\n          </div>\n        </div>\n        <div className=\"flex flex-col items-left space-y-2\">\n          <p className=\"text-theme-text-secondary text-xs rounded-lg w-96\">\n            All events do not record IP-address and contain{\" \"}\n            <b>no identifying</b> content, settings, chats, or other non-usage\n            based information. To see the list of event tags collected you can\n            look on{\" \"}\n            <a\n              href=\"https://github.com/search?q=repo%3AMintplex-Labs%2Fanything-llm%20.sendTelemetry(&type=code\"\n              className=\"underline text-blue-400\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              GitHub here\n            </a>\n            .\n          </p>\n          <p className=\"text-theme-text-secondary text-xs rounded-lg w-96\">\n            As an open-source project we respect your right to privacy. We are\n            dedicated to building the best solution for integrating AI and\n            documents privately and securely. If you do decide to turn off\n            telemetry all we ask is to consider sending us feedback and thoughts\n            so that we can continue to improve AnythingLLM for you.{\" \"}\n            <a\n              href=\"mailto:team@mintplexlabs.com\"\n              className=\"underline text-blue-400\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              team@mintplexlabs.com\n            </a>\n            .\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Security/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport showToast from \"@/utils/toast\";\nimport System from \"@/models/system\";\nimport paths from \"@/utils/paths\";\nimport { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from \"@/utils/constants\";\nimport PreLoader from \"@/components/Preloader\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport { useTranslation } from \"react-i18next\";\nimport Toggle from \"@/components/lib/Toggle\";\nimport {\n  USERNAME_MIN_LENGTH,\n  USERNAME_MAX_LENGTH,\n  USERNAME_PATTERN,\n} from \"@/utils/username\";\n\nexport default function GeneralSecurity() {\n  const { t } = useTranslation();\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:pt-6\">\n          <p className=\"text-lg leading-6 font-bold text-theme-text-primary md-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10 py-4\">\n            {t(\"security.title\")}\n          </p>\n        </div>\n        <MultiUserMode />\n        <PasswordProtection />\n      </div>\n    </div>\n  );\n}\n\nfunction MultiUserMode() {\n  const [saving, setSaving] = useState(false);\n  const [hasChanges, setHasChanges] = useState(false);\n  const [useMultiUserMode, setUseMultiUserMode] = useState(false);\n  const [multiUserModeEnabled, setMultiUserModeEnabled] = useState(false);\n  const [loading, setLoading] = useState(true);\n  const { t } = useTranslation();\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    setSaving(true);\n    setHasChanges(false);\n    if (useMultiUserMode) {\n      const form = new FormData(e.target);\n      const data = {\n        username: form.get(\"username\"),\n        password: form.get(\"password\"),\n      };\n\n      const { success, error } = await System.setupMultiUser(data);\n      if (success) {\n        showToast(\"Multi-User mode enabled successfully.\", \"success\");\n        setSaving(false);\n        setTimeout(() => {\n          window.localStorage.removeItem(AUTH_USER);\n          window.localStorage.removeItem(AUTH_TOKEN);\n          window.localStorage.removeItem(AUTH_TIMESTAMP);\n          window.location = paths.settings.users();\n        }, 2_000);\n        return;\n      }\n\n      showToast(`Failed to enable Multi-User mode: ${error}`, \"error\");\n      setSaving(false);\n      return;\n    }\n  };\n\n  useEffect(() => {\n    async function fetchIsMultiUserMode() {\n      setLoading(true);\n      const multiUserModeEnabled = await System.isMultiUserMode();\n      setMultiUserModeEnabled(multiUserModeEnabled);\n      setLoading(false);\n    }\n    fetchIsMultiUserMode();\n  }, []);\n\n  if (loading) {\n    return (\n      <div className=\"h-1/2 transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] p-[18px] h-full overflow-y-scroll\">\n        <div className=\"w-full h-full flex justify-center items-center\">\n          <PreLoader />\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <form\n      onSubmit={handleSubmit}\n      onChange={() => setHasChanges(true)}\n      className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px]\"\n    >\n      <div className=\"w-full flex flex-col gap-y-1 w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n        <div className=\"w-full flex flex-col gap-y-1\">\n          <div className=\"items-center flex gap-x-4\">\n            <p className=\"text-base font-bold text-white mt-6\">\n              {t(\"security.multiuser.title\")}\n            </p>\n          </div>\n          <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n            {t(\"security.multiuser.description\")}\n          </p>\n        </div>\n        {hasChanges && (\n          <div className=\"flex justify-end\">\n            <CTAButton\n              onClick={() => handleSubmit()}\n              className=\"mt-3 mr-0 -mb-20 z-10\"\n            >\n              {saving ? t(\"common.saving\") : t(\"common.save\")}\n            </CTAButton>\n          </div>\n        )}\n        <div className=\"relative w-full max-h-full\">\n          <div className=\"relative rounded-lg\">\n            <div className=\"flex items-start justify-between px-6 py-4\"></div>\n            <div className=\"space-y-6 flex h-full w-full\">\n              <div className=\"w-full flex flex-col gap-y-4\">\n                {multiUserModeEnabled ? (\n                  <p className=\"text-white text-sm font-semibold\">\n                    {t(\"security.multiuser.enable.is-enable\")}\n                  </p>\n                ) : (\n                  <Toggle\n                    size=\"lg\"\n                    className=\"mb-4\"\n                    label={t(\"security.multiuser.enable.enable\")}\n                    enabled={useMultiUserMode}\n                    onChange={(checked) => setUseMultiUserMode(checked)}\n                  />\n                )}\n                {useMultiUserMode && (\n                  <div className=\"w-full flex flex-col gap-y-2 my-5\">\n                    <div className=\"w-80\">\n                      <label\n                        htmlFor=\"username\"\n                        className=\"text-white text-sm font-semibold block mb-3\"\n                      >\n                        {t(\"security.multiuser.enable.username\")}\n                      </label>\n                      <input\n                        name=\"username\"\n                        type=\"text\"\n                        className=\"border-none bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 placeholder:text-theme-settings-input-placeholder focus:ring-blue-500\"\n                        placeholder=\"Your admin username\"\n                        minLength={USERNAME_MIN_LENGTH}\n                        maxLength={USERNAME_MAX_LENGTH}\n                        pattern={USERNAME_PATTERN}\n                        required={true}\n                        autoComplete=\"off\"\n                        disabled={multiUserModeEnabled}\n                        defaultValue={multiUserModeEnabled ? \"********\" : \"\"}\n                      />\n                      <p className=\"text-white text-opacity-60 text-xs mt-2\">\n                        {t(\"common.username_requirements\")}\n                      </p>\n                    </div>\n                    <div className=\"mt-4 w-80\">\n                      <label\n                        htmlFor=\"password\"\n                        className=\"text-white text-sm font-semibold block mb-3\"\n                      >\n                        {t(\"security.multiuser.enable.password\")}\n                      </label>\n                      <input\n                        name=\"password\"\n                        type=\"text\"\n                        className=\"border-none bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 placeholder:text-theme-settings-input-placeholder focus:ring-blue-500\"\n                        placeholder=\"Your admin password\"\n                        minLength={8}\n                        required={true}\n                        autoComplete=\"off\"\n                        defaultValue={multiUserModeEnabled ? \"********\" : \"\"}\n                      />\n                    </div>\n                  </div>\n                )}\n              </div>\n            </div>\n            <div className=\"flex items-center justify-between space-x-14\">\n              <p className=\"text-white text-opacity-80 text-xs rounded-lg w-96\">\n                {t(\"security.multiuser.enable.description\")}\n              </p>\n            </div>\n          </div>\n        </div>\n      </div>\n    </form>\n  );\n}\n\nexport const PW_REGEX = new RegExp(/^[a-zA-Z0-9_\\-!@$%^&*();]+$/);\nfunction PasswordProtection() {\n  const [saving, setSaving] = useState(false);\n  const [hasChanges, setHasChanges] = useState(false);\n  const [multiUserModeEnabled, setMultiUserModeEnabled] = useState(false);\n  const [usePassword, setUsePassword] = useState(false);\n  const [loading, setLoading] = useState(true);\n  const { t } = useTranslation();\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    if (multiUserModeEnabled) return false;\n    const form = new FormData(e.target);\n\n    if (!PW_REGEX.test(form.get(\"password\"))) {\n      showToast(\n        `Your password has restricted characters in it. Allowed symbols are _,-,!,@,$,%,^,&,*,(,),;`,\n        \"error\"\n      );\n      setSaving(false);\n      return;\n    }\n\n    setSaving(true);\n    setHasChanges(false);\n    const data = {\n      usePassword,\n      newPassword: form.get(\"password\"),\n    };\n\n    const { success, error } = await System.updateSystemPassword(data);\n    if (success) {\n      showToast(\"Your page will refresh in a few seconds.\", \"success\");\n      setSaving(false);\n      setTimeout(() => {\n        window.localStorage.removeItem(AUTH_USER);\n        window.localStorage.removeItem(AUTH_TOKEN);\n        window.localStorage.removeItem(AUTH_TIMESTAMP);\n        window.location.reload();\n      }, 3_000);\n      return;\n    } else {\n      showToast(`Failed to update password: ${error}`, \"error\");\n      setSaving(false);\n    }\n  };\n\n  useEffect(() => {\n    async function fetchIsMultiUserMode() {\n      setLoading(true);\n      const multiUserModeEnabled = await System.isMultiUserMode();\n      const settings = await System.keys();\n      setMultiUserModeEnabled(multiUserModeEnabled);\n      setUsePassword(settings?.RequiresAuth);\n      setLoading(false);\n    }\n    fetchIsMultiUserMode();\n  }, []);\n\n  if (loading) {\n    return (\n      <div className=\"h-1/2 transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] p-[18px] h-full overflow-y-scroll\">\n        <div className=\"w-full h-full flex justify-center items-center\">\n          <PreLoader />\n        </div>\n      </div>\n    );\n  }\n\n  if (multiUserModeEnabled) return null;\n  return (\n    <form\n      onSubmit={handleSubmit}\n      onChange={() => setHasChanges(true)}\n      className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px]\"\n    >\n      <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n        <div className=\"w-full flex flex-col gap-y-1\">\n          <div className=\"items-center flex gap-x-4\">\n            <p className=\"text-base font-bold text-white mt-6\">\n              {t(\"security.password.title\")}\n            </p>\n          </div>\n          <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n            {t(\"security.password.description\")}\n          </p>\n        </div>\n        {hasChanges && (\n          <div className=\"flex justify-end\">\n            <CTAButton\n              onClick={() => handleSubmit()}\n              className=\"mt-3 mr-0 -mb-20 z-10\"\n            >\n              {saving ? t(\"common.saving\") : t(\"common.save\")}\n            </CTAButton>\n          </div>\n        )}\n        <div className=\"relative w-full max-h-full\">\n          <div className=\"relative rounded-lg\">\n            <div className=\"flex items-start justify-between px-6 py-4\"></div>\n            <div className=\"space-y-6 flex h-full w-full\">\n              <div className=\"w-full flex flex-col gap-y-4\">\n                <Toggle\n                  size=\"lg\"\n                  className=\"mb-4\"\n                  label={t(\"security.password.title\")}\n                  enabled={usePassword}\n                  onChange={(checked) => setUsePassword(checked)}\n                />\n                {usePassword && (\n                  <div className=\"w-full flex flex-col gap-y-2 my-5\">\n                    <div className=\"mt-4 w-80\">\n                      <label\n                        htmlFor=\"password\"\n                        className=\"text-white text-sm font-semibold block mb-3\"\n                      >\n                        {t(\"security.password.password-label\")}\n                      </label>\n                      <input\n                        name=\"password\"\n                        type=\"text\"\n                        className=\"border-none bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 placeholder:text-theme-settings-input-placeholder\"\n                        placeholder=\"Your Instance Password\"\n                        minLength={8}\n                        required={true}\n                        autoComplete=\"off\"\n                        defaultValue={usePassword ? \"********\" : \"\"}\n                      />\n                    </div>\n                  </div>\n                )}\n              </div>\n            </div>\n            <div className=\"flex items-center justify-between space-x-14\">\n              <p className=\"text-white text-opacity-80 light:text-theme-text text-xs rounded-lg w-96\">\n                {t(\"security.password.description\")}\n              </p>\n            </div>\n          </div>\n        </div>\n      </div>\n    </form>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/Branding/index.jsx",
    "content": "import Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport FooterCustomization from \"../components/FooterCustomization\";\nimport SupportEmail from \"../components/SupportEmail\";\nimport CustomLogo from \"../components/CustomLogo\";\nimport { useTranslation } from \"react-i18next\";\nimport CustomAppName from \"../components/CustomAppName\";\nimport CustomSiteSettings from \"../components/CustomSiteSettings\";\n\nexport default function BrandingSettings() {\n  const { t } = useTranslation();\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n            <div className=\"items-center\">\n              <p className=\"text-lg leading-6 font-bold text-white\">\n                {t(\"customization.branding.title\")}\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n              {t(\"customization.branding.description\")}\n            </p>\n          </div>\n          <CustomAppName />\n          <CustomLogo />\n          <FooterCustomization />\n          <SupportEmail />\n          <CustomSiteSettings />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/Chat/index.jsx",
    "content": "import Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport { useTranslation } from \"react-i18next\";\nimport AutoSubmit from \"../components/AutoSubmit\";\nimport AutoSpeak from \"../components/AutoSpeak\";\nimport SpellCheck from \"../components/SpellCheck\";\nimport ShowScrollbar from \"../components/ShowScrollbar\";\nimport ChatRenderHTML from \"../components/ChatRenderHTML\";\n\nexport default function ChatSettings() {\n  const { t } = useTranslation();\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n            <div className=\"items-center\">\n              <p className=\"text-lg leading-6 font-bold text-white\">\n                {t(\"customization.chat.title\")}\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n              {t(\"customization.chat.description\")}\n            </p>\n          </div>\n          <AutoSubmit />\n          <AutoSpeak />\n          <SpellCheck />\n          <ShowScrollbar />\n          <ChatRenderHTML />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/Interface/index.jsx",
    "content": "import Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport { useTranslation } from \"react-i18next\";\nimport LanguagePreference from \"../components/LanguagePreference\";\nimport ThemePreference from \"../components/ThemePreference\";\n\nexport default function InterfaceSettings() {\n  const { t } = useTranslation();\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n      >\n        <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16\">\n          <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n            <div className=\"items-center\">\n              <p className=\"text-lg leading-6 font-bold text-white\">\n                {t(\"customization.interface.title\")}\n              </p>\n            </div>\n            <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n              {t(\"customization.interface.description\")}\n            </p>\n          </div>\n          <ThemePreference />\n          <LanguagePreference />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/components/AutoSpeak/index.jsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport Appearance from \"@/models/appearance\";\nimport { useTranslation } from \"react-i18next\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nexport default function AutoSpeak() {\n  const [saving, setSaving] = useState(false);\n  const [autoPlayAssistantTtsResponse, setAutoPlayAssistantTtsResponse] =\n    useState(false);\n  const { t } = useTranslation();\n\n  const handleChange = async (checked) => {\n    setAutoPlayAssistantTtsResponse(checked);\n    setSaving(true);\n    try {\n      Appearance.updateSettings({ autoPlayAssistantTtsResponse: checked });\n    } catch (error) {\n      console.error(\"Failed to update appearance settings:\", error);\n      setAutoPlayAssistantTtsResponse(!checked);\n    }\n    setSaving(false);\n  };\n\n  useEffect(() => {\n    function fetchSettings() {\n      const settings = Appearance.getSettings();\n      setAutoPlayAssistantTtsResponse(\n        settings.autoPlayAssistantTtsResponse ?? false\n      );\n    }\n    fetchSettings();\n  }, []);\n\n  return (\n    <div className=\"my-4\">\n      <Toggle\n        size=\"md\"\n        variant=\"horizontal\"\n        enabled={autoPlayAssistantTtsResponse}\n        onChange={handleChange}\n        disabled={saving}\n        label={t(\"customization.chat.auto_speak.title\")}\n        description={t(\"customization.chat.auto_speak.description\")}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/components/AutoSubmit/index.jsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport Appearance from \"@/models/appearance\";\nimport { useTranslation } from \"react-i18next\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nexport default function AutoSubmit() {\n  const [saving, setSaving] = useState(false);\n  const [autoSubmitSttInput, setAutoSubmitSttInput] = useState(true);\n  const { t } = useTranslation();\n\n  const handleChange = async (checked) => {\n    setAutoSubmitSttInput(checked);\n    setSaving(true);\n    try {\n      Appearance.updateSettings({ autoSubmitSttInput: checked });\n    } catch (error) {\n      console.error(\"Failed to update appearance settings:\", error);\n      setAutoSubmitSttInput(!checked);\n    }\n    setSaving(false);\n  };\n\n  useEffect(() => {\n    function fetchSettings() {\n      const settings = Appearance.getSettings();\n      setAutoSubmitSttInput(settings.autoSubmitSttInput ?? true);\n    }\n    fetchSettings();\n  }, []);\n\n  return (\n    <div className=\"my-4\">\n      <Toggle\n        size=\"md\"\n        variant=\"horizontal\"\n        enabled={autoSubmitSttInput}\n        onChange={handleChange}\n        disabled={saving}\n        label={t(\"customization.chat.auto_submit.title\")}\n        description={t(\"customization.chat.auto_submit.description\")}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/components/ChatRenderHTML/index.jsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport Appearance from \"@/models/appearance\";\nimport { useTranslation } from \"react-i18next\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nexport default function ChatRenderHTML() {\n  const { t } = useTranslation();\n  const [saving, setSaving] = useState(false);\n  const [renderHTML, setRenderHTML] = useState(false);\n\n  const handleChange = async (checked) => {\n    setRenderHTML(checked);\n    setSaving(true);\n    try {\n      Appearance.updateSettings({ renderHTML: checked });\n    } catch (error) {\n      console.error(\"Failed to update appearance settings:\", error);\n      setRenderHTML(!checked);\n    }\n    setSaving(false);\n  };\n\n  useEffect(() => {\n    function fetchSettings() {\n      const settings = Appearance.getSettings();\n      setRenderHTML(settings.renderHTML);\n    }\n    fetchSettings();\n  }, []);\n\n  return (\n    <div className=\"my-4\">\n      <Toggle\n        size=\"md\"\n        variant=\"horizontal\"\n        enabled={renderHTML}\n        onChange={handleChange}\n        disabled={saving}\n        label={t(\"customization.items.render-html.title\")}\n        description={t(\"customization.items.render-html.description\")}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/components/CustomAppName/index.jsx",
    "content": "import Admin from \"@/models/admin\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function CustomAppName() {\n  const { t } = useTranslation();\n  const [loading, setLoading] = useState(true);\n  const [hasChanges, setHasChanges] = useState(false);\n  const [customAppName, setCustomAppName] = useState(\"\");\n  const [originalAppName, setOriginalAppName] = useState(\"\");\n  const [canCustomize, setCanCustomize] = useState(false);\n\n  useEffect(() => {\n    const fetchInitialParams = async () => {\n      const settings = await System.keys();\n      if (!settings?.MultiUserMode && !settings?.RequiresAuth) {\n        setCanCustomize(false);\n        return false;\n      }\n\n      const { appName } = await System.fetchCustomAppName();\n      setCustomAppName(appName || \"\");\n      setOriginalAppName(appName || \"\");\n      setCanCustomize(true);\n      setLoading(false);\n    };\n    fetchInitialParams();\n  }, []);\n\n  const updateCustomAppName = async (e, newValue = null) => {\n    e.preventDefault();\n    let custom_app_name = newValue;\n    if (newValue === null) {\n      const form = new FormData(e.target);\n      custom_app_name = form.get(\"customAppName\");\n    }\n    const { success, error } = await Admin.updateSystemPreferences({\n      custom_app_name,\n    });\n    if (!success) {\n      showToast(`Failed to update custom app name: ${error}`, \"error\");\n      return;\n    } else {\n      showToast(\"Successfully updated custom app name.\", \"success\");\n      window.localStorage.removeItem(System.cacheKeys.customAppName);\n      setCustomAppName(custom_app_name);\n      setOriginalAppName(custom_app_name);\n      setHasChanges(false);\n    }\n  };\n\n  const handleChange = (e) => {\n    setCustomAppName(e.target.value);\n    setHasChanges(true);\n  };\n\n  if (!canCustomize || loading) return null;\n\n  return (\n    <form\n      className=\"flex flex-col gap-y-0.5 mt-4\"\n      onSubmit={updateCustomAppName}\n    >\n      <p className=\"text-sm leading-6 font-semibold text-white\">\n        {t(\"customization.items.app-name.title\")}\n      </p>\n      <p className=\"text-xs text-white/60\">\n        {t(\"customization.items.app-name.description\")}\n      </p>\n      <div className=\"flex items-center gap-x-4\">\n        <input\n          name=\"customAppName\"\n          type=\"text\"\n          className=\"border-none bg-theme-settings-input-bg mt-2 text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-fit py-2 px-4\"\n          placeholder=\"AnythingLLM\"\n          required={true}\n          autoComplete=\"off\"\n          onChange={handleChange}\n          value={customAppName}\n        />\n        {originalAppName !== \"\" && (\n          <button\n            type=\"button\"\n            onClick={(e) => updateCustomAppName(e, \"\")}\n            className=\"text-white text-base font-medium hover:text-opacity-60\"\n          >\n            Clear\n          </button>\n        )}\n      </div>\n      {hasChanges && (\n        <button\n          type=\"submit\"\n          className=\"transition-all mt-2 w-fit duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800\"\n        >\n          Save\n        </button>\n      )}\n    </form>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/components/CustomLogo/index.jsx",
    "content": "import useLogo from \"@/hooks/useLogo\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Plus } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function CustomLogo() {\n  const { t } = useTranslation();\n  const { logo: _initLogo, setLogo: _setLogo } = useLogo();\n  const [logo, setLogo] = useState(\"\");\n  const [isDefaultLogo, setIsDefaultLogo] = useState(true);\n  const fileInputRef = useRef(null);\n\n  useEffect(() => {\n    async function logoInit() {\n      setLogo(_initLogo || \"\");\n      const _isDefaultLogo = await System.isDefaultLogo();\n      setIsDefaultLogo(_isDefaultLogo);\n    }\n    logoInit();\n  }, [_initLogo]);\n\n  const handleFileUpload = async (event) => {\n    const file = event.target.files[0];\n    if (!file) return false;\n\n    const objectURL = URL.createObjectURL(file);\n    setLogo(objectURL);\n\n    const formData = new FormData();\n    formData.append(\"logo\", file);\n    const { success, error } = await System.uploadLogo(formData);\n    if (!success) {\n      showToast(`Failed to upload logo: ${error}`, \"error\");\n      setLogo(_initLogo);\n      return;\n    }\n\n    const { logoURL } = await System.fetchLogo();\n    _setLogo(logoURL);\n\n    showToast(\"Image uploaded successfully.\", \"success\");\n    setIsDefaultLogo(false);\n  };\n\n  const handleRemoveLogo = async () => {\n    setLogo(\"\");\n    setIsDefaultLogo(true);\n\n    const { success, error } = await System.removeCustomLogo();\n    if (!success) {\n      console.error(\"Failed to remove logo:\", error);\n      showToast(`Failed to remove logo: ${error}`, \"error\");\n      const { logoURL } = await System.fetchLogo();\n      setLogo(logoURL);\n      setIsDefaultLogo(false);\n      return;\n    }\n\n    const { logoURL } = await System.fetchLogo();\n    _setLogo(logoURL);\n\n    showToast(\"Image successfully removed.\", \"success\");\n  };\n\n  const triggerFileInputClick = () => {\n    fileInputRef.current?.click();\n  };\n\n  return (\n    <div className=\"flex flex-col gap-y-0.5 my-4\">\n      <p className=\"text-sm leading-6 font-semibold text-white\">\n        {t(\"customization.items.logo.title\")}\n      </p>\n      <p className=\"text-xs text-white/60\">\n        {t(\"customization.items.logo.description\")}\n      </p>\n      {isDefaultLogo ? (\n        <div className=\"flex md:flex-row flex-col items-center\">\n          <div className=\"flex flex-row gap-x-8\">\n            <label\n              className=\"mt-3 transition-all duration-300 hover:opacity-60\"\n              hidden={!isDefaultLogo}\n            >\n              <input\n                id=\"logo-upload\"\n                type=\"file\"\n                accept=\"image/*\"\n                className=\"hidden\"\n                onChange={handleFileUpload}\n              />\n              <div\n                className=\"w-80 py-4 bg-theme-settings-input-bg rounded-2xl border-2 border-dashed border-theme-text-secondary border-opacity-60 justify-center items-center inline-flex cursor-pointer\"\n                htmlFor=\"logo-upload\"\n              >\n                <div className=\"flex flex-col items-center justify-center\">\n                  <div className=\"rounded-full bg-white/40\">\n                    <Plus className=\"w-6 h-6 text-black/80 m-2\" />\n                  </div>\n                  <div className=\"text-theme-text-primary text-opacity-80 text-sm font-semibold py-1\">\n                    {t(\"customization.items.logo.add\")}\n                  </div>\n                  <div className=\"text-theme-text-secondary text-opacity-60 text-xs font-medium py-1\">\n                    {t(\"customization.items.logo.recommended\")}\n                  </div>\n                </div>\n              </div>\n            </label>\n          </div>\n        </div>\n      ) : (\n        <div className=\"flex md:flex-row flex-col items-center relative\">\n          <div className=\"group w-80 h-[130px] mt-3 overflow-hidden\">\n            <img\n              src={logo}\n              alt=\"Uploaded Logo\"\n              className=\"w-full h-full object-cover border-2 border-theme-text-secondary border-opacity-60 p-1 rounded-2xl\"\n            />\n\n            <div className=\"absolute w-80 top-0 left-0 right-0 bottom-0 flex flex-col gap-y-3 justify-center items-center rounded-2xl mt-3 bg-black bg-opacity-80 opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-in-out border-2 border-transparent hover:border-white\">\n              <button\n                onClick={triggerFileInputClick}\n                className=\"text-[#FFFFFF] text-base font-medium hover:text-opacity-60 mx-2\"\n              >\n                {t(\"customization.items.logo.replace\")}\n              </button>\n\n              <input\n                id=\"logo-upload\"\n                type=\"file\"\n                accept=\"image/*\"\n                className=\"hidden\"\n                onChange={handleFileUpload}\n                ref={fileInputRef}\n              />\n              <button\n                onClick={handleRemoveLogo}\n                className=\"text-[#FFFFFF] text-base font-medium hover:text-opacity-60 mx-2\"\n              >\n                {t(\"customization.items.logo.remove\")}\n              </button>\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/components/CustomSiteSettings/index.jsx",
    "content": "import { useEffect, useState } from \"react\";\nimport Admin from \"@/models/admin\";\nimport showToast from \"@/utils/toast\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function CustomSiteSettings() {\n  const { t } = useTranslation();\n  const [hasChanges, setHasChanges] = useState(false);\n  const [settings, setSettings] = useState({\n    title: null,\n    faviconUrl: null,\n  });\n\n  useEffect(() => {\n    Admin.systemPreferencesByFields([\n      \"meta_page_title\",\n      \"meta_page_favicon\",\n    ]).then(({ settings }) => {\n      setSettings({\n        title: settings?.meta_page_title,\n        faviconUrl: settings?.meta_page_favicon,\n      });\n    });\n  }, []);\n\n  async function handleSiteSettingUpdate(e) {\n    e.preventDefault();\n    await Admin.updateSystemPreferences({\n      meta_page_title: settings.title ?? null,\n      meta_page_favicon: settings.faviconUrl ?? null,\n    });\n    showToast(\n      \"Site preferences updated! They will reflect on page reload.\",\n      \"success\",\n      { clear: true }\n    );\n    setHasChanges(false);\n    return;\n  }\n\n  return (\n    <form\n      className=\"flex flex-col gap-y-0.5 my-4 border-t border-white border-opacity-20 light:border-black/20 pt-6\"\n      onChange={() => setHasChanges(true)}\n      onSubmit={handleSiteSettingUpdate}\n    >\n      <p className=\"text-sm leading-6 font-semibold text-white\">\n        {t(\"customization.items.browser-appearance.title\")}\n      </p>\n      <p className=\"text-xs text-white/60\">\n        {t(\"customization.items.browser-appearance.description\")}\n      </p>\n\n      <div className=\"w-fit\">\n        <p className=\"text-sm leading-6 font-medium text-white mt-2\">\n          {t(\"customization.items.browser-appearance.tab.title\")}\n        </p>\n        <p className=\"text-xs text-white/60\">\n          {t(\"customization.items.browser-appearance.tab.description\")}\n        </p>\n        <div className=\"flex items-center gap-x-4\">\n          <input\n            name=\"meta_page_title\"\n            type=\"text\"\n            className=\"border-none bg-theme-settings-input-bg mt-2 text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-fit py-2 px-4\"\n            placeholder=\"AnythingLLM | Your personal LLM trained on anything\"\n            autoComplete=\"off\"\n            onChange={(e) => {\n              setSettings((prev) => {\n                return { ...prev, title: e.target.value };\n              });\n            }}\n            value={\n              settings.title ??\n              \"AnythingLLM | Your personal LLM trained on anything\"\n            }\n          />\n        </div>\n      </div>\n\n      <div className=\"w-fit\">\n        <p className=\"text-sm leading-6 font-medium text-white mt-2\">\n          {t(\"customization.items.browser-appearance.favicon.title\")}\n        </p>\n        <p className=\"text-xs text-white/60\">\n          {t(\"customization.items.browser-appearance.favicon.description\")}\n        </p>\n        <div className=\"flex items-center gap-x-2\">\n          <img\n            src={settings.faviconUrl ?? \"/favicon.png\"}\n            onError={(e) => (e.target.src = \"/favicon.png\")}\n            className=\"h-10 w-10 rounded-lg mt-2\"\n            alt=\"Site favicon\"\n          />\n          <input\n            name=\"meta_page_favicon\"\n            type=\"url\"\n            className=\"border-none bg-theme-settings-input-bg mt-2 text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-fit py-2 px-4\"\n            placeholder=\"url to your image\"\n            onChange={(e) => {\n              setSettings((prev) => {\n                return { ...prev, faviconUrl: e.target.value };\n              });\n            }}\n            autoComplete=\"off\"\n            value={settings.faviconUrl ?? \"\"}\n          />\n        </div>\n      </div>\n\n      {hasChanges && (\n        <button\n          type=\"submit\"\n          className=\"transition-all mt-2 w-fit duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800\"\n        >\n          Save\n        </button>\n      )}\n    </form>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/components/FooterCustomization/NewIconForm/index.jsx",
    "content": "import { ICON_COMPONENTS } from \"@/components/Footer\";\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { Plus, X } from \"@phosphor-icons/react\";\n\nexport default function NewIconForm({ icon, url, onSave, onRemove }) {\n  const [selectedIcon, setSelectedIcon] = useState(icon || \"Plus\");\n  const [selectedUrl, setSelectedUrl] = useState(url || \"\");\n  const [isDropdownOpen, setIsDropdownOpen] = useState(false);\n  const [isEdited, setIsEdited] = useState(false);\n  const dropdownRef = useRef(null);\n\n  useEffect(() => {\n    setSelectedIcon(icon || \"Plus\");\n    setSelectedUrl(url || \"\");\n    setIsEdited(false);\n  }, [icon, url]);\n\n  useEffect(() => {\n    function handleClickOutside(event) {\n      if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {\n        setIsDropdownOpen(false);\n      }\n    }\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n  }, [dropdownRef]);\n\n  const handleSubmit = (e) => {\n    e.preventDefault();\n    if (selectedIcon !== \"Plus\" && selectedUrl) {\n      onSave(selectedIcon, selectedUrl);\n      setIsEdited(false);\n    }\n  };\n\n  const handleRemove = () => {\n    onRemove();\n    setSelectedIcon(\"Plus\");\n    setSelectedUrl(\"\");\n    setIsEdited(false);\n  };\n\n  const handleIconChange = (iconName) => {\n    setSelectedIcon(iconName);\n    setIsDropdownOpen(false);\n    setIsEdited(true);\n  };\n\n  const handleUrlChange = (e) => {\n    setSelectedUrl(e.target.value);\n    setIsEdited(true);\n  };\n\n  return (\n    <form onSubmit={handleSubmit} className=\"flex items-center gap-x-1.5\">\n      <div className=\"relative\" ref={dropdownRef}>\n        <div\n          className=\"h-[34px] w-[34px] bg-theme-settings-input-bg rounded-full flex items-center justify-center cursor-pointer hover:outline-primary-button hover:outline\"\n          onClick={() => setIsDropdownOpen(!isDropdownOpen)}\n        >\n          {React.createElement(ICON_COMPONENTS[selectedIcon] || Plus, {\n            className: \"h-5 w-5\",\n            weight: selectedIcon === \"Plus\" ? \"bold\" : \"fill\",\n            color: \"var(--theme-sidebar-footer-icon-fill)\",\n          })}\n        </div>\n        {isDropdownOpen && (\n          <div className=\"absolute z-10 grid grid-cols-4 bg-theme-settings-input-bg mt-2 rounded-md w-[150px] h-[78px] overflow-y-auto border border-white/20 shadow-lg\">\n            {Object.keys(ICON_COMPONENTS).map((iconName) => (\n              <button\n                key={iconName}\n                type=\"button\"\n                className=\"flex justify-center items-center border border-transparent hover:bg-theme-sidebar-footer-icon-hover hover:border-slate-100 light:hover:border-black/80 rounded-full p-2\"\n                onClick={() => handleIconChange(iconName)}\n              >\n                {React.createElement(ICON_COMPONENTS[iconName], {\n                  className: \"h-5 w-5\",\n                  weight: \"fill\",\n                  color: \"var(--theme-sidebar-footer-icon-fill)\",\n                })}\n              </button>\n            ))}\n          </div>\n        )}\n      </div>\n      <input\n        type=\"url\"\n        value={selectedUrl}\n        onChange={handleUrlChange}\n        placeholder=\"https://example.com\"\n        className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-md p-2.5 w-[300px] h-[32px] focus:outline-primary-button active:outline-primary-button outline-none\"\n        required\n      />\n      {selectedIcon !== \"Plus\" && (\n        <>\n          {isEdited ? (\n            <button\n              type=\"submit\"\n              className=\"text-sky-400 px-2 py-2 rounded-md text-sm font-bold hover:text-sky-500\"\n            >\n              Save\n            </button>\n          ) : (\n            <button\n              type=\"button\"\n              onClick={handleRemove}\n              className=\"hover:text-red-500 text-white/80 px-2 py-2 rounded-md text-sm font-bold\"\n            >\n              <X size={20} />\n            </button>\n          )}\n        </>\n      )}\n    </form>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/components/FooterCustomization/index.jsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport showToast from \"@/utils/toast\";\nimport { safeJsonParse } from \"@/utils/request\";\nimport NewIconForm from \"./NewIconForm\";\nimport Admin from \"@/models/admin\";\nimport System from \"@/models/system\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function FooterCustomization() {\n  const [footerIcons, setFooterIcons] = useState(Array(3).fill(null));\n  const { t } = useTranslation();\n\n  useEffect(() => {\n    async function fetchFooterIcons() {\n      const { settings } = await Admin.systemPreferencesByFields([\n        \"footer_data\",\n      ]);\n\n      const footerData = settings?.footer_data;\n      if (footerData) {\n        const parsedIcons = safeJsonParse(footerData, []);\n        setFooterIcons((prevIcons) => {\n          const updatedIcons = [...prevIcons];\n          parsedIcons.forEach((icon, index) => {\n            updatedIcons[index] = icon;\n          });\n          return updatedIcons;\n        });\n      }\n    }\n    fetchFooterIcons();\n  }, []);\n\n  const updateFooterIcons = async (updatedIcons) => {\n    const { success, error } = await Admin.updateSystemPreferences({\n      footer_data: JSON.stringify(updatedIcons.filter((icon) => icon !== null)),\n    });\n\n    if (!success) {\n      showToast(`Failed to update footer icons - ${error}`, \"error\", {\n        clear: true,\n      });\n      return;\n    }\n\n    window.localStorage.removeItem(System.cacheKeys.footerIcons);\n    setFooterIcons(updatedIcons);\n    showToast(\"Successfully updated footer icons.\", \"success\", { clear: true });\n  };\n\n  const handleRemoveIcon = (index) => {\n    const updatedIcons = [...footerIcons];\n    updatedIcons[index] = null;\n    updateFooterIcons(updatedIcons);\n  };\n\n  return (\n    <div className=\"flex flex-col gap-y-0.5 my-4\">\n      <p className=\"text-sm leading-6 font-semibold text-white\">\n        {t(\"customization.items.sidebar-footer.title\")}\n      </p>\n      <p className=\"text-xs text-white/60\">\n        {t(\"customization.items.sidebar-footer.description\")}\n      </p>\n      <div className=\"mt-2 flex gap-x-3 font-medium text-white text-sm\">\n        <div>{t(\"customization.items.sidebar-footer.icon\")}</div>\n        <div>{t(\"customization.items.sidebar-footer.link\")}</div>\n      </div>\n      <div className=\"mt-2 flex flex-col gap-y-[10px]\">\n        {footerIcons.map((icon, index) => (\n          <NewIconForm\n            key={index}\n            icon={icon?.icon}\n            url={icon?.url}\n            onSave={(newIcon, newUrl) => {\n              const updatedIcons = [...footerIcons];\n              updatedIcons[index] = { icon: newIcon, url: newUrl };\n              updateFooterIcons(updatedIcons);\n            }}\n            onRemove={() => handleRemoveIcon(index)}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/components/LanguagePreference/index.jsx",
    "content": "import { useLanguageOptions } from \"@/hooks/useLanguageOptions\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function LanguagePreference() {\n  const { t } = useTranslation();\n  const {\n    currentLanguage,\n    supportedLanguages,\n    getLanguageName,\n    changeLanguage,\n  } = useLanguageOptions();\n\n  return (\n    <div className=\"flex flex-col gap-y-0.5 my-4\">\n      <p className=\"text-sm leading-6 font-semibold text-white\">\n        {t(\"customization.items.display-language.title\")}\n      </p>\n      <p className=\"text-xs text-white/60\">\n        {t(\"customization.items.display-language.description\")}\n      </p>\n      <div className=\"flex items-center gap-x-4\">\n        <select\n          name=\"userLang\"\n          className=\"border-none bg-theme-settings-input-bg mt-2 text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-fit py-2 px-4\"\n          defaultValue={currentLanguage || \"en\"}\n          onChange={(e) => changeLanguage(e.target.value)}\n        >\n          {supportedLanguages.map((lang) => {\n            return (\n              <option key={lang} value={lang}>\n                {getLanguageName(lang)}\n              </option>\n            );\n          })}\n        </select>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/components/ShowScrollbar/index.jsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport Appearance from \"@/models/appearance\";\nimport { useTranslation } from \"react-i18next\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nexport default function ShowScrollbar() {\n  const { t } = useTranslation();\n  const [saving, setSaving] = useState(false);\n  const [showScrollbar, setShowScrollbar] = useState(false);\n\n  const handleChange = async (checked) => {\n    setShowScrollbar(checked);\n    setSaving(true);\n    try {\n      Appearance.updateSettings({ showScrollbar: checked });\n    } catch (error) {\n      console.error(\"Failed to update appearance settings:\", error);\n      setShowScrollbar(!checked);\n    }\n    setSaving(false);\n  };\n\n  useEffect(() => {\n    function fetchSettings() {\n      const settings = Appearance.getSettings();\n      setShowScrollbar(settings.showScrollbar);\n    }\n    fetchSettings();\n  }, []);\n\n  return (\n    <div className=\"my-4\">\n      <Toggle\n        size=\"md\"\n        variant=\"horizontal\"\n        enabled={showScrollbar}\n        onChange={handleChange}\n        disabled={saving}\n        label={t(\"customization.items.show-scrollbar.title\")}\n        description={t(\"customization.items.show-scrollbar.description\")}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/components/SpellCheck/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport Appearance from \"@/models/appearance\";\nimport { useTranslation } from \"react-i18next\";\nimport Toggle from \"@/components/lib/Toggle\";\n\nexport default function SpellCheck() {\n  const { t } = useTranslation();\n  const [saving, setSaving] = useState(false);\n  const [enableSpellCheck, setEnableSpellCheck] = useState(\n    Appearance.get(\"enableSpellCheck\")\n  );\n\n  const handleChange = async (checked) => {\n    setEnableSpellCheck(checked);\n    setSaving(true);\n    try {\n      Appearance.set(\"enableSpellCheck\", checked);\n    } catch (error) {\n      console.error(\"Failed to update appearance settings:\", error);\n      setEnableSpellCheck(!checked);\n    }\n    setSaving(false);\n  };\n\n  return (\n    <div className=\"my-4\">\n      <Toggle\n        size=\"md\"\n        variant=\"horizontal\"\n        enabled={enableSpellCheck}\n        onChange={handleChange}\n        disabled={saving}\n        label={t(\"customization.chat.spellcheck.title\")}\n        description={t(\"customization.chat.spellcheck.description\")}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/components/SupportEmail/index.jsx",
    "content": "import useUser from \"@/hooks/useUser\";\nimport Admin from \"@/models/admin\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function SupportEmail() {\n  const { user } = useUser();\n  const [loading, setLoading] = useState(true);\n  const [hasChanges, setHasChanges] = useState(false);\n  const [supportEmail, setSupportEmail] = useState(\"\");\n  const [originalEmail, setOriginalEmail] = useState(\"\");\n  const { t } = useTranslation();\n\n  useEffect(() => {\n    const fetchSupportEmail = async () => {\n      const supportEmail = await System.fetchSupportEmail();\n      setSupportEmail(supportEmail.email || \"\");\n      setOriginalEmail(supportEmail.email || \"\");\n      setLoading(false);\n    };\n    fetchSupportEmail();\n  }, []);\n\n  const updateSupportEmail = async (e, newValue = null) => {\n    e.preventDefault();\n    let support_email = newValue;\n    if (newValue === null) {\n      const form = new FormData(e.target);\n      support_email = form.get(\"supportEmail\");\n    }\n\n    const { success, error } = await Admin.updateSystemPreferences({\n      support_email,\n    });\n\n    if (!success) {\n      showToast(`Failed to update support email: ${error}`, \"error\");\n      return;\n    } else {\n      showToast(\"Successfully updated support email.\", \"success\");\n      window.localStorage.removeItem(System.cacheKeys.supportEmail);\n      setSupportEmail(support_email);\n      setOriginalEmail(support_email);\n      setHasChanges(false);\n    }\n  };\n\n  const handleChange = (e) => {\n    setSupportEmail(e.target.value);\n    setHasChanges(true);\n  };\n\n  if (loading || !user?.role) return null;\n  return (\n    <form\n      className=\"flex flex-col gap-y-0.5 mt-4\"\n      onSubmit={updateSupportEmail}\n    >\n      <p className=\"text-sm leading-6 font-semibold text-white\">\n        {t(\"customization.items.support-email.title\")}\n      </p>\n      <p className=\"text-xs text-white/60\">\n        {t(\"customization.items.support-email.description\")}\n      </p>\n      <div className=\"flex items-center gap-x-4\">\n        <input\n          name=\"supportEmail\"\n          type=\"email\"\n          className=\"border-none bg-theme-settings-input-bg mt-2 text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-fit py-2 px-4\"\n          placeholder=\"support@mycompany.com\"\n          required={true}\n          autoComplete=\"off\"\n          onChange={handleChange}\n          value={supportEmail}\n        />\n        {originalEmail !== \"\" && (\n          <button\n            type=\"button\"\n            onClick={(e) => updateSupportEmail(e, \"\")}\n            className=\"text-white text-base font-medium hover:text-opacity-60\"\n          >\n            Clear\n          </button>\n        )}\n      </div>\n      {hasChanges && (\n        <button\n          type=\"submit\"\n          className=\"transition-all mt-2 w-fit duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800\"\n        >\n          Save\n        </button>\n      )}\n    </form>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/Settings/components/ThemePreference/index.jsx",
    "content": "import { useTheme } from \"@/hooks/useTheme\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function ThemePreference() {\n  const { t } = useTranslation();\n  const { theme, setTheme, availableThemes } = useTheme();\n\n  return (\n    <div className=\"flex flex-col gap-y-0.5 my-4\">\n      <p className=\"text-sm leading-6 font-semibold text-white\">\n        {t(\"customization.items.theme.title\")}\n      </p>\n      <p className=\"text-xs text-white/60\">\n        {t(\"customization.items.theme.description\")}\n      </p>\n      <div className=\"flex items-center gap-x-4\">\n        <select\n          value={theme}\n          onChange={(e) => setTheme(e.target.value)}\n          className=\"border-none bg-theme-settings-input-bg mt-2 text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-fit py-2 px-4\"\n        >\n          {Object.entries(availableThemes).map(([key, value]) => (\n            <option key={key} value={key}>\n              {value}\n            </option>\n          ))}\n        </select>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport { isMobile } from \"react-device-detect\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport PreLoader from \"@/components/Preloader\";\nimport OpenAiLogo from \"@/media/llmprovider/openai.png\";\nimport AnythingLLMIcon from \"@/media/logo/anything-llm-icon.png\";\nimport OpenAiWhisperOptions from \"@/components/TranscriptionSelection/OpenAiOptions\";\nimport NativeTranscriptionOptions from \"@/components/TranscriptionSelection/NativeTranscriptionOptions\";\nimport LLMItem from \"@/components/LLMSelection/LLMItem\";\nimport { CaretUpDown, MagnifyingGlass, X } from \"@phosphor-icons/react\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport { useTranslation } from \"react-i18next\";\n\nconst PROVIDERS = [\n  {\n    name: \"OpenAI\",\n    value: \"openai\",\n    logo: OpenAiLogo,\n    options: (settings) => <OpenAiWhisperOptions settings={settings} />,\n    description: \"Leverage the OpenAI Whisper-large model using your API key.\",\n  },\n  {\n    name: \"AnythingLLM Built-In\",\n    value: \"local\",\n    logo: AnythingLLMIcon,\n    options: (settings) => <NativeTranscriptionOptions settings={settings} />,\n    description: \"Run a built-in whisper model on this instance privately.\",\n  },\n];\n\nexport default function TranscriptionModelPreference() {\n  const [saving, setSaving] = useState(false);\n  const [hasChanges, setHasChanges] = useState(false);\n  const [settings, setSettings] = useState(null);\n  const [loading, setLoading] = useState(true);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [filteredProviders, setFilteredProviders] = useState([]);\n  const [selectedProvider, setSelectedProvider] = useState(null);\n  const [searchMenuOpen, setSearchMenuOpen] = useState(false);\n  const searchInputRef = useRef(null);\n  const { t } = useTranslation();\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = e.target;\n    const data = { WhisperProvider: selectedProvider };\n    const formData = new FormData(form);\n\n    for (var [key, value] of formData.entries()) data[key] = value;\n    const { error } = await System.updateSystem(data);\n    setSaving(true);\n\n    if (error) {\n      showToast(`Failed to save preferences: ${error}`, \"error\");\n    } else {\n      showToast(\"Transcription preferences saved successfully.\", \"success\");\n    }\n    setSaving(false);\n    setHasChanges(!!error);\n  };\n\n  const updateProviderChoice = (selection) => {\n    setSearchQuery(\"\");\n    setSelectedProvider(selection);\n    setSearchMenuOpen(false);\n    setHasChanges(true);\n  };\n\n  const handleXButton = () => {\n    if (searchQuery.length > 0) {\n      setSearchQuery(\"\");\n      if (searchInputRef.current) searchInputRef.current.value = \"\";\n    } else {\n      setSearchMenuOpen(!searchMenuOpen);\n    }\n  };\n\n  useEffect(() => {\n    async function fetchKeys() {\n      const _settings = await System.keys();\n      setSettings(_settings);\n      setSelectedProvider(_settings?.WhisperProvider || \"local\");\n      setLoading(false);\n    }\n    fetchKeys();\n  }, []);\n\n  useEffect(() => {\n    const filtered = PROVIDERS.filter((provider) =>\n      provider.name.toLowerCase().includes(searchQuery.toLowerCase())\n    );\n    setFilteredProviders(filtered);\n  }, [searchQuery, selectedProvider]);\n\n  const selectedProviderObject = PROVIDERS.find(\n    (provider) => provider.value === selectedProvider\n  );\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      {loading ? (\n        <div\n          style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n          className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n        >\n          <div className=\"w-full h-full flex justify-center items-center\">\n            <PreLoader />\n          </div>\n        </div>\n      ) : (\n        <div\n          style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n          className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n        >\n          <form onSubmit={handleSubmit} className=\"flex w-full\">\n            <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] py-16 md:py-6\">\n              <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n                <div className=\"flex gap-x-4 items-center\">\n                  <p className=\"text-lg leading-6 font-bold text-white\">\n                    {t(\"transcription.title\")}\n                  </p>\n                </div>\n                <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n                  {t(\"transcription.description\")}\n                </p>\n              </div>\n              <div className=\"w-full justify-end flex\">\n                {hasChanges && (\n                  <CTAButton\n                    onClick={() => handleSubmit()}\n                    className=\"mt-3 mr-0 -mb-14 z-10\"\n                  >\n                    {saving ? \"Saving...\" : \"Save changes\"}\n                  </CTAButton>\n                )}\n              </div>\n              <div className=\"text-base font-bold text-white mt-6 mb-4\">\n                {t(\"transcription.provider\")}\n              </div>\n              <div className=\"relative\">\n                {searchMenuOpen && (\n                  <div\n                    className=\"fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10\"\n                    onClick={() => setSearchMenuOpen(false)}\n                  />\n                )}\n                {searchMenuOpen ? (\n                  <div className=\"absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] min-h-[64px] bg-theme-settings-input-bg rounded-lg flex flex-col justify-between cursor-pointer border-2 border-primary-button z-20\">\n                    <div className=\"w-full flex flex-col gap-y-1\">\n                      <div className=\"flex items-center sticky top-0 z-10 border-b border-[#9CA3AF] mx-4 bg-theme-settings-input-bg\">\n                        <MagnifyingGlass\n                          size={20}\n                          weight=\"bold\"\n                          className=\"absolute left-4 z-30 text-theme-text-primary -ml-4 my-2\"\n                        />\n                        <input\n                          type=\"text\"\n                          name=\"provider-search\"\n                          autoComplete=\"off\"\n                          placeholder=\"Search audio transcription providers\"\n                          className=\"border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:outline-primary-button active:outline-primary-button outline-none text-theme-text-primary placeholder:text-theme-text-primary placeholder:font-medium\"\n                          onChange={(e) => setSearchQuery(e.target.value)}\n                          ref={searchInputRef}\n                          onKeyDown={(e) => {\n                            if (e.key === \"Enter\") e.preventDefault();\n                          }}\n                        />\n                        <X\n                          size={20}\n                          weight=\"bold\"\n                          className=\"cursor-pointer text-white hover:text-x-button\"\n                          onClick={handleXButton}\n                        />\n                      </div>\n                      <div className=\"flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4 max-h-[245px]\">\n                        {filteredProviders.map((provider) => (\n                          <LLMItem\n                            key={provider.name}\n                            name={provider.name}\n                            value={provider.value}\n                            image={provider.logo}\n                            description={provider.description}\n                            checked={selectedProvider === provider.value}\n                            onClick={() => updateProviderChoice(provider.value)}\n                          />\n                        ))}\n                      </div>\n                    </div>\n                  </div>\n                ) : (\n                  <button\n                    className=\"w-full max-w-[640px] h-[64px] bg-theme-settings-input-bg rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-primary-button transition-all duration-300\"\n                    type=\"button\"\n                    onClick={() => setSearchMenuOpen(true)}\n                  >\n                    <div className=\"flex gap-x-4 items-center\">\n                      <img\n                        src={selectedProviderObject.logo}\n                        alt={`${selectedProviderObject.name} logo`}\n                        className=\"w-10 h-10 rounded-md\"\n                      />\n                      <div className=\"flex flex-col text-left\">\n                        <div className=\"text-sm font-semibold text-white\">\n                          {selectedProviderObject.name}\n                        </div>\n                        <div className=\"mt-1 text-xs text-description\">\n                          {selectedProviderObject.description}\n                        </div>\n                      </div>\n                    </div>\n                    <CaretUpDown\n                      size={24}\n                      weight=\"bold\"\n                      className=\"text-white\"\n                    />\n                  </button>\n                )}\n              </div>\n              <div\n                onChange={() => setHasChanges(true)}\n                className=\"mt-4 flex flex-col gap-y-1\"\n              >\n                {selectedProvider &&\n                  PROVIDERS.find(\n                    (provider) => provider.value === selectedProvider\n                  )?.options(settings)}\n              </div>\n            </div>\n          </form>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx",
    "content": "import React, { useState, useEffect, useRef } from \"react\";\nimport Sidebar from \"@/components/SettingsSidebar\";\nimport { isMobile } from \"react-device-detect\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { useModal } from \"@/hooks/useModal\";\nimport CTAButton from \"@/components/lib/CTAButton\";\nimport { CaretUpDown, MagnifyingGlass, X } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\nimport PreLoader from \"@/components/Preloader\";\nimport ChangeWarningModal from \"@/components/ChangeWarning\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport VectorDBItem from \"@/components/VectorDBSelection/VectorDBItem\";\n\nimport LanceDbLogo from \"@/media/vectordbs/lancedb.png\";\nimport ChromaLogo from \"@/media/vectordbs/chroma.png\";\nimport PineconeLogo from \"@/media/vectordbs/pinecone.png\";\nimport WeaviateLogo from \"@/media/vectordbs/weaviate.png\";\nimport QDrantLogo from \"@/media/vectordbs/qdrant.png\";\nimport MilvusLogo from \"@/media/vectordbs/milvus.png\";\nimport ZillizLogo from \"@/media/vectordbs/zilliz.png\";\nimport AstraDBLogo from \"@/media/vectordbs/astraDB.png\";\nimport PGVectorLogo from \"@/media/vectordbs/pgvector.png\";\n\nimport LanceDBOptions from \"@/components/VectorDBSelection/LanceDBOptions\";\nimport ChromaDBOptions from \"@/components/VectorDBSelection/ChromaDBOptions\";\nimport ChromaCloudOptions from \"@/components/VectorDBSelection/ChromaCloudOptions\";\nimport PineconeDBOptions from \"@/components/VectorDBSelection/PineconeDBOptions\";\nimport WeaviateDBOptions from \"@/components/VectorDBSelection/WeaviateDBOptions\";\nimport QDrantDBOptions from \"@/components/VectorDBSelection/QDrantDBOptions\";\nimport MilvusDBOptions from \"@/components/VectorDBSelection/MilvusDBOptions\";\nimport ZillizCloudOptions from \"@/components/VectorDBSelection/ZillizCloudOptions\";\nimport AstraDBOptions from \"@/components/VectorDBSelection/AstraDBOptions\";\nimport PGVectorOptions from \"@/components/VectorDBSelection/PGVectorOptions\";\n\nconst VECTOR_DBS = [\n  {\n    name: \"LanceDB\",\n    value: \"lancedb\",\n    logo: LanceDbLogo,\n    options: (_) => <LanceDBOptions />,\n    description:\n      \"100% local vector DB that runs on the same instance as AnythingLLM.\",\n  },\n  {\n    name: \"PGVector\",\n    value: \"pgvector\",\n    logo: PGVectorLogo,\n    options: (settings) => <PGVectorOptions settings={settings} />,\n    description: \"Vector search powered by PostgreSQL.\",\n  },\n  {\n    name: \"Chroma\",\n    value: \"chroma\",\n    logo: ChromaLogo,\n    options: (settings) => <ChromaDBOptions settings={settings} />,\n    description:\n      \"Open source vector database you can host yourself or on the cloud.\",\n  },\n  {\n    name: \"Chroma Cloud\",\n    value: \"chromacloud\",\n    logo: ChromaLogo,\n    options: (settings) => <ChromaCloudOptions settings={settings} />,\n    description:\n      \"Fully managed Chroma cloud service with enterprise features and support.\",\n  },\n  {\n    name: \"Pinecone\",\n    value: \"pinecone\",\n    logo: PineconeLogo,\n    options: (settings) => <PineconeDBOptions settings={settings} />,\n    description: \"100% cloud-based vector database for enterprise use cases.\",\n  },\n  {\n    name: \"Zilliz Cloud\",\n    value: \"zilliz\",\n    logo: ZillizLogo,\n    options: (settings) => <ZillizCloudOptions settings={settings} />,\n    description:\n      \"Cloud hosted vector database built for enterprise with SOC 2 compliance.\",\n  },\n  {\n    name: \"QDrant\",\n    value: \"qdrant\",\n    logo: QDrantLogo,\n    options: (settings) => <QDrantDBOptions settings={settings} />,\n    description: \"Open source local and distributed cloud vector database.\",\n  },\n  {\n    name: \"Weaviate\",\n    value: \"weaviate\",\n    logo: WeaviateLogo,\n    options: (settings) => <WeaviateDBOptions settings={settings} />,\n    description:\n      \"Open source local and cloud hosted multi-modal vector database.\",\n  },\n  {\n    name: \"Milvus\",\n    value: \"milvus\",\n    logo: MilvusLogo,\n    options: (settings) => <MilvusDBOptions settings={settings} />,\n    description: \"Open-source, highly scalable, and blazing fast.\",\n  },\n  {\n    name: \"AstraDB\",\n    value: \"astra\",\n    logo: AstraDBLogo,\n    options: (settings) => <AstraDBOptions settings={settings} />,\n    description: \"Vector Search for Real-world GenAI.\",\n  },\n];\n\nexport default function GeneralVectorDatabase() {\n  const [saving, setSaving] = useState(false);\n  const [hasChanges, setHasChanges] = useState(false);\n  const [hasEmbeddings, setHasEmbeddings] = useState(false);\n  const [settings, setSettings] = useState({});\n  const [loading, setLoading] = useState(true);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [filteredVDBs, setFilteredVDBs] = useState([]);\n  const [selectedVDB, setSelectedVDB] = useState(null);\n  const [searchMenuOpen, setSearchMenuOpen] = useState(false);\n  const searchInputRef = useRef(null);\n  const { isOpen, openModal, closeModal } = useModal();\n  const { t } = useTranslation();\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    if (selectedVDB !== settings?.VectorDB && hasChanges && hasEmbeddings) {\n      openModal();\n    } else {\n      await handleSaveSettings();\n    }\n  };\n\n  const handleSaveSettings = async () => {\n    setSaving(true);\n    const form = document.getElementById(\"vectordb-form\");\n    const settingsData = {};\n    const formData = new FormData(form);\n    settingsData.VectorDB = selectedVDB;\n    for (var [key, value] of formData.entries()) settingsData[key] = value;\n\n    const { error } = await System.updateSystem(settingsData);\n    if (error) {\n      showToast(`Failed to save vector database settings: ${error}`, \"error\");\n      setHasChanges(true);\n    } else {\n      showToast(\"Vector database preferences saved successfully.\", \"success\");\n      setHasChanges(false);\n    }\n    setSaving(false);\n    closeModal();\n  };\n\n  const updateVectorChoice = (selection) => {\n    setSearchQuery(\"\");\n    setSelectedVDB(selection);\n    setSearchMenuOpen(false);\n    setHasChanges(true);\n  };\n\n  const handleXButton = () => {\n    if (searchQuery.length > 0) {\n      setSearchQuery(\"\");\n      if (searchInputRef.current) searchInputRef.current.value = \"\";\n    } else {\n      setSearchMenuOpen(!searchMenuOpen);\n    }\n  };\n\n  useEffect(() => {\n    async function fetchKeys() {\n      const _settings = await System.keys();\n      setSettings(_settings);\n      setSelectedVDB(_settings?.VectorDB || \"lancedb\");\n      setHasEmbeddings(_settings?.HasExistingEmbeddings || false);\n      setLoading(false);\n    }\n    fetchKeys();\n  }, []);\n\n  useEffect(() => {\n    const filtered = VECTOR_DBS.filter((vdb) =>\n      vdb.name.toLowerCase().includes(searchQuery.toLowerCase())\n    );\n    setFilteredVDBs(filtered);\n  }, [searchQuery, selectedVDB]);\n\n  const selectedVDBObject =\n    VECTOR_DBS.find((vdb) => vdb.value === selectedVDB) ?? VECTOR_DBS[0];\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n      <Sidebar />\n      {loading ? (\n        <div\n          style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n          className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n        >\n          <div className=\"w-full h-full flex justify-center items-center\">\n            <PreLoader />\n          </div>\n        </div>\n      ) : (\n        <div\n          style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n          className=\"relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll p-4 md:p-0\"\n        >\n          <form\n            id=\"vectordb-form\"\n            onSubmit={handleSubmit}\n            className=\"flex w-full\"\n          >\n            <div className=\"flex flex-col w-full px-1 md:pl-6 md:pr-[50px] py-16 md:py-6\">\n              <div className=\"w-full flex flex-col gap-y-1 pb-6 border-white light:border-theme-sidebar-border border-b-2 border-opacity-10\">\n                <div className=\"flex gap-x-4 items-center\">\n                  <p className=\"text-lg leading-6 font-bold text-white\">\n                    {t(\"vector.title\")}\n                  </p>\n                </div>\n                <p className=\"text-xs leading-[18px] font-base text-white text-opacity-60\">\n                  {t(\"vector.description\")}\n                </p>\n              </div>\n              <div className=\"w-full justify-end flex\">\n                {hasChanges && (\n                  <CTAButton\n                    onClick={() => handleSubmit()}\n                    className=\"mt-3 mr-0 -mb-14 z-10\"\n                  >\n                    {saving ? t(\"common.saving\") : t(\"common.save\")}\n                  </CTAButton>\n                )}\n              </div>\n              <div className=\"text-base font-bold text-white mt-6 mb-4\">\n                {t(\"vector.provider.title\")}\n              </div>\n              <div className=\"relative\">\n                {searchMenuOpen && (\n                  <div\n                    className=\"fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10\"\n                    onClick={() => setSearchMenuOpen(false)}\n                  />\n                )}\n                {searchMenuOpen ? (\n                  <div className=\"absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] min-h-[64px] bg-theme-settings-input-bg rounded-lg flex flex-col justify-between cursor-pointer border-2 border-primary-button z-20\">\n                    <div className=\"w-full flex flex-col gap-y-1\">\n                      <div className=\"flex items-center sticky top-0 z-10 border-b border-[#9CA3AF] mx-4 bg-theme-settings-input-bg\">\n                        <MagnifyingGlass\n                          size={20}\n                          weight=\"bold\"\n                          className=\"absolute left-4 z-30 text-theme-text-primary -ml-4 my-2\"\n                        />\n                        <input\n                          type=\"text\"\n                          name=\"vdb-search\"\n                          autoComplete=\"off\"\n                          placeholder=\"Search all vector database providers\"\n                          className=\"border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none text-theme-text-primary placeholder:text-theme-text-primary placeholder:font-medium\"\n                          onChange={(e) => setSearchQuery(e.target.value)}\n                          ref={searchInputRef}\n                          onKeyDown={(e) => {\n                            if (e.key === \"Enter\") e.preventDefault();\n                          }}\n                        />\n                        <X\n                          size={20}\n                          weight=\"bold\"\n                          className=\"cursor-pointer text-white hover:text-x-button\"\n                          onClick={handleXButton}\n                        />\n                      </div>\n                      <div className=\"flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4 max-h-[245px]\">\n                        {filteredVDBs.map((vdb) => (\n                          <VectorDBItem\n                            key={vdb.name}\n                            name={vdb.name}\n                            value={vdb.value}\n                            image={vdb.logo}\n                            description={vdb.description}\n                            checked={selectedVDB === vdb.value}\n                            onClick={() => updateVectorChoice(vdb.value)}\n                          />\n                        ))}\n                      </div>\n                    </div>\n                  </div>\n                ) : (\n                  <button\n                    className=\"w-full max-w-[640px] h-[64px] bg-theme-settings-input-bg rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-primary-button transition-all duration-300\"\n                    type=\"button\"\n                    onClick={() => setSearchMenuOpen(true)}\n                  >\n                    <div className=\"flex gap-x-4 items-center\">\n                      <img\n                        src={selectedVDBObject.logo}\n                        alt={`${selectedVDBObject.name} logo`}\n                        className=\"w-10 h-10 rounded-md\"\n                      />\n                      <div className=\"flex flex-col text-left\">\n                        <div className=\"text-sm font-semibold text-white\">\n                          {selectedVDBObject.name}\n                        </div>\n                        <div className=\"mt-1 text-xs text-description\">\n                          {selectedVDBObject.description}\n                        </div>\n                      </div>\n                    </div>\n                    <CaretUpDown\n                      size={24}\n                      weight=\"bold\"\n                      className=\"text-white\"\n                    />\n                  </button>\n                )}\n              </div>\n              <div\n                onChange={() => setHasChanges(true)}\n                className=\"mt-4 flex flex-col gap-y-1\"\n              >\n                {selectedVDB &&\n                  VECTOR_DBS.find((vdb) => vdb.value === selectedVDB)?.options(\n                    settings\n                  )}\n              </div>\n            </div>\n          </form>\n        </div>\n      )}\n      <ModalWrapper isOpen={isOpen}>\n        <ChangeWarningModal\n          warningText=\"Switching the vector database will reset all previously embedded documents in all workspaces.\\n\\nConfirming will clear all embeddings from your vector database and remove all documents from your workspaces. Your uploaded documents will not be deleted, they will be available for re-embedding.\"\n          onClose={closeModal}\n          onConfirm={handleSaveSettings}\n        />\n      </ModalWrapper>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Invite/NewUserModal/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport Invite from \"@/models/invite\";\nimport paths from \"@/utils/paths\";\nimport { useParams } from \"react-router-dom\";\nimport { AUTH_TOKEN, AUTH_USER } from \"@/utils/constants\";\nimport System from \"@/models/system\";\nimport { useTranslation } from \"react-i18next\";\nimport {\n  USERNAME_MIN_LENGTH,\n  USERNAME_MAX_LENGTH,\n  USERNAME_PATTERN,\n} from \"@/utils/username\";\n\nexport default function NewUserModal() {\n  const { code } = useParams();\n  const [error, setError] = useState(null);\n  const { t } = useTranslation();\n\n  const handleCreate = async (e) => {\n    setError(null);\n    e.preventDefault();\n    const data = {};\n    const form = new FormData(e.target);\n    for (var [key, value] of form.entries()) data[key] = value;\n    const { success, error } = await Invite.acceptInvite(code, data);\n    if (success) {\n      const { valid, user, token, message } = await System.requestToken(data);\n      if (valid && !!token && !!user) {\n        window.localStorage.setItem(AUTH_USER, JSON.stringify(user));\n        window.localStorage.setItem(AUTH_TOKEN, token);\n        window.location = paths.home();\n      } else {\n        setError(message);\n      }\n      return;\n    }\n    setError(error);\n  };\n\n  return (\n    <div className=\"relative w-full max-w-2xl max-h-full\">\n      <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n        <div className=\"flex items-start justify-between p-4 border-b rounded-t border-theme-modal-border\">\n          <h3 className=\"text-xl font-semibold text-theme-text-primary\">\n            Create a new account\n          </h3>\n        </div>\n        <form onSubmit={handleCreate}>\n          <div className=\"p-6 space-y-6 flex h-full w-full\">\n            <div className=\"w-full flex flex-col gap-y-4\">\n              <div>\n                <label\n                  htmlFor=\"username\"\n                  className=\"block mb-2 text-sm font-medium text-theme-text-primary\"\n                >\n                  Username\n                </label>\n                <input\n                  name=\"username\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"My username\"\n                  minLength={USERNAME_MIN_LENGTH}\n                  maxLength={USERNAME_MAX_LENGTH}\n                  pattern={USERNAME_PATTERN}\n                  required={true}\n                  autoComplete=\"off\"\n                />\n                <p className=\"mt-2 text-xs text-theme-text-secondary\">\n                  {t(\"common.username_requirements\")}\n                </p>\n              </div>\n              <div>\n                <label\n                  htmlFor=\"password\"\n                  className=\"block mb-2 text-sm font-medium text-theme-text-primary\"\n                >\n                  Password\n                </label>\n                <input\n                  name=\"password\"\n                  type=\"password\"\n                  className=\"border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n                  placeholder=\"Your password\"\n                  required={true}\n                  minLength={8}\n                  autoComplete=\"off\"\n                />\n              </div>\n              {error && <p className=\"text-red-400 text-sm\">Error: {error}</p>}\n              <p className=\"text-theme-text-secondary text-xs md:text-sm\">\n                After creating your account you will be able to login with these\n                credentials and start using workspaces.\n              </p>\n            </div>\n          </div>\n          <div className=\"flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-theme-modal-border\">\n            <button\n              type=\"submit\"\n              className=\"w-full transition-all duration-300 border border-theme-text-primary px-4 py-2 rounded-lg text-theme-text-primary text-sm items-center flex gap-x-2 hover:bg-theme-text-primary hover:text-theme-bg-primary focus:ring-gray-800 text-center justify-center\"\n            >\n              Accept Invitation\n            </button>\n          </div>\n        </form>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Invite/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport { FullScreenLoader } from \"@/components/Preloader\";\nimport Invite from \"@/models/invite\";\nimport NewUserModal from \"./NewUserModal\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\n\nexport default function InvitePage() {\n  const { code } = useParams();\n  const [result, setResult] = useState({\n    status: \"loading\",\n    message: null,\n  });\n\n  useEffect(() => {\n    async function checkInvite() {\n      if (!code) {\n        setResult({\n          status: \"invalid\",\n          message: \"No invite code provided.\",\n        });\n        return;\n      }\n      const { invite, error } = await Invite.checkInvite(code);\n      setResult({\n        status: invite ? \"valid\" : \"invalid\",\n        message: error,\n      });\n    }\n    checkInvite();\n  }, []);\n\n  if (result.status === \"loading\") {\n    return (\n      <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex\">\n        <FullScreenLoader />\n      </div>\n    );\n  }\n\n  if (result.status === \"invalid\") {\n    return (\n      <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex items-center justify-center\">\n        <p className=\"text-red-400 text-lg\">{result.message}</p>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-container flex items-center justify-center\">\n      <ModalWrapper isOpen={true}>\n        <NewUserModal />\n      </ModalWrapper>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Login/SSO/simple.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { FullScreenLoader } from \"@/components/Preloader\";\nimport paths from \"@/utils/paths\";\nimport useQuery from \"@/hooks/useQuery\";\nimport System from \"@/models/system\";\nimport { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from \"@/utils/constants\";\n\nexport default function SimpleSSOPassthrough() {\n  const query = useQuery();\n  const redirectPath = query.get(\"redirectTo\") || paths.home();\n  const [ready, setReady] = useState(false);\n  const [error, setError] = useState(null);\n\n  useEffect(() => {\n    try {\n      if (!query.get(\"token\")) throw new Error(\"No token provided.\");\n\n      // Clear any existing auth data\n      window.localStorage.removeItem(AUTH_USER);\n      window.localStorage.removeItem(AUTH_TOKEN);\n      window.localStorage.removeItem(AUTH_TIMESTAMP);\n\n      System.simpleSSOLogin(query.get(\"token\"))\n        .then((res) => {\n          if (!res.valid) throw new Error(res.message);\n\n          window.localStorage.setItem(AUTH_USER, JSON.stringify(res.user));\n          window.localStorage.setItem(AUTH_TOKEN, res.token);\n          window.localStorage.setItem(AUTH_TIMESTAMP, Number(new Date()));\n          setReady(res.valid);\n        })\n        .catch((e) => {\n          setError(e.message);\n        });\n    } catch (e) {\n      setError(e.message);\n    }\n  }, []);\n\n  if (error)\n    return (\n      <div className=\"w-screen h-screen overflow-hidden bg-theme-bg-primary flex items-center justify-center flex-col gap-4\">\n        <p className=\"text-theme-text-primary font-mono text-lg\">{error}</p>\n        <p className=\"text-theme-text-secondary font-mono text-sm\">\n          Please contact the system administrator about this error.\n        </p>\n      </div>\n    );\n  if (ready) return window.location.replace(redirectPath);\n\n  // Loading state by default\n  return <FullScreenLoader />;\n}\n"
  },
  {
    "path": "frontend/src/pages/Login/index.jsx",
    "content": "import React from \"react\";\nimport PasswordModal, { usePasswordModal } from \"@/components/Modals/Password\";\nimport { FullScreenLoader } from \"@/components/Preloader\";\nimport { Navigate } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\nimport useQuery from \"@/hooks/useQuery\";\nimport useSimpleSSO from \"@/hooks/useSimpleSSO\";\n\n/**\n * Login page that handles both single and multi-user login.\n *\n * If Simple SSO is enabled and no login is allowed, the user will be redirected to the SSO login page\n * which may not have a token so the login will fail.\n *\n * @returns {JSX.Element}\n */\nexport default function Login() {\n  const query = useQuery();\n  const { loading: ssoLoading, ssoConfig } = useSimpleSSO();\n  const { loading, requiresAuth, mode } = usePasswordModal(!!query.get(\"nt\"));\n\n  if (loading || ssoLoading) return <FullScreenLoader />;\n\n  // If simple SSO is enabled and no login is allowed, redirect to the SSO login page.\n  if (ssoConfig.enabled && ssoConfig.noLogin) {\n    // If a noLoginRedirect is provided and no token is provided, redirect to that webpage.\n    if (!!ssoConfig.noLoginRedirect && !query.has(\"token\"))\n      return window.location.replace(ssoConfig.noLoginRedirect);\n    // Otherwise, redirect to the SSO login page.\n    else return <Navigate to={paths.sso.login()} />;\n  }\n\n  if (requiresAuth === false) return <Navigate to={paths.home()} />;\n\n  return <PasswordModal mode={mode} />;\n}\n"
  },
  {
    "path": "frontend/src/pages/Main/Home/index.jsx",
    "content": "import React, { useState, useEffect, useRef, useContext } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\nimport { isMobile } from \"react-device-detect\";\nimport { SidebarMobileHeader } from \"@/components/Sidebar\";\nimport PromptInput, {\n  PROMPT_INPUT_EVENT,\n  PROMPT_INPUT_ID,\n} from \"@/components/WorkspaceChat/ChatContainer/PromptInput\";\nimport DnDFileUploaderWrapper, {\n  DndUploaderContext,\n  DnDFileUploaderProvider,\n  PASTE_ATTACHMENT_EVENT,\n} from \"@/components/WorkspaceChat/ChatContainer/DnDWrapper\";\nimport { useTranslation } from \"react-i18next\";\nimport {\n  LAST_VISITED_WORKSPACE,\n  PENDING_HOME_MESSAGE,\n} from \"@/utils/constants\";\nimport Workspace from \"@/models/workspace\";\nimport paths from \"@/utils/paths\";\nimport showToast from \"@/utils/toast\";\nimport { safeJsonParse } from \"@/utils/request\";\nimport QuickActions from \"@/components/lib/QuickActions\";\nimport SuggestedMessages from \"@/components/lib/SuggestedMessages\";\nimport useUser from \"@/hooks/useUser\";\nimport TextSizeMenu from \"@/components/WorkspaceChat/ChatContainer/TextSizeMenu\";\nimport WorkspaceModelPicker from \"@/components/WorkspaceChat/ChatContainer/WorkspaceModelPicker\";\nimport { ChatTooltips } from \"@/components/WorkspaceChat/ChatContainer/ChatTooltips\";\n\nasync function getTargetWorkspace() {\n  const lastVisited = safeJsonParse(\n    localStorage.getItem(LAST_VISITED_WORKSPACE)\n  );\n  if (lastVisited?.slug) {\n    const workspace = await Workspace.bySlug(lastVisited.slug);\n    if (workspace) return workspace;\n  }\n\n  const workspaces = await Workspace.all();\n  return workspaces.length > 0 ? workspaces[0] : null;\n}\n\nasync function createDefaultWorkspace(workspaceName = \"My Workspace\") {\n  const { workspace, message: errorMsg } = await Workspace.new({\n    name: workspaceName,\n  });\n  if (!workspace) {\n    showToast(errorMsg || \"Failed to create workspace\", \"error\");\n    return null;\n  }\n  return workspace;\n}\n\nexport default function Home() {\n  const { t } = useTranslation();\n  const { user } = useUser();\n  const [workspace, setWorkspace] = useState(null);\n  const [threadSlug, setThreadSlug] = useState(null);\n  const [workspaceLoading, setWorkspaceLoading] = useState(true);\n  const [dragging, setDragging] = useState(false);\n  const pendingFilesRef = useRef([]);\n\n  useEffect(() => {\n    async function init() {\n      const ws = await getTargetWorkspace();\n      if (ws) {\n        const [suggestedMessages, { showAgentCommand }] = await Promise.all([\n          Workspace.getSuggestedMessages(ws.slug),\n          Workspace.agentCommandAvailable(ws.slug),\n        ]);\n        setWorkspace({\n          ...ws,\n          suggestedMessages,\n          showAgentCommand,\n        });\n      }\n      setWorkspaceLoading(false);\n    }\n    init();\n  }, []);\n\n  // When workspace/thread becomes available and we have pending files, trigger upload\n  useEffect(() => {\n    if (workspace && threadSlug && pendingFilesRef.current.length > 0) {\n      const files = pendingFilesRef.current;\n      pendingFilesRef.current = [];\n      window.dispatchEvent(\n        new CustomEvent(PASTE_ATTACHMENT_EVENT, { detail: { files } })\n      );\n    }\n  }, [workspace, threadSlug]);\n\n  // Handle paste events when no thread exists yet\n  useEffect(() => {\n    if (threadSlug) return;\n\n    async function handlePaste(e) {\n      const files = e.detail?.files;\n      if (!files?.length) return;\n\n      pendingFilesRef.current = files;\n      let ws = workspace;\n      if (!ws) {\n        ws = await createDefaultWorkspace(t(\"new-workspace.placeholder\"));\n        if (!ws) return;\n        setWorkspace(ws);\n      }\n      const { thread } = await Workspace.threads.new(ws.slug);\n      if (thread) setThreadSlug(thread.slug);\n    }\n\n    window.addEventListener(PASTE_ATTACHMENT_EVENT, handlePaste);\n    return () =>\n      window.removeEventListener(PASTE_ATTACHMENT_EVENT, handlePaste);\n  }, [workspace, threadSlug]);\n\n  async function handleDropWithoutWorkspace(acceptedFiles) {\n    setDragging(false);\n    pendingFilesRef.current = acceptedFiles;\n    const ws = await createDefaultWorkspace(t(\"new-workspace.placeholder\"));\n    if (!ws) return;\n    setWorkspace(ws);\n    const { thread } = await Workspace.threads.new(ws.slug);\n    if (thread) setThreadSlug(thread.slug);\n  }\n\n  async function handleDropWithWorkspace(acceptedFiles) {\n    setDragging(false);\n    pendingFilesRef.current = acceptedFiles;\n    const { thread } = await Workspace.threads.new(workspace.slug);\n    if (thread) setThreadSlug(thread.slug);\n  }\n\n  if (workspaceLoading) {\n    return (\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-zinc-900 light:bg-white w-full h-full overflow-hidden\"\n      />\n    );\n  }\n\n  if (!workspace && user?.role === \"default\") {\n    return <NoWorkspacesAssigned />;\n  }\n\n  if (workspace && threadSlug) {\n    return (\n      <DnDFileUploaderProvider workspace={workspace} threadSlug={threadSlug}>\n        <HomeContent\n          workspace={workspace}\n          setWorkspace={setWorkspace}\n          threadSlug={threadSlug}\n          setThreadSlug={setThreadSlug}\n        />\n      </DnDFileUploaderProvider>\n    );\n  }\n\n  return (\n    <DndUploaderContext.Provider\n      value={{\n        files: [],\n        ready: true,\n        dragging,\n        setDragging,\n        onDrop: workspace\n          ? handleDropWithWorkspace\n          : handleDropWithoutWorkspace,\n        parseAttachments: () => [],\n      }}\n    >\n      <HomeContent\n        workspace={workspace}\n        setWorkspace={setWorkspace}\n        threadSlug={null}\n        setThreadSlug={setThreadSlug}\n      />\n    </DndUploaderContext.Provider>\n  );\n}\n\nfunction HomeContent({ workspace, setWorkspace, threadSlug, setThreadSlug }) {\n  const { t } = useTranslation();\n  const navigate = useNavigate();\n  const [loading, setLoading] = useState(false);\n  const { files, parseAttachments } = useContext(DndUploaderContext);\n\n  useEffect(() => {\n    window.dispatchEvent(\n      new CustomEvent(PROMPT_INPUT_EVENT, {\n        detail: { messageContent: \"\", writeMode: \"replace\" },\n      })\n    );\n  }, []);\n\n  async function submitMessage(message, attachments = []) {\n    if (!message || loading) return;\n    setLoading(true);\n    try {\n      let targetWorkspace = workspace;\n      let targetThread = threadSlug;\n\n      if (!targetWorkspace) {\n        targetWorkspace = await createDefaultWorkspace(\n          t(\"new-workspace.placeholder\")\n        );\n        if (!targetWorkspace) {\n          setLoading(false);\n          return;\n        }\n        setWorkspace(targetWorkspace);\n      }\n\n      if (!targetThread) {\n        const { thread } = await Workspace.threads.new(targetWorkspace.slug);\n        targetThread = thread?.slug;\n        if (thread) setThreadSlug(thread.slug);\n      }\n\n      sessionStorage.setItem(\n        PENDING_HOME_MESSAGE,\n        JSON.stringify({ message, attachments })\n      );\n\n      if (targetThread) {\n        navigate(paths.workspace.thread(targetWorkspace.slug, targetThread));\n      } else {\n        navigate(paths.workspace.chat(targetWorkspace.slug));\n      }\n    } catch (error) {\n      console.error(\"Error submitting message:\", error);\n      showToast(\"Failed to send message\", \"error\");\n      setLoading(false);\n    }\n  }\n\n  async function handleSubmit(e) {\n    e.preventDefault();\n    const currentMessage =\n      document.getElementById(PROMPT_INPUT_ID)?.value?.trim() || \"\";\n    await submitMessage(currentMessage, parseAttachments());\n  }\n\n  function sendCommand({\n    text = \"\",\n    autoSubmit = false,\n    writeMode = \"replace\",\n  }) {\n    if (autoSubmit) {\n      if (writeMode === \"append\") {\n        const currentText =\n          document.getElementById(PROMPT_INPUT_ID)?.value ?? \"\";\n        text = currentText + text;\n      }\n      if (!text.trim()) return;\n      submitMessage(text.trim());\n      return;\n    }\n    window.dispatchEvent(\n      new CustomEvent(PROMPT_INPUT_EVENT, {\n        detail: { messageContent: text, writeMode },\n      })\n    );\n  }\n\n  async function handleEditWorkspace() {\n    let targetWorkspace = workspace;\n\n    if (!targetWorkspace) {\n      targetWorkspace = await createDefaultWorkspace(\n        t(\"new-workspace.placeholder\")\n      );\n      if (!targetWorkspace) return;\n      setWorkspace(targetWorkspace);\n    }\n\n    navigate(paths.workspace.settings.generalAppearance(targetWorkspace.slug));\n  }\n\n  return (\n    <div\n      style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n      className=\"transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-zinc-900 light:bg-white w-full h-full overflow-hidden border-none light:border-solid light:border light:border-theme-modal-border\"\n    >\n      {isMobile && <SidebarMobileHeader />}\n      <TextSizeMenu />\n      <WorkspaceModelPicker workspaceSlug={workspace?.slug} />\n      <DnDFileUploaderWrapper>\n        <div className=\"flex flex-col h-full w-full items-center justify-center\">\n          <div className=\"flex flex-col items-center w-full max-w-[750px]\">\n            <h1 className=\"text-white text-xl md:text-2xl mb-11 text-center\">\n              {t(\"main-page.greeting\")}\n            </h1>\n            <PromptInput\n              workspace={workspace}\n              submit={handleSubmit}\n              isStreaming={loading}\n              sendCommand={sendCommand}\n              attachments={files}\n              centered={true}\n              workspaceSlug={workspace?.slug}\n              threadSlug={threadSlug}\n            />\n            <QuickActions\n              hasAvailableWorkspace={!!workspace}\n              onCreateAgent={() => navigate(paths.settings.agentSkills())}\n              onEditWorkspace={handleEditWorkspace}\n              onUploadDocument={() =>\n                document.getElementById(\"dnd-chat-file-uploader\")?.click()\n              }\n            />\n          </div>\n          <SuggestedMessages\n            suggestedMessages={workspace?.suggestedMessages}\n            sendCommand={sendCommand}\n          />\n        </div>\n      </DnDFileUploaderWrapper>\n      <ChatTooltips />\n    </div>\n  );\n}\n\nfunction NoWorkspacesAssigned() {\n  const { t } = useTranslation();\n  return (\n    <div\n      style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n      className=\"transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-zinc-900 light:bg-white w-full h-full overflow-hidden\"\n    >\n      <div className=\"flex flex-col h-full w-full items-center justify-center\">\n        <p className=\"text-white/60 text-sm text-center whitespace-pre-line\">\n          {t(\"home.notAssigned\")}\n        </p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/Main/index.jsx",
    "content": "import React from \"react\";\nimport PasswordModal, { usePasswordModal } from \"@/components/Modals/Password\";\nimport { FullScreenLoader } from \"@/components/Preloader\";\nimport Home from \"./Home\";\nimport { isMobile } from \"react-device-detect\";\nimport Sidebar, { SidebarMobileHeader } from \"@/components/Sidebar\";\n\nexport default function Main() {\n  const { loading, requiresAuth, mode } = usePasswordModal();\n\n  if (loading) return <FullScreenLoader />;\n  if (requiresAuth !== false)\n    return <>{requiresAuth !== null && <PasswordModal mode={mode} />}</>;\n\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-zinc-950 light:bg-slate-50 flex\">\n      {!isMobile ? <Sidebar /> : <SidebarMobileHeader />}\n      <Home />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx",
    "content": "import { useEffect } from \"react\";\nimport paths from \"@/utils/paths\";\nimport { useNavigate } from \"react-router-dom\";\nimport { useTranslation } from \"react-i18next\";\nimport ProviderPrivacy from \"@/components/ProviderPrivacy\";\n\nexport default function DataHandling({ setHeader, setForwardBtn, setBackBtn }) {\n  const { t } = useTranslation();\n  const navigate = useNavigate();\n\n  const TITLE = t(\"onboarding.data.title\");\n  const DESCRIPTION = t(\"onboarding.data.description\");\n\n  useEffect(() => {\n    setHeader({ title: TITLE, description: DESCRIPTION });\n    setForwardBtn({ showing: true, disabled: false, onClick: handleForward });\n    setBackBtn({ showing: false, disabled: false, onClick: handleBack });\n  }, []);\n\n  function handleForward() {\n    navigate(paths.onboarding.survey());\n  }\n\n  function handleBack() {\n    navigate(paths.onboarding.userSetup());\n  }\n\n  return (\n    <div className=\"w-full flex items-center justify-center flex-col gap-y-6\">\n      <ProviderPrivacy />\n      <p className=\"text-theme-text-secondary text-sm font-medium py-1\">\n        {t(\"onboarding.data.settingsHint\")}\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/OnboardingFlow/Steps/Home/index.jsx",
    "content": "import paths from \"@/utils/paths\";\nimport LGroupImg from \"./l_group.png\";\nimport RGroupImg from \"./r_group.png\";\nimport LGroupImgLight from \"./l_group-light.png\";\nimport RGroupImgLight from \"./r_group-light.png\";\nimport AnythingLLMLogo from \"@/media/logo/anything-llm.png\";\nimport { useNavigate } from \"react-router-dom\";\nimport { useTheme } from \"@/hooks/useTheme\";\nimport { useTranslation } from \"react-i18next\";\nimport useRedirectToHomeOnOnboardingComplete from \"@/hooks/useOnboardingComplete\";\n\nconst IMG_SRCSET = {\n  light: {\n    l: LGroupImgLight,\n    r: RGroupImgLight,\n  },\n  default: {\n    l: LGroupImg,\n    r: RGroupImg,\n  },\n};\n\nexport default function OnboardingHome() {\n  const navigate = useNavigate();\n  useRedirectToHomeOnOnboardingComplete();\n  const { theme } = useTheme();\n  const { t } = useTranslation();\n  const srcSet = IMG_SRCSET?.[theme] || IMG_SRCSET.default;\n\n  return (\n    <>\n      <div className=\"relative w-screen h-screen flex overflow-hidden bg-theme-bg-primary\">\n        <div\n          className=\"hidden md:block fixed bottom-10 left-10 w-[320px] h-[320px] bg-no-repeat bg-contain\"\n          style={{ backgroundImage: `url(${srcSet.l})` }}\n        ></div>\n\n        <div\n          className=\"hidden md:block fixed top-10 right-10 w-[320px] h-[320px] bg-no-repeat bg-contain\"\n          style={{ backgroundImage: `url(${srcSet.r})` }}\n        ></div>\n\n        <div className=\"relative flex justify-center items-center m-auto\">\n          <div className=\"flex flex-col justify-center items-center\">\n            <p className=\"text-theme-text-primary font-thin text-[24px]\">\n              {t(\"onboarding.home.title\")}\n            </p>\n            <img\n              src={AnythingLLMLogo}\n              alt=\"AnythingLLM\"\n              className=\"md:h-[50px] flex-shrink-0 max-w-[300px] light:invert\"\n            />\n            <button\n              onClick={() => navigate(paths.onboarding.llmPreference())}\n              className=\"border-[2px] border-theme-text-primary animate-pulse light:animate-none w-full md:max-w-[350px] md:min-w-[300px] text-center py-3 bg-theme-button-primary hover:bg-theme-bg-secondary text-theme-text-primary font-semibold text-sm my-10 rounded-md \"\n            >\n              {t(\"onboarding.home.getStarted\")}\n            </button>\n          </div>\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx",
    "content": "import { MagnifyingGlass } from \"@phosphor-icons/react\";\nimport { useEffect, useState, useRef } from \"react\";\nimport OpenAiLogo from \"@/media/llmprovider/openai.png\";\nimport GenericOpenAiLogo from \"@/media/llmprovider/generic-openai.png\";\nimport AzureOpenAiLogo from \"@/media/llmprovider/azure.png\";\nimport AnthropicLogo from \"@/media/llmprovider/anthropic.png\";\nimport GeminiLogo from \"@/media/llmprovider/gemini.png\";\nimport OllamaLogo from \"@/media/llmprovider/ollama.png\";\nimport LMStudioLogo from \"@/media/llmprovider/lmstudio.png\";\nimport LocalAiLogo from \"@/media/llmprovider/localai.png\";\nimport TogetherAILogo from \"@/media/llmprovider/togetherai.png\";\nimport FireworksAILogo from \"@/media/llmprovider/fireworksai.jpeg\";\nimport MistralLogo from \"@/media/llmprovider/mistral.jpeg\";\nimport HuggingFaceLogo from \"@/media/llmprovider/huggingface.png\";\nimport PerplexityLogo from \"@/media/llmprovider/perplexity.png\";\nimport OpenRouterLogo from \"@/media/llmprovider/openrouter.jpeg\";\nimport GroqLogo from \"@/media/llmprovider/groq.png\";\nimport KoboldCPPLogo from \"@/media/llmprovider/koboldcpp.png\";\nimport TextGenWebUILogo from \"@/media/llmprovider/text-generation-webui.png\";\nimport LiteLLMLogo from \"@/media/llmprovider/litellm.png\";\nimport AWSBedrockLogo from \"@/media/llmprovider/bedrock.png\";\nimport DeepSeekLogo from \"@/media/llmprovider/deepseek.png\";\nimport APIPieLogo from \"@/media/llmprovider/apipie.png\";\nimport NovitaLogo from \"@/media/llmprovider/novita.png\";\nimport XAILogo from \"@/media/llmprovider/xai.png\";\nimport ZAiLogo from \"@/media/llmprovider/zai.png\";\nimport NvidiaNimLogo from \"@/media/llmprovider/nvidia-nim.png\";\nimport CohereLogo from \"@/media/llmprovider/cohere.png\";\nimport PPIOLogo from \"@/media/llmprovider/ppio.png\";\nimport DellProAiStudioLogo from \"@/media/llmprovider/dpais.png\";\nimport MoonshotAiLogo from \"@/media/llmprovider/moonshotai.png\";\nimport CometApiLogo from \"@/media/llmprovider/cometapi.png\";\nimport GiteeAILogo from \"@/media/llmprovider/giteeai.png\";\nimport DockerModelRunnerLogo from \"@/media/llmprovider/docker-model-runner.png\";\nimport PrivateModeLogo from \"@/media/llmprovider/privatemode.png\";\nimport SambaNovaLogo from \"@/media/llmprovider/sambanova.png\";\nimport LemonadeLogo from \"@/media/llmprovider/lemonade.png\";\n\nimport OpenAiOptions from \"@/components/LLMSelection/OpenAiOptions\";\nimport GenericOpenAiOptions from \"@/components/LLMSelection/GenericOpenAiOptions\";\nimport AzureAiOptions from \"@/components/LLMSelection/AzureAiOptions\";\nimport AnthropicAiOptions from \"@/components/LLMSelection/AnthropicAiOptions\";\nimport LMStudioOptions from \"@/components/LLMSelection/LMStudioOptions\";\nimport LocalAiOptions from \"@/components/LLMSelection/LocalAiOptions\";\nimport GeminiLLMOptions from \"@/components/LLMSelection/GeminiLLMOptions\";\nimport OllamaLLMOptions from \"@/components/LLMSelection/OllamaLLMOptions\";\nimport MistralOptions from \"@/components/LLMSelection/MistralOptions\";\nimport HuggingFaceOptions from \"@/components/LLMSelection/HuggingFaceOptions\";\nimport TogetherAiOptions from \"@/components/LLMSelection/TogetherAiOptions\";\nimport FireworksAiOptions from \"@/components/LLMSelection/FireworksAiOptions\";\nimport PerplexityOptions from \"@/components/LLMSelection/PerplexityOptions\";\nimport OpenRouterOptions from \"@/components/LLMSelection/OpenRouterOptions\";\nimport GroqAiOptions from \"@/components/LLMSelection/GroqAiOptions\";\nimport CohereAiOptions from \"@/components/LLMSelection/CohereAiOptions\";\nimport KoboldCPPOptions from \"@/components/LLMSelection/KoboldCPPOptions\";\nimport TextGenWebUIOptions from \"@/components/LLMSelection/TextGenWebUIOptions\";\nimport LiteLLMOptions from \"@/components/LLMSelection/LiteLLMOptions\";\nimport AWSBedrockLLMOptions from \"@/components/LLMSelection/AwsBedrockLLMOptions\";\nimport DeepSeekOptions from \"@/components/LLMSelection/DeepSeekOptions\";\nimport ApiPieLLMOptions from \"@/components/LLMSelection/ApiPieOptions\";\nimport NovitaLLMOptions from \"@/components/LLMSelection/NovitaLLMOptions\";\nimport XAILLMOptions from \"@/components/LLMSelection/XAiLLMOptions\";\nimport ZAiLLMOptions from \"@/components/LLMSelection/ZAiLLMOptions\";\nimport NvidiaNimOptions from \"@/components/LLMSelection/NvidiaNimOptions\";\nimport PPIOLLMOptions from \"@/components/LLMSelection/PPIOLLMOptions\";\nimport DellProAiStudioOptions from \"@/components/LLMSelection/DPAISOptions\";\nimport MoonshotAiOptions from \"@/components/LLMSelection/MoonshotAiOptions\";\nimport CometApiLLMOptions from \"@/components/LLMSelection/CometApiLLMOptions\";\nimport GiteeAiOptions from \"@/components/LLMSelection/GiteeAIOptions\";\nimport DockerModelRunnerOptions from \"@/components/LLMSelection/DockerModelRunnerOptions\";\nimport PrivateModeOptions from \"@/components/LLMSelection/PrivateModeOptions\";\nimport SambaNovaOptions from \"@/components/LLMSelection/SambaNovaOptions\";\nimport LemonadeOptions from \"@/components/LLMSelection/LemonadeOptions\";\n\nimport LLMItem from \"@/components/LLMSelection/LLMItem\";\nimport System from \"@/models/system\";\nimport paths from \"@/utils/paths\";\nimport showToast from \"@/utils/toast\";\nimport { useNavigate } from \"react-router-dom\";\nimport { useTranslation } from \"react-i18next\";\n\nconst LLMS = [\n  {\n    name: \"OpenAI\",\n    value: \"openai\",\n    logo: OpenAiLogo,\n    options: (settings) => <OpenAiOptions settings={settings} />,\n    description: \"The standard option for most non-commercial use.\",\n  },\n  {\n    name: \"Azure OpenAI\",\n    value: \"azure\",\n    logo: AzureOpenAiLogo,\n    options: (settings) => <AzureAiOptions settings={settings} />,\n    description: \"The enterprise option of OpenAI hosted on Azure services.\",\n  },\n  {\n    name: \"Anthropic\",\n    value: \"anthropic\",\n    logo: AnthropicLogo,\n    options: (settings) => <AnthropicAiOptions settings={settings} />,\n    description: \"A friendly AI Assistant hosted by Anthropic.\",\n  },\n  {\n    name: \"Gemini\",\n    value: \"gemini\",\n    logo: GeminiLogo,\n    options: (settings) => <GeminiLLMOptions settings={settings} />,\n    description: \"Google's largest and most capable AI model\",\n  },\n  {\n    name: \"NVIDIA NIM\",\n    value: \"nvidia-nim\",\n    logo: NvidiaNimLogo,\n    options: (settings) => <NvidiaNimOptions settings={settings} />,\n    description:\n      \"Run full parameter LLMs directly on your NVIDIA RTX GPU using NVIDIA NIM.\",\n  },\n  {\n    name: \"HuggingFace\",\n    value: \"huggingface\",\n    logo: HuggingFaceLogo,\n    options: (settings) => <HuggingFaceOptions settings={settings} />,\n    description:\n      \"Access 150,000+ open-source LLMs and the world's AI community\",\n  },\n  {\n    name: \"Ollama\",\n    value: \"ollama\",\n    logo: OllamaLogo,\n    options: (settings) => <OllamaLLMOptions settings={settings} />,\n    description: \"Run LLMs locally on your own machine.\",\n  },\n  {\n    name: \"Dell Pro AI Studio\",\n    value: \"dpais\",\n    logo: DellProAiStudioLogo,\n    options: (settings) => <DellProAiStudioOptions settings={settings} />,\n    description:\n      \"Run powerful LLMs quickly on NPU powered by Dell Pro AI Studio.\",\n  },\n  {\n    name: \"LM Studio\",\n    value: \"lmstudio\",\n    logo: LMStudioLogo,\n    options: (settings) => <LMStudioOptions settings={settings} />,\n    description:\n      \"Discover, download, and run thousands of cutting edge LLMs in a few clicks.\",\n  },\n  {\n    name: \"Docker Model Runner\",\n    value: \"docker-model-runner\",\n    logo: DockerModelRunnerLogo,\n    options: (settings) => <DockerModelRunnerOptions settings={settings} />,\n    description: \"Run LLMs using Docker Model Runner.\",\n  },\n  {\n    name: \"Lemonade\",\n    value: \"lemonade\",\n    logo: LemonadeLogo,\n    options: (settings) => <LemonadeOptions settings={settings} />,\n    description:\n      \"Run local LLMs, ASR, TTS, and more in a single unified AI runtime.\",\n  },\n  {\n    name: \"Local AI\",\n    value: \"localai\",\n    logo: LocalAiLogo,\n    options: (settings) => <LocalAiOptions settings={settings} />,\n    description: \"Run LLMs locally on your own machine.\",\n  },\n  {\n    name: \"SambaNova\",\n    value: \"sambanova\",\n    logo: SambaNovaLogo,\n    options: (settings) => <SambaNovaOptions settings={settings} />,\n    description: \"Run open source models from SambaNova.\",\n  },\n  {\n    name: \"Novita AI\",\n    value: \"novita\",\n    logo: NovitaLogo,\n    options: (settings) => <NovitaLLMOptions settings={settings} />,\n    description:\n      \"Reliable, Scalable, and Cost-Effective for LLMs from Novita AI\",\n  },\n  {\n    name: \"KoboldCPP\",\n    value: \"koboldcpp\",\n    logo: KoboldCPPLogo,\n    options: (settings) => <KoboldCPPOptions settings={settings} />,\n    description: \"Run local LLMs using koboldcpp.\",\n  },\n  {\n    name: \"Oobabooga Web UI\",\n    value: \"textgenwebui\",\n    logo: TextGenWebUILogo,\n    options: (settings) => <TextGenWebUIOptions settings={settings} />,\n    description: \"Run local LLMs using Oobabooga's Text Generation Web UI.\",\n  },\n  {\n    name: \"Together AI\",\n    value: \"togetherai\",\n    logo: TogetherAILogo,\n    options: (settings) => <TogetherAiOptions settings={settings} />,\n    description: \"Run open source models from Together AI.\",\n  },\n  {\n    name: \"Fireworks AI\",\n    value: \"fireworksai\",\n    logo: FireworksAILogo,\n    options: (settings) => <FireworksAiOptions settings={settings} />,\n    description:\n      \"The fastest and most efficient inference engine to build production-ready, compound AI systems.\",\n  },\n  {\n    name: \"Mistral\",\n    value: \"mistral\",\n    logo: MistralLogo,\n    options: (settings) => <MistralOptions settings={settings} />,\n    description: \"Run open source models from Mistral AI.\",\n  },\n  {\n    name: \"Perplexity AI\",\n    value: \"perplexity\",\n    logo: PerplexityLogo,\n    options: (settings) => <PerplexityOptions settings={settings} />,\n    description:\n      \"Run powerful and internet-connected models hosted by Perplexity AI.\",\n  },\n  {\n    name: \"OpenRouter\",\n    value: \"openrouter\",\n    logo: OpenRouterLogo,\n    options: (settings) => <OpenRouterOptions settings={settings} />,\n    description: \"A unified interface for LLMs.\",\n  },\n  {\n    name: \"Groq\",\n    value: \"groq\",\n    logo: GroqLogo,\n    options: (settings) => <GroqAiOptions settings={settings} />,\n    description:\n      \"The fastest LLM inferencing available for real-time AI applications.\",\n  },\n  {\n    name: \"Cohere\",\n    value: \"cohere\",\n    logo: CohereLogo,\n    options: (settings) => <CohereAiOptions settings={settings} />,\n    description: \"Run Cohere's powerful Command models.\",\n  },\n  {\n    name: \"LiteLLM\",\n    value: \"litellm\",\n    logo: LiteLLMLogo,\n    options: (settings) => <LiteLLMOptions settings={settings} />,\n    description: \"Run LiteLLM's OpenAI compatible proxy for various LLMs.\",\n  },\n  {\n    name: \"DeepSeek\",\n    value: \"deepseek\",\n    logo: DeepSeekLogo,\n    options: (settings) => <DeepSeekOptions settings={settings} />,\n    description: \"Run DeepSeek's powerful LLMs.\",\n  },\n  {\n    name: \"PPIO\",\n    value: \"ppio\",\n    logo: PPIOLogo,\n    options: (settings) => <PPIOLLMOptions settings={settings} />,\n    description:\n      \"Run stable and cost-efficient open-source LLM APIs, such as DeepSeek, Llama, Qwen etc.\",\n  },\n  {\n    name: \"APIpie\",\n    value: \"apipie\",\n    logo: APIPieLogo,\n    options: (settings) => <ApiPieLLMOptions settings={settings} />,\n    description: \"A unified API of AI services from leading providers\",\n  },\n  {\n    name: \"Generic OpenAI\",\n    value: \"generic-openai\",\n    logo: GenericOpenAiLogo,\n    options: (settings) => <GenericOpenAiOptions settings={settings} />,\n    description:\n      \"Connect to any OpenAi-compatible service via a custom configuration\",\n  },\n  {\n    name: \"AWS Bedrock\",\n    value: \"bedrock\",\n    logo: AWSBedrockLogo,\n    options: (settings) => <AWSBedrockLLMOptions settings={settings} />,\n    description: \"Run powerful foundation models privately with AWS Bedrock.\",\n  },\n  {\n    name: \"Privatemode\",\n    value: \"privatemode\",\n    logo: PrivateModeLogo,\n    options: (settings) => <PrivateModeOptions settings={settings} />,\n    description: \"Run LLMs with end-to-end encryption.\",\n  },\n  {\n    name: \"xAI\",\n    value: \"xai\",\n    logo: XAILogo,\n    options: (settings) => <XAILLMOptions settings={settings} />,\n    description: \"Run xAI's powerful LLMs like Grok-2 and more.\",\n  },\n  {\n    name: \"Z.AI\",\n    value: \"zai\",\n    logo: ZAiLogo,\n    options: (settings) => <ZAiLLMOptions settings={settings} />,\n    description: \"Run Z.AI's powerful GLM models.\",\n  },\n  {\n    name: \"Moonshot AI\",\n    value: \"moonshotai\",\n    logo: MoonshotAiLogo,\n    options: (settings) => <MoonshotAiOptions settings={settings} />,\n    description: \"Run Moonshot AI's powerful LLMs.\",\n  },\n  {\n    name: \"CometAPI\",\n    value: \"cometapi\",\n    logo: CometApiLogo,\n    options: (settings) => <CometApiLLMOptions settings={settings} />,\n    description: \"500+ AI Models all in one API.\",\n  },\n  {\n    name: \"GiteeAI\",\n    value: \"giteeai\",\n    logo: GiteeAILogo,\n    options: (settings) => <GiteeAiOptions settings={settings} />,\n    description: \"Run GiteeAI's powerful LLMs.\",\n  },\n];\n\nexport default function LLMPreference({\n  setHeader,\n  setForwardBtn,\n  setBackBtn,\n}) {\n  const { t } = useTranslation();\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [filteredLLMs, setFilteredLLMs] = useState([]);\n  const [selectedLLM, setSelectedLLM] = useState(null);\n  const [settings, setSettings] = useState(null);\n  const formRef = useRef(null);\n  const hiddenSubmitButtonRef = useRef(null);\n  const isHosted = window.location.hostname.includes(\"useanything.com\");\n  const navigate = useNavigate();\n\n  const TITLE = t(\"onboarding.llm.title\");\n  const DESCRIPTION = t(\"onboarding.llm.description\");\n\n  useEffect(() => {\n    async function fetchKeys() {\n      const _settings = await System.keys();\n      setSettings(_settings);\n      setSelectedLLM(_settings?.LLMProvider || \"openai\");\n    }\n    fetchKeys();\n  }, []);\n\n  async function handleForward() {\n    try {\n      await System.markOnboardingComplete();\n      console.log(\"Onboarding complete\");\n    } catch (error) {\n      console.error(\"Onboarding complete failed\", error);\n    } finally {\n      if (hiddenSubmitButtonRef.current) {\n        hiddenSubmitButtonRef.current.click();\n      }\n    }\n  }\n\n  function handleBack() {\n    navigate(paths.onboarding.home());\n  }\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = e.target;\n    const data = {};\n    const formData = new FormData(form);\n    data.LLMProvider = selectedLLM;\n    // Default to AnythingLLM embedder and LanceDB\n    data.EmbeddingEngine = \"native\";\n    data.VectorDB = \"lancedb\";\n    for (var [key, value] of formData.entries()) data[key] = value;\n\n    const { error } = await System.updateSystem(data);\n    if (error) {\n      showToast(`Failed to save LLM settings: ${error}`, \"error\");\n      return;\n    }\n    navigate(paths.onboarding.userSetup());\n  };\n\n  useEffect(() => {\n    setHeader({ title: TITLE, description: DESCRIPTION });\n    setForwardBtn({ showing: true, disabled: false, onClick: handleForward });\n    setBackBtn({ showing: true, disabled: false, onClick: handleBack });\n  }, []);\n\n  useEffect(() => {\n    const filtered = LLMS.filter((llm) =>\n      llm.name.toLowerCase().includes(searchQuery.toLowerCase())\n    );\n    setFilteredLLMs(filtered);\n  }, [searchQuery, selectedLLM]);\n\n  return (\n    <div>\n      <form ref={formRef} onSubmit={handleSubmit} className=\"w-full\">\n        <div className=\"w-full relative border-theme-chat-input-border shadow border-2 rounded-lg text-white\">\n          <div className=\"w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm\">\n            <div className=\"w-full flex items-center sticky top-0\">\n              <MagnifyingGlass\n                size={16}\n                weight=\"bold\"\n                className=\"absolute left-4 z-30 text-theme-text-primary\"\n              />\n              <input\n                type=\"text\"\n                placeholder=\"Search LLM providers\"\n                className=\"bg-theme-bg-secondary placeholder:text-theme-text-secondary z-20 pl-10 h-[38px] rounded-full w-full px-4 py-1 text-sm border border-theme-chat-input-border outline-none focus:outline-primary-button active:outline-primary-button outline-none text-theme-text-primary\"\n                onChange={(e) => setSearchQuery(e.target.value)}\n                autoComplete=\"off\"\n                onKeyDown={(e) => {\n                  if (e.key === \"Enter\") e.preventDefault();\n                }}\n              />\n            </div>\n          </div>\n          <div className=\"px-4 pt-[70px] flex flex-col gap-y-1 max-h-[390px] overflow-y-auto no-scroll pb-4\">\n            {filteredLLMs.map((llm) => {\n              if (llm.value === \"native\" && isHosted) return null;\n              return (\n                <LLMItem\n                  key={llm.name}\n                  name={llm.name}\n                  value={llm.value}\n                  image={llm.logo}\n                  description={llm.description}\n                  checked={selectedLLM === llm.value}\n                  onClick={() => setSelectedLLM(llm.value)}\n                />\n              );\n            })}\n          </div>\n        </div>\n        <div className=\"mt-4 flex flex-col gap-y-1\">\n          {selectedLLM &&\n            LLMS.find((llm) => llm.value === selectedLLM)?.options(settings)}\n        </div>\n        <button\n          type=\"submit\"\n          ref={hiddenSubmitButtonRef}\n          hidden\n          aria-hidden=\"true\"\n        ></button>\n      </form>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/OnboardingFlow/Steps/Survey/index.jsx",
    "content": "import {\n  COMPLETE_QUESTIONNAIRE,\n  ONBOARDING_SURVEY_URL,\n} from \"@/utils/constants\";\nimport paths from \"@/utils/paths\";\nimport { CheckCircle } from \"@phosphor-icons/react\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\nimport { useTranslation } from \"react-i18next\";\nimport Workspace from \"@/models/workspace\";\n\nasync function sendQuestionnaire({ email, useCase, comment }) {\n  if (import.meta.env.DEV) {\n    console.log(\"sendQuestionnaire\", { email, useCase, comment });\n    return;\n  }\n\n  const data = JSON.stringify({\n    email,\n    useCase,\n    comment,\n    sourceId: \"0VRjqHh6Vukqi0x0Vd0n/m8JuT7k8nOz\",\n  });\n\n  if (!navigator.sendBeacon) {\n    console.log(\"navigator.sendBeacon not supported, falling back to fetch\");\n    return fetch(ONBOARDING_SURVEY_URL, {\n      method: \"POST\",\n      body: data,\n    })\n      .then(() => {\n        window.localStorage.setItem(COMPLETE_QUESTIONNAIRE, true);\n        console.log(`✅ Questionnaire responses sent.`);\n      })\n      .catch((error) => {\n        console.error(`sendQuestionnaire`, error.message);\n      });\n  }\n\n  navigator.sendBeacon(ONBOARDING_SURVEY_URL, data);\n  window.localStorage.setItem(COMPLETE_QUESTIONNAIRE, true);\n  console.log(`✅ Questionnaire responses sent.`);\n}\n\nexport default function Survey({ setHeader, setForwardBtn, setBackBtn }) {\n  const { t } = useTranslation();\n  const [selectedOption, setSelectedOption] = useState(\"\");\n  const formRef = useRef(null);\n  const navigate = useNavigate();\n  const submitRef = useRef(null);\n\n  const TITLE = t(\"onboarding.survey.title\");\n  const DESCRIPTION = t(\"onboarding.survey.description\");\n\n  function handleForward() {\n    if (!!window?.localStorage?.getItem(COMPLETE_QUESTIONNAIRE)) {\n      navigate(paths.home());\n      return;\n    }\n\n    if (!formRef.current) {\n      skipSurvey();\n      return;\n    }\n\n    // Check if any inputs are not empty. If that is the case, trigger form validation.\n    // via the requestSubmit() handler\n    const formData = new FormData(formRef.current);\n    if (\n      !!formData.get(\"email\") ||\n      !!formData.get(\"use_case\") ||\n      !!formData.get(\"comment\")\n    ) {\n      formRef.current.requestSubmit();\n      return;\n    }\n\n    skipSurvey();\n  }\n\n  function skipSurvey() {\n    navigate(paths.home());\n  }\n\n  function handleBack() {\n    navigate(paths.onboarding.dataHandling());\n  }\n\n  useEffect(() => {\n    setHeader({ title: TITLE, description: DESCRIPTION });\n    setForwardBtn({ showing: true, disabled: false, onClick: handleForward });\n    setBackBtn({ showing: true, disabled: false, onClick: handleBack });\n  }, []);\n\n  useEffect(() => {\n    async function createDefaultWorkspace() {\n      const workspaces = await Workspace.all();\n      if (workspaces.length === 0) {\n        await Workspace.new({\n          name: t(\"new-workspace.placeholder\"),\n          onboardingComplete: true,\n        });\n      }\n    }\n    createDefaultWorkspace();\n  }, []);\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = e.target;\n    const formData = new FormData(form);\n\n    await sendQuestionnaire({\n      email: formData.get(\"email\"),\n      useCase: formData.get(\"use_case\") || \"other\",\n      comment: formData.get(\"comment\") || null,\n    });\n\n    navigate(paths.home());\n  };\n\n  if (!!window?.localStorage?.getItem(COMPLETE_QUESTIONNAIRE)) {\n    return (\n      <div className=\"w-full flex justify-center items-center py-40\">\n        <div className=\"w-full flex items-center justify-center px-1 md:px-8 py-4\">\n          <div className=\"w-auto flex flex-col gap-y-1 items-center\">\n            <CheckCircle size={60} className=\"text-green-500\" />\n            <p className=\"text-white text-lg\">\n              {t(\"onboarding.survey.thankYou\")}\n            </p>\n            <a\n              href={paths.mailToMintplex()}\n              className=\"text-sky-400 underline text-xs\"\n            >\n              team@mintplexlabs.com\n            </a>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"w-full flex justify-center bo\">\n      <form onSubmit={handleSubmit} ref={formRef} className=\"\">\n        <div className=\"md:min-w-[400px]\">\n          <label\n            htmlFor=\"email\"\n            className=\"text-theme-text-primary text-base font-medium\"\n          >\n            {t(\"onboarding.survey.email\")}{\" \"}\n          </label>\n          <input\n            name=\"email\"\n            type=\"email\"\n            placeholder=\"you@gmail.com\"\n            required={true}\n            className=\"mt-2 bg-theme-settings-input-bg text-white focus:outline-primary-button active:outline-primary-button placeholder:text-theme-settings-input-placeholder outline-none text-sm font-medium font-['Plus Jakarta Sans'] leading-tight w-full h-11 p-2.5 bg-theme-settings-input-bg rounded-lg\"\n          />\n        </div>\n\n        <div className=\"mt-8\">\n          <label\n            className=\"text-theme-text-primary text-base font-medium\"\n            htmlFor=\"use_case\"\n          >\n            {t(\"onboarding.survey.useCase\")}{\" \"}\n          </label>\n          <div className=\"mt-2 gap-y-3 flex flex-col\">\n            <label\n              className={`border-solid transition-all duration-300 w-full h-11 p-2.5 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border ${\n                selectedOption === \"job\"\n                  ? \"border-theme-sidebar-item-workspace-active bg-theme-bg-secondary\"\n                  : \"border-theme-sidebar-border\"\n              } hover:border-theme-sidebar-border hover:bg-theme-bg-secondary`}\n            >\n              <input\n                type=\"radio\"\n                name=\"use_case\"\n                value={\"job\"}\n                checked={selectedOption === \"job\"}\n                onChange={(e) => setSelectedOption(e.target.value)}\n                className=\"hidden\"\n              />\n              <div\n                className={`w-4 h-4 rounded-full border-2 border-theme-sidebar-border mr-2 ${\n                  selectedOption === \"job\"\n                    ? \"bg-[var(--theme-sidebar-item-workspace-active)]\"\n                    : \"\"\n                }`}\n              ></div>\n              <div className=\"text-theme-text-primary text-sm font-medium font-['Plus Jakarta Sans'] leading-tight\">\n                {t(\"onboarding.survey.useCaseWork\")}\n              </div>\n            </label>\n            <label\n              className={`border-solid transition-all duration-300 w-full h-11 p-2.5 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border-[1px] ${\n                selectedOption === \"personal\"\n                  ? \"border-theme-sidebar-item-workspace-active bg-theme-bg-secondary\"\n                  : \"border-theme-sidebar-border\"\n              } hover:border-theme-sidebar-border hover:bg-theme-bg-secondary`}\n            >\n              <input\n                type=\"radio\"\n                name=\"use_case\"\n                value={\"personal\"}\n                checked={selectedOption === \"personal\"}\n                onChange={(e) => setSelectedOption(e.target.value)}\n                className=\"hidden\"\n              />\n              <div\n                className={`w-4 h-4 rounded-full border-2 border-theme-sidebar-border mr-2 ${\n                  selectedOption === \"personal\"\n                    ? \"bg-[var(--theme-sidebar-item-workspace-active)]\"\n                    : \"\"\n                }`}\n              ></div>\n              <div className=\"text-theme-text-primary text-sm font-medium font-['Plus Jakarta Sans'] leading-tight\">\n                {t(\"onboarding.survey.useCasePersonal\")}\n              </div>\n            </label>\n            <label\n              className={`border-solid transition-all duration-300 w-full h-11 p-2.5 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border-[1px] ${\n                selectedOption === \"other\"\n                  ? \"border-theme-sidebar-item-workspace-active bg-theme-bg-secondary\"\n                  : \"border-theme-sidebar-border\"\n              } hover:border-theme-sidebar-border hover:bg-theme-bg-secondary`}\n            >\n              <input\n                type=\"radio\"\n                name=\"use_case\"\n                value={\"other\"}\n                checked={selectedOption === \"other\"}\n                onChange={(e) => setSelectedOption(e.target.value)}\n                className=\"hidden\"\n              />\n              <div\n                className={`w-4 h-4 rounded-full border-2 border-theme-sidebar-border mr-2 ${\n                  selectedOption === \"other\"\n                    ? \"bg-[var(--theme-sidebar-item-workspace-active)]\"\n                    : \"\"\n                }`}\n              ></div>\n              <div className=\"text-theme-text-primary text-sm font-medium font-['Plus Jakarta Sans'] leading-tight\">\n                {t(\"onboarding.survey.useCaseOther\")}\n              </div>\n            </label>\n          </div>\n        </div>\n\n        <div className=\"mt-8\">\n          <label htmlFor=\"comment\" className=\"text-white text-base font-medium\">\n            {t(\"onboarding.survey.comment\")}{\" \"}\n            <span className=\"text-neutral-400 text-base font-light\">\n              ({t(\"common.optional\")})\n            </span>\n          </label>\n          <textarea\n            name=\"comment\"\n            rows={5}\n            className=\"mt-2 bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button placeholder:text-theme-settings-input-placeholder outline-none block w-full p-2.5\"\n            placeholder={t(\"onboarding.survey.commentPlaceholder\")}\n            wrap=\"soft\"\n            autoComplete=\"off\"\n          />\n        </div>\n        <button\n          type=\"submit\"\n          ref={submitRef}\n          hidden\n          aria-hidden=\"true\"\n        ></button>\n\n        <div className=\"w-full flex items-center justify-center\">\n          <button\n            type=\"button\"\n            onClick={skipSurvey}\n            className=\"text-white text-base font-medium text-opacity-30 hover:text-opacity-100 hover:text-teal mt-8\"\n          >\n            {t(\"onboarding.survey.skip\")}\n          </button>\n        </div>\n      </form>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx",
    "content": "import System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport debounce from \"lodash.debounce\";\nimport paths from \"@/utils/paths\";\nimport { useNavigate } from \"react-router-dom\";\nimport { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from \"@/utils/constants\";\nimport { useTranslation } from \"react-i18next\";\nimport { USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH } from \"@/utils/username\";\nimport { PW_REGEX } from \"@/pages/GeneralSettings/Security\";\n\nexport default function UserSetup({ setHeader, setForwardBtn, setBackBtn }) {\n  const { t } = useTranslation();\n  const [selectedOption, setSelectedOption] = useState(\"\");\n  const [singleUserPasswordValid, setSingleUserPasswordValid] = useState(false);\n  const [multiUserLoginValid, setMultiUserLoginValid] = useState(false);\n  const [enablePassword, setEnablePassword] = useState(false);\n  const myTeamSubmitRef = useRef(null);\n  const justMeSubmitRef = useRef(null);\n  const navigate = useNavigate();\n\n  const TITLE = t(\"onboarding.userSetup.title\");\n  const DESCRIPTION = t(\"onboarding.userSetup.description\");\n\n  function handleForward() {\n    if (selectedOption === \"just_me\" && enablePassword) {\n      justMeSubmitRef.current?.click();\n    } else if (selectedOption === \"just_me\" && !enablePassword) {\n      navigate(paths.onboarding.dataHandling());\n    } else if (selectedOption === \"my_team\") {\n      myTeamSubmitRef.current?.click();\n    }\n  }\n\n  function handleBack() {\n    navigate(paths.onboarding.llmPreference());\n  }\n\n  useEffect(() => {\n    let isDisabled = true;\n    if (selectedOption === \"just_me\") {\n      isDisabled = !singleUserPasswordValid;\n    } else if (selectedOption === \"my_team\") {\n      isDisabled = !multiUserLoginValid;\n    }\n\n    setForwardBtn({\n      showing: true,\n      disabled: isDisabled,\n      onClick: handleForward,\n    });\n  }, [selectedOption, singleUserPasswordValid, multiUserLoginValid]);\n\n  useEffect(() => {\n    setHeader({ title: TITLE, description: DESCRIPTION });\n    setBackBtn({ showing: true, disabled: false, onClick: handleBack });\n  }, []);\n\n  return (\n    <div className=\"w-full flex items-center justify-center flex-col gap-y-6\">\n      <div className=\"flex flex-col border rounded-lg border-white/20 light:border-theme-sidebar-border p-8 items-center gap-y-4 w-full max-w-[600px]\">\n        <div className=\" text-white text-sm font-semibold md:-ml-44\">\n          {t(\"onboarding.userSetup.howManyUsers\")}\n        </div>\n        <div className=\"flex flex-col md:flex-row gap-6 w-full justify-center\">\n          <button\n            onClick={() => setSelectedOption(\"just_me\")}\n            className={`${\n              selectedOption === \"just_me\"\n                ? \"text-sky-400 border-sky-400/70\"\n                : \"text-theme-text-primary border-theme-sidebar-border\"\n            } min-w-[230px] h-11 p-4 rounded-[10px] border-2  justify-center items-center gap-[100px] inline-flex hover:border-sky-400/70 hover:text-sky-400 transition-all duration-300`}\n          >\n            <div className=\"text-center text-sm font-bold\">\n              {t(\"onboarding.userSetup.justMe\")}\n            </div>\n          </button>\n          <button\n            onClick={() => setSelectedOption(\"my_team\")}\n            className={`${\n              selectedOption === \"my_team\"\n                ? \"text-sky-400 border-sky-400/70\"\n                : \"text-theme-text-primary border-theme-sidebar-border\"\n            } min-w-[230px] h-11 p-4 rounded-[10px] border-2  justify-center items-center gap-[100px] inline-flex hover:border-sky-400/70 hover:text-sky-400 transition-all duration-300`}\n          >\n            <div className=\"text-center text-sm font-bold\">\n              {t(\"onboarding.userSetup.myTeam\")}\n            </div>\n          </button>\n        </div>\n      </div>\n      {selectedOption === \"just_me\" && (\n        <JustMe\n          setSingleUserPasswordValid={setSingleUserPasswordValid}\n          enablePassword={enablePassword}\n          setEnablePassword={setEnablePassword}\n          justMeSubmitRef={justMeSubmitRef}\n          navigate={navigate}\n        />\n      )}\n      {selectedOption === \"my_team\" && (\n        <MyTeam\n          setMultiUserLoginValid={setMultiUserLoginValid}\n          myTeamSubmitRef={myTeamSubmitRef}\n          navigate={navigate}\n        />\n      )}\n    </div>\n  );\n}\n\nconst JustMe = ({\n  setSingleUserPasswordValid,\n  enablePassword,\n  setEnablePassword,\n  justMeSubmitRef,\n  navigate,\n}) => {\n  const { t } = useTranslation();\n  const [itemSelected, setItemSelected] = useState(false);\n  const [password, setPassword] = useState(\"\");\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = e.target;\n    const formData = new FormData(form);\n\n    if (!PW_REGEX.test(formData.get(\"password\"))) {\n      showToast(\n        `Your password has restricted characters in it. Allowed symbols are _,-,!,@,$,%,^,&,*,(,),;`,\n        \"error\"\n      );\n      return;\n    }\n\n    const { error } = await System.updateSystemPassword({\n      usePassword: true,\n      newPassword: formData.get(\"password\"),\n    });\n\n    if (error) {\n      showToast(`Failed to set password: ${error}`, \"error\");\n      return;\n    }\n\n    // Auto-request token with password that was just set so they\n    // are not redirected to login after completion.\n    const { token } = await System.requestToken({\n      password: formData.get(\"password\"),\n    });\n    window.localStorage.removeItem(AUTH_USER);\n    window.localStorage.removeItem(AUTH_TIMESTAMP);\n    window.localStorage.setItem(AUTH_TOKEN, token);\n\n    navigate(paths.onboarding.dataHandling());\n  };\n\n  const setNewPassword = (e) => setPassword(e.target.value);\n  const handlePasswordChange = debounce(setNewPassword, 500);\n\n  function handleYes() {\n    setItemSelected(true);\n    setEnablePassword(true);\n  }\n\n  function handleNo() {\n    setItemSelected(true);\n    setEnablePassword(false);\n  }\n\n  useEffect(() => {\n    if (enablePassword && itemSelected && password.length >= 8) {\n      setSingleUserPasswordValid(true);\n    } else if (!enablePassword && itemSelected) {\n      setSingleUserPasswordValid(true);\n    } else {\n      setSingleUserPasswordValid(false);\n    }\n  });\n  return (\n    <div className=\"w-full flex items-center justify-center flex-col gap-y-6\">\n      <div className=\"flex flex-col border rounded-lg border-white/20 light:border-theme-sidebar-border p-8 items-center gap-y-4 w-full max-w-[600px]\">\n        <div className=\" text-white text-sm font-semibold md:-ml-56\">\n          {t(\"onboarding.userSetup.setPassword\")}\n        </div>\n        <div className=\"flex flex-col md:flex-row gap-6 w-full justify-center\">\n          <button\n            onClick={handleYes}\n            className={`${\n              enablePassword && itemSelected\n                ? \"text-sky-400 border-sky-400/70\"\n                : \"text-theme-text-primary border-theme-sidebar-border\"\n            } min-w-[230px] h-11 p-4 rounded-[10px] border-2  justify-center items-center gap-[100px] inline-flex hover:border-sky-400/70 hover:text-sky-400 transition-all duration-300`}\n          >\n            <div className=\"text-center text-sm font-bold\">\n              {t(\"common.yes\")}\n            </div>\n          </button>\n          <button\n            onClick={handleNo}\n            className={`${\n              !enablePassword && itemSelected\n                ? \"text-sky-400 border-sky-400/70\"\n                : \"text-theme-text-primary border-theme-sidebar-border\"\n            } min-w-[230px] h-11 p-4 rounded-[10px] border-2  justify-center items-center gap-[100px] inline-flex hover:border-sky-400/70 hover:text-sky-400 transition-all duration-300`}\n          >\n            <div className=\"text-center text-sm font-bold\">\n              {t(\"common.no\")}\n            </div>\n          </button>\n        </div>\n        {enablePassword && (\n          <form className=\"w-full mt-4\" onSubmit={handleSubmit}>\n            <label\n              htmlFor=\"name\"\n              className=\"block mb-3 text-sm font-medium text-white\"\n            >\n              {t(\"onboarding.userSetup.instancePassword\")}\n            </label>\n            <input\n              name=\"password\"\n              type=\"password\"\n              className=\"border-none bg-theme-settings-input-bg text-white text-sm rounded-lg block w-full p-2.5 focus:outline-primary-button active:outline-primary-button outline-none placeholder:text-theme-text-secondary\"\n              placeholder=\"Your admin password\"\n              minLength={6}\n              required={true}\n              autoComplete=\"off\"\n              onChange={handlePasswordChange}\n            />\n            <div className=\"mt-4 text-white text-opacity-80 text-xs font-base -mb-2\">\n              {t(\"onboarding.userSetup.passwordReq\")}\n              <br />\n              <i>{t(\"onboarding.userSetup.passwordWarn\")}</i>{\" \"}\n            </div>\n            <button\n              type=\"submit\"\n              ref={justMeSubmitRef}\n              hidden\n              aria-hidden=\"true\"\n            ></button>\n          </form>\n        )}\n      </div>\n    </div>\n  );\n};\n\nconst MyTeam = ({ setMultiUserLoginValid, myTeamSubmitRef, navigate }) => {\n  const { t } = useTranslation();\n  const [username, setUsername] = useState(\"\");\n  const [password, setPassword] = useState(\"\");\n\n  const handleSubmit = async (e) => {\n    e.preventDefault();\n    const form = e.target;\n    const formData = new FormData(form);\n    const data = {\n      username: formData.get(\"username\"),\n      password: formData.get(\"password\"),\n    };\n    const { success, error } = await System.setupMultiUser(data);\n    if (!success) {\n      showToast(`Error: ${error}`, \"error\");\n      return;\n    }\n\n    navigate(paths.onboarding.dataHandling());\n    // Auto-request token with credentials that was just set so they\n    // are not redirected to login after completion.\n    const { user, token } = await System.requestToken(data);\n    window.localStorage.setItem(AUTH_USER, JSON.stringify(user));\n    window.localStorage.setItem(AUTH_TOKEN, token);\n    window.localStorage.removeItem(AUTH_TIMESTAMP);\n  };\n\n  const setNewUsername = (e) => setUsername(e.target.value);\n  const setNewPassword = (e) => setPassword(e.target.value);\n  const handleUsernameChange = debounce(setNewUsername, 500);\n  const handlePasswordChange = debounce(setNewPassword, 500);\n\n  useEffect(() => {\n    // Enable button if there's any input, allowing users to attempt submission\n    // Validation errors will be shown via toast in handleSubmit\n    if (username.trim().length > 0 && password.length > 0) {\n      setMultiUserLoginValid(true);\n    } else {\n      setMultiUserLoginValid(false);\n    }\n  }, [username, password]);\n  return (\n    <div className=\"w-full flex items-center justify-center border max-w-[600px] rounded-lg border-white/20 light:border-theme-sidebar-border\">\n      <form onSubmit={handleSubmit}>\n        <div className=\"flex flex-col w-full md:px-8 px-2 py-4\">\n          <div className=\"space-y-6 flex h-full w-full\">\n            <div className=\"w-full flex flex-col gap-y-4\">\n              <div>\n                <label\n                  htmlFor=\"name\"\n                  className=\"block mb-3 text-sm font-medium text-white\"\n                >\n                  {t(\"onboarding.userSetup.adminUsername\")}\n                </label>\n                <input\n                  name=\"username\"\n                  type=\"text\"\n                  className=\"border-none bg-theme-settings-input-bg text-white text-sm rounded-lg block w-full p-2.5 focus:outline-primary-button active:outline-primary-button placeholder:text-theme-text-secondary outline-none\"\n                  placeholder=\"Your admin username\"\n                  minLength={USERNAME_MIN_LENGTH}\n                  maxLength={USERNAME_MAX_LENGTH}\n                  required={true}\n                  autoComplete=\"off\"\n                  onChange={handleUsernameChange}\n                />\n              </div>\n              <p className=\" text-white text-opacity-80 text-xs font-base\">\n                {t(\"common.username_requirements\")}\n              </p>\n              <div className=\"mt-4\">\n                <label\n                  htmlFor=\"name\"\n                  className=\"block mb-3 text-sm font-medium text-white\"\n                >\n                  {t(\"onboarding.userSetup.adminPassword\")}\n                </label>\n                <input\n                  name=\"password\"\n                  type=\"password\"\n                  className=\"border-none bg-theme-settings-input-bg text-white text-sm rounded-lg block w-full p-2.5 focus:outline-primary-button active:outline-primary-button placeholder:text-theme-text-secondary outline-none\"\n                  placeholder=\"Your admin password\"\n                  minLength={8}\n                  required={true}\n                  autoComplete=\"off\"\n                  onChange={handlePasswordChange}\n                />\n              </div>\n              <p className=\" text-white text-opacity-80 text-xs font-base\">\n                {t(\"onboarding.userSetup.adminPasswordReq\")}\n              </p>\n            </div>\n          </div>\n        </div>\n        <div className=\"flex w-full justify-between items-center px-6 py-4 space-x-6 border-t rounded-b border-theme-sidebar-border\">\n          <div className=\"text-theme-text-secondary text-opacity-80 text-xs font-base\">\n            {t(\"onboarding.userSetup.teamHint\")}\n          </div>\n        </div>\n        <button\n          type=\"submit\"\n          ref={myTeamSubmitRef}\n          hidden\n          aria-hidden=\"true\"\n        ></button>\n      </form>\n    </div>\n  );\n};\n"
  },
  {
    "path": "frontend/src/pages/OnboardingFlow/Steps/index.jsx",
    "content": "import { ArrowLeft, ArrowRight } from \"@phosphor-icons/react\";\nimport { useState } from \"react\";\nimport { isMobile } from \"react-device-detect\";\nimport useRedirectToHomeOnOnboardingComplete from \"@/hooks/useOnboardingComplete\";\nimport Home from \"./Home\";\nimport LLMPreference from \"./LLMPreference\";\nimport UserSetup from \"./UserSetup\";\nimport DataHandling from \"./DataHandling\";\nimport Survey from \"./Survey\";\n\nconst OnboardingSteps = {\n  home: Home,\n  \"llm-preference\": LLMPreference,\n  \"user-setup\": UserSetup,\n  \"data-handling\": DataHandling,\n  survey: Survey,\n};\n\nexport default OnboardingSteps;\n\nexport function OnboardingLayout({ children }) {\n  useRedirectToHomeOnOnboardingComplete();\n  const [header, setHeader] = useState({\n    title: \"\",\n    description: \"\",\n  });\n  const [backBtn, setBackBtn] = useState({\n    showing: false,\n    disabled: true,\n    onClick: () => null,\n  });\n  const [forwardBtn, setForwardBtn] = useState({\n    showing: false,\n    disabled: true,\n    onClick: () => null,\n  });\n\n  if (isMobile) {\n    return (\n      <div\n        data-layout=\"onboarding\"\n        className=\"w-screen h-screen overflow-y-auto bg-theme-bg-primary overflow-hidden\"\n      >\n        <div className=\"flex flex-col\">\n          <div className=\"w-full relative py-10 px-2\">\n            <div className=\"flex flex-col w-fit mx-auto gap-y-1 mb-[55px]\">\n              <h1 className=\"text-theme-text-primary font-semibold text-center text-2xl\">\n                {header.title}\n              </h1>\n              <p className=\"text-theme-text-secondary text-base text-center\">\n                {header.description}\n              </p>\n            </div>\n            {children(setHeader, setBackBtn, setForwardBtn)}\n          </div>\n          <div className=\"flex w-full justify-center gap-x-4 pb-20\">\n            <div className=\"flex justify-center items-center\">\n              {backBtn.showing && (\n                <button\n                  disabled={backBtn.disabled}\n                  onClick={backBtn.onClick}\n                  className=\"group p-2 rounded-lg border-2 border-zinc-300 disabled:border-zinc-600 h-fit w-fit disabled:not-allowed hover:bg-zinc-100 disabled:hover:bg-transparent\"\n                >\n                  <ArrowLeft\n                    className=\"text-white group-hover:text-black group-disabled:text-gray-500\"\n                    size={30}\n                  />\n                </button>\n              )}\n            </div>\n\n            <div className=\"flex justify-center items-center\">\n              {forwardBtn.showing && (\n                <button\n                  disabled={forwardBtn.disabled}\n                  onClick={forwardBtn.onClick}\n                  className=\"group p-2 rounded-lg border-2 border-zinc-300 disabled:border-zinc-600 h-fit w-fit disabled:not-allowed hover:bg-teal disabled:hover:bg-transparent\"\n                >\n                  <ArrowRight\n                    className=\"text-white group-hover:text-teal group-disabled:text-gray-500\"\n                    size={30}\n                  />\n                </button>\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      data-layout=\"onboarding\"\n      className=\"w-screen overflow-y-auto bg-theme-bg-primary flex justify-center overflow-hidden\"\n    >\n      <div className=\"flex w-1/5 h-screen justify-center items-center\">\n        {backBtn.showing && (\n          <button\n            disabled={backBtn.disabled}\n            onClick={backBtn.onClick}\n            className=\"group p-2 rounded-lg border-2 border-theme-sidebar-border h-fit w-fit disabled:cursor-not-allowed hover:bg-theme-bg-secondary disabled:hover:bg-transparent\"\n            aria-label=\"Back\"\n          >\n            <ArrowLeft\n              className=\"text-theme-text-secondary group-hover:text-theme-text-primary group-disabled:text-gray-500\"\n              size={30}\n            />\n          </button>\n        )}\n      </div>\n\n      <div className=\"w-full md:w-3/5 relative h-full py-10\">\n        <div className=\"flex flex-col w-fit mx-auto gap-y-1 mb-[55px]\">\n          <h1 className=\"text-theme-text-primary font-semibold text-center text-2xl\">\n            {header.title}\n          </h1>\n          <p className=\"text-theme-text-secondary text-base text-center\">\n            {header.description}\n          </p>\n        </div>\n        {children(setHeader, setBackBtn, setForwardBtn)}\n      </div>\n\n      <div className=\"flex w-1/5 h-screen justify-center items-center\">\n        {forwardBtn.showing && (\n          <button\n            disabled={forwardBtn.disabled}\n            onClick={forwardBtn.onClick}\n            className=\"group p-2 rounded-lg border-2 border-theme-sidebar-border h-fit w-fit disabled:cursor-not-allowed hover:bg-teal disabled:hover:bg-transparent\"\n            aria-label=\"Continue\"\n          >\n            <ArrowRight\n              className=\"text-theme-text-secondary group-hover:text-white group-disabled:text-gray-500\"\n              size={30}\n            />\n          </button>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/OnboardingFlow/index.jsx",
    "content": "import React from \"react\";\nimport OnboardingSteps, { OnboardingLayout } from \"./Steps\";\nimport { useParams } from \"react-router-dom\";\n\nexport default function OnboardingFlow() {\n  const { step } = useParams();\n  const StepPage = OnboardingSteps[step || \"home\"];\n  if (step === \"home\" || !step) return <StepPage />;\n\n  return (\n    <OnboardingLayout>\n      {(setHeader, setBackBtn, setForwardBtn) => (\n        <StepPage\n          setHeader={setHeader}\n          setBackBtn={setBackBtn}\n          setForwardBtn={setForwardBtn}\n        />\n      )}\n    </OnboardingLayout>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceChat/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { default as WorkspaceChatContainer } from \"@/components/WorkspaceChat\";\nimport Sidebar from \"@/components/Sidebar\";\nimport { useParams } from \"react-router-dom\";\nimport Workspace from \"@/models/workspace\";\nimport PasswordModal, { usePasswordModal } from \"@/components/Modals/Password\";\nimport { isMobile } from \"react-device-detect\";\nimport { FullScreenLoader } from \"@/components/Preloader\";\nimport { LAST_VISITED_WORKSPACE } from \"@/utils/constants\";\n\nexport default function WorkspaceChat() {\n  const { loading, requiresAuth, mode } = usePasswordModal();\n\n  if (loading) return <FullScreenLoader />;\n  if (requiresAuth !== false) {\n    return <>{requiresAuth !== null && <PasswordModal mode={mode} />}</>;\n  }\n\n  return <ShowWorkspaceChat />;\n}\n\nfunction ShowWorkspaceChat() {\n  const { slug } = useParams();\n  const [workspace, setWorkspace] = useState(null);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function getWorkspace() {\n      if (!slug) return;\n      const _workspace = await Workspace.bySlug(slug);\n      if (!_workspace) return setLoading(false);\n\n      const [suggestedMessages, { showAgentCommand }] = await Promise.all([\n        Workspace.getSuggestedMessages(slug),\n        Workspace.agentCommandAvailable(slug),\n      ]);\n      setWorkspace({\n        ..._workspace,\n        suggestedMessages,\n        showAgentCommand,\n      });\n      setLoading(false);\n      localStorage.setItem(\n        LAST_VISITED_WORKSPACE,\n        JSON.stringify({\n          slug: _workspace.slug,\n          name: _workspace.name,\n        })\n      );\n    }\n    getWorkspace();\n  }, []);\n\n  return (\n    <>\n      <div className=\"w-screen h-screen overflow-hidden bg-zinc-950 light:bg-slate-50 flex\">\n        {!isMobile && <Sidebar />}\n        <WorkspaceChatContainer loading={loading} workspace={workspace} />\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/AgentLLMItem/index.jsx",
    "content": "// This component differs from the main LLMItem in that it shows if a provider is\n// \"ready for use\" and if not - will then highjack the click handler to show a modal\n// of the provider options that must be saved to continue.\nimport { createPortal } from \"react-dom\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { useModal } from \"@/hooks/useModal\";\nimport { X, Gear } from \"@phosphor-icons/react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { useEffect, useState } from \"react\";\n\nconst NO_SETTINGS_NEEDED = [\"default\", \"none\"];\nexport default function AgentLLMItem({\n  llm,\n  availableLLMs,\n  settings,\n  checked,\n  onClick,\n}) {\n  const { isOpen, openModal, closeModal } = useModal();\n  const { name, value, logo, description } = llm;\n  const [currentSettings, setCurrentSettings] = useState(settings);\n\n  useEffect(() => {\n    async function getSettings() {\n      if (isOpen) {\n        const _settings = await System.keys();\n        setCurrentSettings(_settings ?? {});\n      }\n    }\n    getSettings();\n  }, [isOpen]);\n\n  function handleProviderSelection() {\n    // Determine if provider needs additional setup because its minimum required keys are\n    // not yet set in settings.\n    if (!checked) {\n      const requiresAdditionalSetup = (llm.requiredConfig || []).some(\n        (key) => !currentSettings[key]\n      );\n      if (requiresAdditionalSetup) {\n        openModal();\n        return;\n      }\n      onClick(value);\n    }\n  }\n\n  return (\n    <>\n      <div\n        onClick={handleProviderSelection}\n        className={`w-full p-2 rounded-md hover:cursor-pointer hover:bg-theme-bg-secondary ${\n          checked ? \"bg-theme-bg-secondary\" : \"\"\n        }`}\n      >\n        <input\n          type=\"checkbox\"\n          value={value}\n          className=\"peer hidden\"\n          checked={checked}\n          readOnly={true}\n          formNoValidate={true}\n        />\n        <div className=\"flex gap-x-4 items-center justify-between\">\n          <div className=\"flex gap-x-4 items-center\">\n            <img\n              src={logo}\n              alt={`${name} logo`}\n              className=\"w-10 h-10 rounded-md\"\n            />\n            <div className=\"flex flex-col\">\n              <div className=\"text-sm font-semibold text-white\">{name}</div>\n              <div className=\"mt-1 text-xs text-white/60\">{description}</div>\n            </div>\n          </div>\n          {checked &&\n            value !== \"none\" &&\n            !NO_SETTINGS_NEEDED.includes(value) && (\n              <button\n                onClick={(e) => {\n                  e.preventDefault();\n                  openModal();\n                }}\n                className=\"border-none p-2 text-white/60 hover:text-white hover:bg-theme-bg-hover rounded-md transition-all duration-300\"\n                title=\"Edit Settings\"\n              >\n                <Gear size={20} weight=\"bold\" />\n              </button>\n            )}\n        </div>\n      </div>\n      <SetupProvider\n        availableLLMs={availableLLMs}\n        isOpen={isOpen}\n        provider={value}\n        closeModal={closeModal}\n        postSubmit={onClick}\n        settings={currentSettings}\n      />\n    </>\n  );\n}\n\nfunction SetupProvider({\n  availableLLMs,\n  isOpen,\n  provider,\n  closeModal,\n  postSubmit,\n  settings,\n}) {\n  if (!isOpen) return null;\n  const LLMOption = availableLLMs.find((llm) => llm.value === provider);\n  if (!LLMOption) return null;\n\n  async function handleUpdate(e) {\n    e.preventDefault();\n    e.stopPropagation();\n    const data = {};\n    const form = new FormData(e.target);\n    for (var [key, value] of form.entries()) data[key] = value;\n    const { error } = await System.updateSystem(data);\n    if (error) {\n      showToast(`Failed to save ${LLMOption.name} settings: ${error}`, \"error\");\n      return;\n    }\n\n    closeModal();\n    postSubmit();\n    return false;\n  }\n\n  // Cannot do nested forms, it will cause all sorts of issues, so we portal this out\n  // to the parent container form so we don't have nested forms.\n  return createPortal(\n    <ModalWrapper isOpen={isOpen}>\n      <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n        <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n          <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n            <div className=\"w-full flex gap-x-2 items-center\">\n              <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n                {LLMOption.name} Settings\n              </h3>\n            </div>\n            <button\n              onClick={closeModal}\n              type=\"button\"\n              className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n            >\n              <X size={24} weight=\"bold\" className=\"text-white\" />\n            </button>\n          </div>\n          <form id=\"provider-form\" onSubmit={handleUpdate}>\n            <div className=\"px-7 py-6\">\n              <div className=\"space-y-6 max-h-[60vh] overflow-y-auto p-1\">\n                <p className=\"text-sm text-white/60\">\n                  To use {LLMOption.name} as this workspace's agent LLM you need\n                  to set it up first.\n                </p>\n                <div>\n                  {LLMOption.options(settings, { credentialsOnly: true })}\n                </div>\n              </div>\n            </div>\n            <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border px-7 pb-6\">\n              <button\n                type=\"button\"\n                onClick={closeModal}\n                className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n              >\n                Cancel\n              </button>\n              <button\n                type=\"submit\"\n                form=\"provider-form\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                Save {LLMOption.name} settings\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </ModalWrapper>,\n    document.getElementById(\"workspace-agent-settings-container\")\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/AgentConfig/AgentLLMSelection/index.jsx",
    "content": "import React, { useEffect, useRef, useState } from \"react\";\nimport AnythingLLMIcon from \"@/media/logo/anything-llm-icon.png\";\nimport AgentLLMItem from \"./AgentLLMItem\";\nimport { AVAILABLE_LLM_PROVIDERS } from \"@/pages/GeneralSettings/LLMPreference\";\nimport { CaretUpDown, Gauge, MagnifyingGlass, X } from \"@phosphor-icons/react\";\nimport AgentModelSelection from \"../AgentModelSelection\";\nimport { useTranslation } from \"react-i18next\";\n\nconst ENABLED_PROVIDERS = [\n  \"openai\",\n  \"anthropic\",\n  \"lmstudio\",\n  \"ollama\",\n  \"localai\",\n  \"groq\",\n  \"azure\",\n  \"koboldcpp\",\n  \"togetherai\",\n  \"openrouter\",\n  \"novita\",\n  \"mistral\",\n  \"perplexity\",\n  \"textgenwebui\",\n  \"generic-openai\",\n  \"bedrock\",\n  \"fireworksai\",\n  \"deepseek\",\n  \"ppio\",\n  \"litellm\",\n  \"apipie\",\n  \"xai\",\n  \"nvidia-nim\",\n  \"gemini\",\n  \"moonshotai\",\n  \"cometapi\",\n  \"foundry\",\n  \"zai\",\n  \"giteeai\",\n  \"cohere\",\n  \"docker-model-runner\",\n  \"privatemode\",\n  \"sambanova\",\n  \"lemonade\",\n  // TODO: More agent support.\n  // \"huggingface\"     // Can be done but already has issues with no-chat templated. Needs to be tested.\n];\nconst WARN_PERFORMANCE = [\n  \"lmstudio\",\n  \"koboldcpp\",\n  \"ollama\",\n  \"localai\",\n  \"textgenwebui\",\n  \"docker-model-runner\",\n];\n\nconst LLM_DEFAULT = {\n  name: \"System Default\",\n  value: \"none\",\n  logo: AnythingLLMIcon,\n  options: () => <React.Fragment />,\n  description:\n    \"Agents will use the workspace or system LLM unless otherwise specified.\",\n  requiredConfig: [],\n};\n\nconst LLMS = [\n  LLM_DEFAULT,\n  ...AVAILABLE_LLM_PROVIDERS.filter((llm) =>\n    ENABLED_PROVIDERS.includes(llm.value)\n  ),\n];\n\nexport default function AgentLLMSelection({\n  settings,\n  workspace,\n  setHasChanges,\n}) {\n  const [filteredLLMs, setFilteredLLMs] = useState([]);\n  const [selectedLLM, setSelectedLLM] = useState(\n    workspace?.agentProvider ?? \"none\"\n  );\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [searchMenuOpen, setSearchMenuOpen] = useState(false);\n  const searchInputRef = useRef(null);\n  const { t } = useTranslation();\n  function updateLLMChoice(selection) {\n    setSearchQuery(\"\");\n    setSelectedLLM(selection);\n    setSearchMenuOpen(false);\n    setHasChanges(true);\n  }\n\n  function handleXButton() {\n    if (searchQuery.length > 0) {\n      setSearchQuery(\"\");\n      if (searchInputRef.current) searchInputRef.current.value = \"\";\n    } else {\n      setSearchMenuOpen(!searchMenuOpen);\n    }\n  }\n\n  useEffect(() => {\n    const filtered = LLMS.filter((llm) =>\n      llm.name.toLowerCase().includes(searchQuery.toLowerCase())\n    );\n    setFilteredLLMs(filtered);\n  }, [searchQuery, selectedLLM]);\n\n  const selectedLLMObject = LLMS.find((llm) => llm.value === selectedLLM);\n  return (\n    <div className=\"border-b border-white/40 pb-8\">\n      {WARN_PERFORMANCE.includes(selectedLLM) && (\n        <div className=\"flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2\">\n          <div className=\"gap-x-2 flex items-center\">\n            <Gauge className=\"shrink-0\" size={25} />\n            <p className=\"text-sm\">{t(\"agent.performance-warning\")}</p>\n          </div>\n        </div>\n      )}\n\n      <div className=\"flex flex-col\">\n        <label htmlFor=\"name\" className=\"block input-label\">\n          {t(\"agent.provider.title\")}\n        </label>\n        <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n          {t(\"agent.provider.description\")}\n        </p>\n      </div>\n\n      <div className=\"relative\">\n        <input type=\"hidden\" name=\"agentProvider\" value={selectedLLM} />\n        {searchMenuOpen && (\n          <div\n            className=\"fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10\"\n            onClick={() => setSearchMenuOpen(false)}\n          />\n        )}\n        {searchMenuOpen ? (\n          <div className=\"absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] min-h-[64px] bg-theme-settings-input-bg rounded-lg flex flex-col justify-between cursor-pointer border-2 border-primary-button z-20\">\n            <div className=\"w-full flex flex-col gap-y-1\">\n              <div className=\"flex items-center sticky top-0 z-10 border-b border-[#9CA3AF] mx-4 bg-theme-settings-input-bg\">\n                <MagnifyingGlass\n                  size={20}\n                  weight=\"bold\"\n                  className=\"absolute left-4 z-30 text-theme-text-primary -ml-4 my-2\"\n                />\n                <input\n                  type=\"text\"\n                  name=\"llm-search\"\n                  autoComplete=\"off\"\n                  placeholder=\"Search available LLM providers\"\n                  className=\"border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none text-theme-text-primary placeholder:text-theme-text-primary placeholder:font-medium\"\n                  onChange={(e) => setSearchQuery(e.target.value)}\n                  ref={searchInputRef}\n                  onKeyDown={(e) => {\n                    if (e.key === \"Enter\") e.preventDefault();\n                  }}\n                />\n                <X\n                  size={20}\n                  weight=\"bold\"\n                  className=\"cursor-pointer text-theme-text-primary hover:text-x-button\"\n                  onClick={handleXButton}\n                />\n              </div>\n              <div className=\"flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4 max-h-[245px]\">\n                {filteredLLMs.map((llm) => {\n                  return (\n                    <AgentLLMItem\n                      llm={llm}\n                      key={llm.name}\n                      availableLLMs={LLMS}\n                      settings={settings}\n                      checked={selectedLLM === llm.value}\n                      onClick={() => updateLLMChoice(llm.value)}\n                    />\n                  );\n                })}\n              </div>\n            </div>\n          </div>\n        ) : (\n          <button\n            className=\"w-full max-w-[640px] h-[64px] bg-theme-settings-input-bg rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-primary-button transition-all duration-300\"\n            type=\"button\"\n            onClick={() => setSearchMenuOpen(true)}\n          >\n            <div className=\"flex gap-x-4 items-center\">\n              <img\n                src={selectedLLMObject.logo}\n                alt={`${selectedLLMObject.name} logo`}\n                className=\"w-10 h-10 rounded-md\"\n              />\n              <div className=\"flex flex-col text-left\">\n                <div className=\"text-sm font-semibold text-white\">\n                  {selectedLLMObject.name}\n                </div>\n                <div className=\"mt-1 text-xs text-description\">\n                  {selectedLLMObject.description}\n                </div>\n              </div>\n            </div>\n            <CaretUpDown size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        )}\n      </div>\n      {selectedLLM !== \"none\" && (\n        <div className=\"mt-4 flex flex-col gap-y-1\">\n          <AgentModelSelection\n            provider={selectedLLM}\n            workspace={workspace}\n            setHasChanges={setHasChanges}\n          />\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/AgentConfig/AgentModelSelection/index.jsx",
    "content": "import useGetProviderModels, {\n  DISABLED_PROVIDERS,\n} from \"@/hooks/useGetProvidersModels\";\nimport paths from \"@/utils/paths\";\nimport { useTranslation } from \"react-i18next\";\nimport { Link, useParams } from \"react-router-dom\";\n\n/**\n * These models do NOT support function calling\n * or do not support system prompts\n * and therefore are not supported for agents.\n * @param {string} provider - The AI provider.\n * @param {string} model - The model name.\n * @returns {boolean} Whether the model is supported for agents.\n */\nfunction supportedModel(provider, model = \"\") {\n  if (provider === \"openai\") {\n    return (\n      [\n        \"gpt-3.5-turbo-0301\",\n        \"gpt-4-turbo-2024-04-09\",\n        \"gpt-4-turbo\",\n        \"o1-preview\",\n        \"o1-preview-2024-09-12\",\n        \"o1-mini\",\n        \"o1-mini-2024-09-12\",\n        \"o3-mini\",\n        \"o3-mini-2025-01-31\",\n      ].includes(model) === false\n    );\n  }\n\n  return true;\n}\n\nexport default function AgentModelSelection({\n  provider,\n  workspace,\n  setHasChanges,\n}) {\n  const { slug } = useParams();\n  const { defaultModels, customModels, loading } =\n    useGetProviderModels(provider);\n\n  const { t } = useTranslation();\n  if (DISABLED_PROVIDERS.includes(provider)) {\n    return (\n      <div className=\"w-full h-10 justify-center items-center flex\">\n        <p className=\"text-sm font-base text-white text-opacity-60 text-center\">\n          Multi-model support is not supported for this provider yet.\n          <br />\n          Agent's will use{\" \"}\n          <Link\n            to={paths.workspace.settings.chatSettings(slug)}\n            className=\"underline\"\n          >\n            the model set for the workspace\n          </Link>{\" \"}\n          or{\" \"}\n          <Link to={paths.settings.llmPreference()} className=\"underline\">\n            the model set for the system.\n          </Link>\n        </p>\n      </div>\n    );\n  }\n\n  if (loading) {\n    return (\n      <div>\n        <div className=\"flex flex-col\">\n          <label htmlFor=\"name\" className=\"block input-label\">\n            {t(\"agent.mode.chat.title\")}\n          </label>\n          <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n            {t(\"agent.mode.chat.description\")}\n          </p>\n        </div>\n        <select\n          name=\"agentModel\"\n          required={true}\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            {t(\"agent.mode.wait\")}\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div>\n      <div className=\"flex flex-col\">\n        <label htmlFor=\"name\" className=\"block input-label\">\n          {t(\"agent.mode.title\")}\n        </label>\n        <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n          {t(\"agent.mode.description\")}\n        </p>\n      </div>\n\n      <select\n        name=\"agentModel\"\n        required={true}\n        onChange={() => {\n          setHasChanges(true);\n        }}\n        className=\"border-none bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n      >\n        {defaultModels.length > 0 && (\n          <optgroup label=\"General models\">\n            {defaultModels.map((model) => {\n              if (!supportedModel(provider, model)) return null;\n              return (\n                <option\n                  key={model}\n                  value={model}\n                  selected={workspace?.agentModel === model}\n                >\n                  {model}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n        {Array.isArray(customModels) && customModels.length > 0 && (\n          <optgroup label=\"Custom models\">\n            {customModels.map((model) => {\n              if (!supportedModel(provider, model.id)) return null;\n\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={workspace?.agentModel === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n        {/* For providers like TogetherAi where we partition model by creator entity. */}\n        {!Array.isArray(customModels) &&\n          Object.keys(customModels).length > 0 && (\n            <>\n              {Object.entries(customModels).map(([organization, models]) => (\n                <optgroup key={organization} label={organization}>\n                  {models.map((model) => {\n                    if (!supportedModel(provider, model.id)) return null;\n                    return (\n                      <option\n                        key={model.id}\n                        value={model.id}\n                        selected={workspace?.agentModel === model.id}\n                      >\n                        {model.name}\n                      </option>\n                    );\n                  })}\n                </optgroup>\n              ))}\n            </>\n          )}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/AgentConfig/index.jsx",
    "content": "import System from \"@/models/system\";\nimport Workspace from \"@/models/workspace\";\nimport showToast from \"@/utils/toast\";\nimport { castToType } from \"@/utils/types\";\nimport { useEffect, useRef, useState } from \"react\";\nimport AgentLLMSelection from \"./AgentLLMSelection\";\nimport Admin from \"@/models/admin\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\nimport paths from \"@/utils/paths\";\nimport useUser from \"@/hooks/useUser\";\n\nexport default function WorkspaceAgentConfiguration({ workspace }) {\n  const { user } = useUser();\n  const [settings, setSettings] = useState({});\n  const [hasChanges, setHasChanges] = useState(false);\n  const [saving, setSaving] = useState(false);\n  const [loading, setLoading] = useState(true);\n  const formEl = useRef(null);\n\n  useEffect(() => {\n    async function fetchSettings() {\n      const _settings = await System.keys();\n      setSettings(_settings ?? {});\n      setLoading(false);\n    }\n    fetchSettings();\n  }, []);\n\n  const handleUpdate = async (e) => {\n    setSaving(true);\n    e.preventDefault();\n    const data = {\n      workspace: {},\n      system: {},\n      env: {},\n    };\n\n    const form = new FormData(formEl.current);\n    for (var [key, value] of form.entries()) {\n      if (key.startsWith(\"system::\")) {\n        const [_, label] = key.split(\"system::\");\n        data.system[label] = String(value);\n        continue;\n      }\n\n      if (key.startsWith(\"env::\")) {\n        const [_, label] = key.split(\"env::\");\n        data.env[label] = String(value);\n        continue;\n      }\n\n      data.workspace[key] = castToType(key, value);\n    }\n\n    const { workspace: updatedWorkspace, message } = await Workspace.update(\n      workspace.slug,\n      data.workspace\n    );\n    await Admin.updateSystemPreferences(data.system);\n    await System.updateSystem(data.env);\n\n    if (!!updatedWorkspace) {\n      showToast(\"Workspace updated!\", \"success\", { clear: true });\n    } else {\n      showToast(`Error: ${message}`, \"error\", { clear: true });\n    }\n\n    setSaving(false);\n    setHasChanges(false);\n  };\n\n  if (!workspace || loading) return <LoadingSkeleton />;\n  return (\n    <div id=\"workspace-agent-settings-container\">\n      <form\n        ref={formEl}\n        onSubmit={handleUpdate}\n        onChange={() => setHasChanges(true)}\n        id=\"agent-settings-form\"\n        className=\"w-1/2 flex flex-col gap-y-6\"\n      >\n        <AgentLLMSelection\n          settings={settings}\n          workspace={workspace}\n          setHasChanges={setHasChanges}\n        />\n        {(!user || user?.role === \"admin\") && (\n          <>\n            {!hasChanges && (\n              <div className=\"flex flex-col gap-y-4\">\n                <a\n                  className=\"w-fit transition-all duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800\"\n                  href={paths.settings.agentSkills()}\n                >\n                  Configure Agent Skills\n                </a>\n                <p className=\"text-white text-opacity-60 text-xs font-medium\">\n                  Customize and enhance the default agent's capabilities by\n                  enabling or disabling specific skills. These settings will be\n                  applied across all workspaces.\n                </p>\n              </div>\n            )}\n          </>\n        )}\n\n        {hasChanges && (\n          <button\n            type=\"submit\"\n            form=\"agent-settings-form\"\n            className=\"w-fit transition-all duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800\"\n          >\n            {saving ? \"Updating agent...\" : \"Update workspace agent\"}\n          </button>\n        )}\n      </form>\n    </div>\n  );\n}\n\nfunction LoadingSkeleton() {\n  return (\n    <div id=\"workspace-agent-settings-container\">\n      <div className=\"w-1/2 flex flex-col gap-y-6\">\n        <Skeleton.default\n          height={100}\n          width=\"100%\"\n          count={2}\n          highlightColor=\"var(--theme-bg-primary)\"\n          baseColor=\"var(--theme-bg-secondary)\"\n          enableAnimation={true}\n          containerClassName=\"flex flex-col gap-y-1\"\n        />\n        <div className=\"bg-white/10 h-[1px] w-full\" />\n        <Skeleton.default\n          height={100}\n          width=\"100%\"\n          count={2}\n          highlightColor=\"var(--theme-bg-primary)\"\n          baseColor=\"var(--theme-bg-secondary)\"\n          enableAnimation={true}\n          containerClassName=\"flex flex-col gap-y-1 mt-4\"\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/ChatSettings/ChatHistorySettings/index.jsx",
    "content": "import { useTranslation } from \"react-i18next\";\nexport default function ChatHistorySettings({ workspace, setHasChanges }) {\n  const { t } = useTranslation();\n  return (\n    <div>\n      <div className=\"flex flex-col gap-y-1 mb-4\">\n        <label htmlFor=\"name\" className=\"block mb-2 input-label\">\n          {t(\"chat.history.title\")}\n        </label>\n        <p className=\"text-white text-opacity-60 text-xs font-medium\">\n          {t(\"chat.history.desc-start\")}\n          <i> {t(\"chat.history.recommend\")} </i>\n          {t(\"chat.history.desc-end\")}\n        </p>\n      </div>\n      <input\n        name=\"openAiHistory\"\n        type=\"number\"\n        min={1}\n        max={45}\n        step={1}\n        onWheel={(e) => e.target.blur()}\n        defaultValue={workspace?.openAiHistory ?? 20}\n        className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n        placeholder=\"20\"\n        required={true}\n        autoComplete=\"off\"\n        onChange={() => setHasChanges(true)}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/ChatSettings/ChatModeSelection/index.jsx",
    "content": "import { useState } from \"react\";\nimport { Trans, useTranslation } from \"react-i18next\";\n\nexport default function ChatModeSelection({ workspace, setHasChanges }) {\n  const { t } = useTranslation();\n  const [chatMode, setChatMode] = useState(workspace?.chatMode || \"chat\");\n\n  return (\n    <div>\n      <div className=\"flex flex-col\">\n        <label htmlFor=\"chatMode\" className=\"block input-label\">\n          {t(\"chat.mode.title\")}\n        </label>\n      </div>\n\n      <div className=\"flex flex-col gap-y-1 mt-2\">\n        <div className=\"w-fit flex gap-x-1 items-center p-1 rounded-lg bg-theme-settings-input-bg \">\n          <input type=\"hidden\" name=\"chatMode\" value={chatMode} />\n          <button\n            type=\"button\"\n            disabled={chatMode === \"automatic\"}\n            onClick={() => {\n              setChatMode(\"automatic\");\n              setHasChanges(true);\n            }}\n            className=\"border-none transition-bg duration-200 px-6 py-1 text-md text-white/60 disabled:text-white bg-transparent disabled:bg-[#687280] rounded-md hover:bg-white/10\"\n          >\n            {t(\"chat.mode.automatic.title\")}\n          </button>\n          <button\n            type=\"button\"\n            disabled={chatMode === \"chat\"}\n            onClick={() => {\n              setChatMode(\"chat\");\n              setHasChanges(true);\n            }}\n            className=\"border-none transition-bg duration-200 px-6 py-1 text-md text-white/60 disabled:text-white bg-transparent disabled:bg-[#687280] rounded-md hover:bg-white/10 light:hover:bg-black/10\"\n          >\n            {t(\"chat.mode.chat.title\")}\n          </button>\n          <button\n            type=\"button\"\n            disabled={chatMode === \"query\"}\n            onClick={() => {\n              setChatMode(\"query\");\n              setHasChanges(true);\n            }}\n            className=\"border-none transition-bg duration-200 px-6 py-1 text-md text-white/60 disabled:text-white bg-transparent disabled:bg-[#687280] rounded-md hover:bg-white/10 light:hover:bg-black/10\"\n          >\n            {t(\"chat.mode.query.title\")}\n          </button>\n        </div>\n        <ChatModeExplanation chatMode={chatMode} />\n      </div>\n    </div>\n  );\n}\n\n/**\n * A component that displays the explanation for a given chat mode.\n * @param {'automatic' | 'chat' | 'query'} chatMode - The chat mode to display the explanation for.\n * @returns {JSX.Element} The component to display the explanation for the given chat mode.\n */\nfunction ChatModeExplanation({ chatMode = \"chat\" }) {\n  const { t } = useTranslation();\n  return (\n    <p className=\"text-sm text-white/60\">\n      <b>{t(`chat.mode.${chatMode}.title`)}</b>{\" \"}\n      <Trans\n        i18nKey={`chat.mode.${chatMode}.description`}\n        components={{ b: <b />, br: <br /> }}\n      />\n    </p>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/ChatSettings/ChatPromptSettings/ChatPromptHistory/PromptHistoryItem/index.jsx",
    "content": "import { DotsThreeVertical } from \"@phosphor-icons/react\";\nimport { useRef, useState, useEffect } from \"react\";\nimport PromptHistory from \"@/models/promptHistory\";\nimport { useTranslation } from \"react-i18next\";\nimport moment from \"moment\";\nimport truncate from \"truncate\";\n\nconst MAX_PROMPT_LENGTH = 200; // chars\n\nexport default function PromptHistoryItem({\n  id,\n  prompt,\n  modifiedAt,\n  user,\n  onRestore,\n  setHistory,\n  onPublishClick,\n}) {\n  const { t } = useTranslation();\n  const [showMenu, setShowMenu] = useState(false);\n  const menuRef = useRef(null);\n  const menuButtonRef = useRef(null);\n  const [expanded, setExpanded] = useState(false);\n\n  const deleteHistory = async (id) => {\n    if (window.confirm(t(\"chat.prompt.history.deleteConfirm\"))) {\n      const { success } = await PromptHistory.delete(id);\n      if (success) {\n        setHistory((prevHistory) =>\n          prevHistory.filter((item) => item.id !== id)\n        );\n      }\n    }\n  };\n\n  useEffect(() => {\n    const handleClickOutside = (event) => {\n      if (\n        showMenu &&\n        !menuRef.current.contains(event.target) &&\n        !menuButtonRef.current.contains(event.target)\n      ) {\n        setShowMenu(false);\n      }\n    };\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, [showMenu]);\n\n  return (\n    <div className=\"text-white\">\n      <div className=\"flex items-center justify-between\">\n        <div className=\"text-xs\">\n          {user && (\n            <>\n              <span className=\"text-primary-button\">{user.username}</span>{\" \"}\n              <span className=\"mx-1 text-white\">•</span>\n            </>\n          )}\n          <span className=\"text-white opacity-50 light:opacity-100\">\n            {moment(modifiedAt).fromNow()}\n          </span>\n        </div>\n        <div className=\"flex items-center gap-2\">\n          <button\n            type=\"button\"\n            className=\"border-none text-sm cursor-pointer text-theme-text-primary hover:text-primary-button\"\n            onClick={onRestore}\n          >\n            {t(\"chat.prompt.history.restore\")}\n          </button>\n          <div className=\"relative\">\n            <button\n              type=\"button\"\n              ref={menuButtonRef}\n              className=\"border-none text-theme-text-secondary cursor-pointer hover:text-primary-button flex items-center justify-center\"\n              onClick={() => setShowMenu(!showMenu)}\n            >\n              <DotsThreeVertical size={16} weight=\"bold\" />\n            </button>\n            {showMenu && (\n              <div\n                ref={menuRef}\n                className=\"absolute right-0 top-6 bg-theme-bg-popup-menu rounded-lg z-50 min-w-[200px]\"\n              >\n                <button\n                  type=\"button\"\n                  className=\"px-[10px] py-[6px] text-sm text-white hover:bg-theme-sidebar-item-hover rounded-t-lg cursor-pointer border-none w-full text-left whitespace-nowrap\"\n                  onClick={() => {\n                    setShowMenu(false);\n                    onPublishClick(prompt);\n                  }}\n                >\n                  {t(\"chat.prompt.history.publish\")}\n                </button>\n                <button\n                  type=\"button\"\n                  className=\"px-[10px] py-[6px] text-sm text-white hover:bg-red-500/60 light:hover:bg-red-300/80 rounded-b-lg cursor-pointer border-none w-full text-left whitespace-nowrap\"\n                  onClick={() => {\n                    setShowMenu(false);\n                    deleteHistory(id);\n                  }}\n                >\n                  {t(\"chat.prompt.history.delete\")}\n                </button>\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n      <div className=\"flex items-center mt-1\">\n        <div className=\"text-theme-text-primary text-sm font-medium break-all whitespace-pre-wrap\">\n          {prompt.length > MAX_PROMPT_LENGTH && !expanded ? (\n            <>\n              {truncate(prompt, MAX_PROMPT_LENGTH)}{\" \"}\n              <button\n                type=\"button\"\n                className=\"text-theme-text-secondary hover:text-primary-button border-none\"\n                onClick={() => setExpanded(!expanded)}\n              >\n                {t(\"chat.prompt.history.expand\")}\n              </button>\n            </>\n          ) : (\n            prompt\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/ChatSettings/ChatPromptSettings/ChatPromptHistory/index.jsx",
    "content": "import { useEffect, useState, forwardRef } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { X } from \"@phosphor-icons/react\";\nimport PromptHistory from \"@/models/promptHistory\";\nimport PromptHistoryItem from \"./PromptHistoryItem\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport \"react-loading-skeleton/dist/skeleton.css\";\n\nexport default forwardRef(function ChatPromptHistory(\n  { show, workspaceSlug, onRestore, onClose, onPublishClick },\n  ref\n) {\n  const { t } = useTranslation();\n  const [history, setHistory] = useState([]);\n  const [loading, setLoading] = useState(true);\n\n  function loadHistory() {\n    if (!workspaceSlug) return;\n    setLoading(true);\n    PromptHistory.forWorkspace(workspaceSlug)\n      .then((historyData) => {\n        setHistory(historyData);\n      })\n      .catch((error) => {\n        console.error(error);\n      })\n      .finally(() => {\n        setLoading(false);\n      });\n  }\n\n  function handleClearAll() {\n    if (!workspaceSlug) return;\n    if (window.confirm(t(\"chat.prompt.history.clearAllConfirm\"))) {\n      PromptHistory.clearAll(workspaceSlug)\n        .then(({ success }) => {\n          if (success) setHistory([]);\n        })\n        .catch((error) => {\n          console.error(error);\n        });\n    }\n  }\n\n  useEffect(() => {\n    if (show && workspaceSlug) loadHistory();\n  }, [show, workspaceSlug]);\n\n  return (\n    <div\n      ref={ref}\n      className={`fixed right-3 top-3 bottom-3 w-[375px] bg-theme-action-menu-bg light:bg-theme-home-update-card-bg rounded-xl py-4 px-4 z-[9999] overflow-y-hidden ${\n        show\n          ? \"translate-x-0 opacity-100 visible\"\n          : \"translate-x-full opacity-0 invisible\"\n      } transition-all duration-300`}\n    >\n      <div className=\"sticky flex items-center justify-between\">\n        <div className=\"text-theme-text-primary text-sm font-semibold\">\n          {t(\"chat.prompt.history.title\")}\n        </div>\n        <div className=\"flex items-center gap-2\">\n          {history.length > 0 && (\n            <button\n              type=\"button\"\n              className=\"text-sm font-medium text-theme-text-secondary cursor-pointer hover:text-primary-button border-none\"\n              onClick={handleClearAll}\n            >\n              {t(\"chat.prompt.history.clearAll\")}\n            </button>\n          )}\n          <button\n            type=\"button\"\n            className=\"text-theme-text-secondary cursor-pointer hover:text-primary-button border-none\"\n            onClick={onClose}\n          >\n            <X size={16} weight=\"bold\" />\n          </button>\n        </div>\n      </div>\n      <div className=\"mt-4 flex flex-col gap-y-[14px] h-full overflow-y-scroll pb-[50px]\">\n        {loading ? (\n          <LoaderSkeleton />\n        ) : history.length === 0 ? (\n          <div className=\"flex text-theme-text-secondary text-sm text-center w-full h-full flex items-center justify-center\">\n            {t(\"chat.prompt.history.noHistory\")}\n          </div>\n        ) : (\n          history.map((item) => (\n            <PromptHistoryItem\n              key={item.id}\n              id={item.id}\n              {...item}\n              onRestore={() => onRestore(item.prompt)}\n              onPublishClick={onPublishClick}\n              setHistory={setHistory}\n            />\n          ))\n        )}\n      </div>\n    </div>\n  );\n});\n\nfunction LoaderSkeleton() {\n  const highlightColor = \"var(--theme-bg-primary)\";\n  const baseColor = \"var(--theme-bg-secondary)\";\n  return (\n    <Skeleton.default\n      height=\"85px\"\n      width=\"100%\"\n      highlightColor={highlightColor}\n      baseColor={baseColor}\n      count={8}\n    />\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/ChatSettings/ChatPromptSettings/index.jsx",
    "content": "import { useEffect, useState, useRef, Fragment } from \"react\";\nimport { getWorkspaceSystemPrompt } from \"@/utils/chat\";\nimport { useTranslation } from \"react-i18next\";\nimport SystemPromptVariable from \"@/models/systemPromptVariable\";\nimport Highlighter from \"react-highlight-words\";\nimport { Link, useSearchParams } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\nimport ChatPromptHistory from \"./ChatPromptHistory\";\nimport PublishEntityModal from \"@/components/CommunityHub/PublishEntityModal\";\nimport { useModal } from \"@/hooks/useModal\";\nimport System from \"@/models/system\";\n\nexport default function ChatPromptSettings({\n  workspace,\n  setHasChanges,\n  hasChanges,\n}) {\n  const { t } = useTranslation();\n  const [searchParams] = useSearchParams();\n\n  // Prompt state\n  const initialPrompt = getWorkspaceSystemPrompt(workspace);\n  const [prompt, setPrompt] = useState(initialPrompt);\n  const [savedPrompt, setSavedPrompt] = useState(initialPrompt);\n  const [defaultSystemPrompt, setDefaultSystemPrompt] = useState(\"\");\n\n  // UI state\n  const [isEditing, setIsEditing] = useState(false);\n  const [showPromptHistory, setShowPromptHistory] = useState(false);\n  const [availableVariables, setAvailableVariables] = useState([]);\n\n  // Refs\n  const promptRef = useRef(null);\n  const promptHistoryRef = useRef(null);\n  const historyButtonRef = useRef(null);\n\n  // Modals\n  const {\n    isOpen: showPublishModal,\n    closeModal: closePublishModal,\n    openModal: openPublishModal,\n  } = useModal();\n\n  // Derived state\n  const isDirty = prompt !== savedPrompt;\n  const hasBeenModified = savedPrompt?.trim() !== initialPrompt?.trim();\n  const showPublishButton =\n    !isEditing && prompt?.trim().length >= 10 && (isDirty || hasBeenModified);\n\n  // Load variables and handle focus on mount\n  useEffect(() => {\n    async function setupVariableHighlighting() {\n      const { variables } = await SystemPromptVariable.getAll();\n      setAvailableVariables(variables);\n    }\n    setupVariableHighlighting();\n    if (searchParams.get(\"action\") === \"focus-system-prompt\")\n      setIsEditing(true);\n  }, [searchParams]);\n\n  // Update saved prompt when parent clears hasChanges\n  useEffect(() => {\n    if (!hasChanges) setSavedPrompt(prompt);\n  }, [hasChanges, prompt]);\n\n  // Auto-focus textarea when editing\n  useEffect(() => {\n    if (isEditing && promptRef.current) {\n      promptRef.current.focus();\n    }\n  }, [isEditing]);\n\n  useEffect(() => {\n    System.fetchDefaultSystemPrompt().then(({ defaultSystemPrompt }) =>\n      setDefaultSystemPrompt(defaultSystemPrompt)\n    );\n  }, []);\n\n  // Handle click outside for history panel\n  useEffect(() => {\n    const handleClickOutside = (event) => {\n      if (\n        promptHistoryRef.current &&\n        !promptHistoryRef.current.contains(event.target) &&\n        historyButtonRef.current &&\n        !historyButtonRef.current.contains(event.target)\n      ) {\n        setShowPromptHistory(false);\n      }\n    };\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n  }, []);\n\n  const handleRestoreFromHistory = (historicalPrompt) => {\n    setPrompt(historicalPrompt);\n    setShowPromptHistory(false);\n    setHasChanges(true);\n  };\n\n  const handlePublishFromHistory = (historicalPrompt) => {\n    openPublishModal();\n    setShowPromptHistory(false);\n    setTimeout(() => setPrompt(historicalPrompt), 0);\n  };\n\n  // Restore to default system prompt, if no default system prompt is set\n  const handleRestoreToDefaultSystemPrompt = () => {\n    System.fetchDefaultSystemPrompt().then(({ defaultSystemPrompt }) => {\n      setPrompt(defaultSystemPrompt);\n      setHasChanges(true);\n    });\n  };\n\n  return (\n    <>\n      <ChatPromptHistory\n        ref={promptHistoryRef}\n        workspaceSlug={workspace.slug}\n        show={showPromptHistory}\n        onRestore={handleRestoreFromHistory}\n        onPublishClick={handlePublishFromHistory}\n        onClose={() => setShowPromptHistory(false)}\n      />\n      <div>\n        <div className=\"flex flex-col\">\n          <div className=\"flex items-center justify-between\">\n            <label htmlFor=\"name\" className=\"block input-label\">\n              {t(\"chat.prompt.title\")}\n            </label>\n          </div>\n          <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n            {t(\"chat.prompt.description\")}\n          </p>\n          <p className=\"text-white text-opacity-60 text-xs font-medium mb-2\">\n            You can insert{\" \"}\n            <Link\n              to={paths.settings.systemPromptVariables()}\n              className=\"text-primary-button\"\n            >\n              prompt variables\n            </Link>{\" \"}\n            like:{\" \"}\n            {availableVariables.slice(0, 3).map((v, i) => (\n              <Fragment key={v.key}>\n                <span className=\"bg-theme-settings-input-bg px-1 py-0.5 rounded\">\n                  {`{${v.key}}`}\n                </span>\n                {i < availableVariables.length - 1 && \", \"}\n              </Fragment>\n            ))}\n            {availableVariables.length > 3 && (\n              <Link\n                to={paths.settings.systemPromptVariables()}\n                className=\"text-primary-button\"\n              >\n                +{availableVariables.length - 3} more...\n              </Link>\n            )}\n          </p>\n        </div>\n\n        <input type=\"hidden\" name=\"openAiPrompt\" value={prompt} />\n        <div className=\"relative w-full flex flex-col items-end\">\n          <button\n            ref={historyButtonRef}\n            type=\"button\"\n            className=\"text-theme-text-secondary hover:text-white light:hover:text-black text-xs font-medium\"\n            onClick={(e) => {\n              e.preventDefault();\n              setShowPromptHistory(!showPromptHistory);\n            }}\n          >\n            {showPromptHistory ? \"Hide History\" : \"View History\"}\n          </button>\n          <div className=\"relative w-full\">\n            {isEditing ? (\n              <textarea\n                ref={promptRef}\n                autoFocus={true}\n                rows={5}\n                onFocus={(e) => {\n                  const length = e.target.value.length;\n                  e.target.setSelectionRange(length, length);\n                }}\n                onBlur={(e) => {\n                  setIsEditing(false);\n                  setPrompt(e.target.value);\n                }}\n                onChange={(e) => {\n                  setPrompt(e.target.value);\n                  setHasChanges(true);\n                }}\n                onPaste={(e) => {\n                  setPrompt(e.target.value);\n                  setHasChanges(true);\n                }}\n                style={{\n                  resize: \"vertical\",\n                  overflowY: \"scroll\",\n                  minHeight: \"150px\",\n                }}\n                defaultValue={prompt}\n                className=\"border-none bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 mt-2\"\n              />\n            ) : (\n              <div\n                onClick={() => setIsEditing(true)}\n                style={{\n                  resize: \"vertical\",\n                  overflowY: \"scroll\",\n                  minHeight: \"150px\",\n                }}\n                className=\"border-none bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 mt-2\"\n              >\n                <Highlighter\n                  className=\"whitespace-pre-wrap\"\n                  highlightClassName=\"bg-cta-button p-0.5 rounded-md\"\n                  searchWords={availableVariables.map((v) => `{${v.key}}`)}\n                  autoEscape={true}\n                  caseSensitive={true}\n                  textToHighlight={prompt}\n                />\n              </div>\n            )}\n          </div>\n          <div className=\"w-full flex flex-row items-center justify-between pt-2\">\n            {prompt !== defaultSystemPrompt && (\n              <button\n                type=\"button\"\n                onClick={handleRestoreToDefaultSystemPrompt}\n                className=\"text-theme-text-primary hover:text-white light:hover:text-black text-xs font-medium\"\n              >\n                Restore to Default\n              </button>\n            )}\n            <PublishPromptCTA\n              hidden={!showPublishButton}\n              onClick={openPublishModal}\n            />\n          </div>\n        </div>\n      </div>\n      <PublishEntityModal\n        show={showPublishModal}\n        onClose={closePublishModal}\n        entityType=\"system-prompt\"\n        entity={prompt}\n      />\n    </>\n  );\n}\n\nfunction PublishPromptCTA({ hidden = false, onClick }) {\n  if (hidden) return null;\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      className=\"border-none text-primary-button hover:text-white light:hover:text-black text-xs font-medium\"\n    >\n      Publish to Community Hub\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/ChatSettings/ChatQueryRefusalResponse/index.jsx",
    "content": "import { chatQueryRefusalResponse } from \"@/utils/chat\";\nimport { useTranslation } from \"react-i18next\";\nexport default function ChatQueryRefusalResponse({ workspace, setHasChanges }) {\n  const { t } = useTranslation();\n  return (\n    <div>\n      <div className=\"flex flex-col\">\n        <label htmlFor=\"name\" className=\"block input-label\">\n          {t(\"chat.refusal.title\")}\n        </label>\n        <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n          {t(\"chat.refusal.desc-start\")}{\" \"}\n          <code className=\"border-none bg-theme-settings-input-bg p-0.5 rounded-sm\">\n            {t(\"chat.refusal.query\")}\n          </code>{\" \"}\n          {t(\"chat.refusal.desc-end\")}\n        </p>\n      </div>\n      <textarea\n        name=\"queryRefusalResponse\"\n        rows={2}\n        defaultValue={chatQueryRefusalResponse(workspace)}\n        className=\"border-none bg-theme-settings-input-bg placeholder:text-theme-settings-input-placeholder text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 mt-2\"\n        placeholder=\"The text returned in query mode when there is no relevant context found for a response.\"\n        required={true}\n        wrap=\"soft\"\n        autoComplete=\"off\"\n        onChange={() => setHasChanges(true)}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/ChatSettings/ChatTemperatureSettings/index.jsx",
    "content": "import { useTranslation } from \"react-i18next\";\nfunction recommendedSettings(provider = null) {\n  switch (provider) {\n    case \"mistral\":\n      return { temp: 0 };\n    default:\n      return { temp: 0.7 };\n  }\n}\n\nexport default function ChatTemperatureSettings({\n  settings,\n  workspace,\n  setHasChanges,\n}) {\n  const defaults = recommendedSettings(settings?.LLMProvider);\n  const { t } = useTranslation();\n  return (\n    <div>\n      <div className=\"flex flex-col\">\n        <label htmlFor=\"name\" className=\"block input-label\">\n          {t(\"chat.temperature.title\")}\n        </label>\n        <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n          {t(\"chat.temperature.desc-start\")}\n          <br />\n          {t(\"chat.temperature.desc-end\")}\n          <br />\n          <br />\n          <i>{t(\"chat.temperature.hint\")}</i>\n        </p>\n      </div>\n      <input\n        name=\"openAiTemp\"\n        type=\"number\"\n        min={0.0}\n        step={0.1}\n        onWheel={(e) => e.target.blur()}\n        defaultValue={workspace?.openAiTemp ?? defaults.temp}\n        className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n        placeholder=\"0.7\"\n        required={true}\n        autoComplete=\"off\"\n        onChange={() => setHasChanges(true)}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/ChatSettings/WorkspaceLLMSelection/ChatModelSelection/index.jsx",
    "content": "import useGetProviderModels, {\n  DISABLED_PROVIDERS,\n} from \"@/hooks/useGetProvidersModels\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function ChatModelSelection({\n  provider,\n  workspace,\n  setHasChanges,\n}) {\n  const { defaultModels, customModels, loading } =\n    useGetProviderModels(provider);\n  const { t } = useTranslation();\n  if (DISABLED_PROVIDERS.includes(provider)) return null;\n\n  if (loading) {\n    return (\n      <div>\n        <div className=\"flex flex-col mt-6\">\n          <label htmlFor=\"name\" className=\"block input-label\">\n            {t(\"chat.model.title\")}\n          </label>\n          <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n            {t(\"chat.model.description\")}\n          </p>\n        </div>\n        <select\n          name=\"chatModel\"\n          required={true}\n          disabled={true}\n          className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n        >\n          <option disabled={true} selected={true}>\n            -- waiting for models --\n          </option>\n        </select>\n      </div>\n    );\n  }\n\n  return (\n    <div>\n      <div className=\"flex flex-col mt-6\">\n        <label htmlFor=\"name\" className=\"block input-label\">\n          {t(\"chat.model.title\")}\n        </label>\n        <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n          {t(\"chat.model.description\")}\n        </p>\n      </div>\n\n      <select\n        name=\"chatModel\"\n        required={true}\n        onChange={() => {\n          setHasChanges(true);\n        }}\n        className=\"border-none bg-theme-settings-input-bg text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n      >\n        {defaultModels.length > 0 && (\n          <optgroup label=\"General models\">\n            {defaultModels.map((model) => {\n              return (\n                <option\n                  key={model}\n                  value={model}\n                  selected={workspace?.chatModel === model}\n                >\n                  {model}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n        {Array.isArray(customModels) && customModels.length > 0 && (\n          <optgroup label=\"Discovered models\">\n            {customModels.map((model) => {\n              return (\n                <option\n                  key={model.id}\n                  value={model.id}\n                  selected={workspace?.chatModel === model.id}\n                >\n                  {model.id}\n                </option>\n              );\n            })}\n          </optgroup>\n        )}\n        {/* For providers like TogetherAi where we partition model by creator entity. */}\n        {!Array.isArray(customModels) &&\n          Object.keys(customModels).length > 0 && (\n            <>\n              {Object.entries(customModels).map(([organization, models]) => (\n                <optgroup key={organization} label={organization}>\n                  {models.map((model) => (\n                    <option\n                      key={model.id}\n                      value={model.id}\n                      selected={workspace?.chatModel === model.id}\n                    >\n                      {model.name}\n                    </option>\n                  ))}\n                </optgroup>\n              ))}\n            </>\n          )}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/ChatSettings/WorkspaceLLMSelection/WorkspaceLLMItem/index.jsx",
    "content": "// This component differs from the main LLMItem in that it shows if a provider is\n// \"ready for use\" and if not - will then highjack the click handler to show a modal\n// of the provider options that must be saved to continue.\nimport { createPortal } from \"react-dom\";\nimport ModalWrapper from \"@/components/ModalWrapper\";\nimport { useModal } from \"@/hooks/useModal\";\nimport { X, Gear } from \"@phosphor-icons/react\";\nimport System from \"@/models/system\";\nimport showToast from \"@/utils/toast\";\nimport { useEffect, useState } from \"react\";\n\nconst NO_SETTINGS_NEEDED = [\"default\"];\nexport default function WorkspaceLLM({\n  llm,\n  availableLLMs,\n  settings,\n  checked,\n  onClick,\n}) {\n  const { isOpen, openModal, closeModal } = useModal();\n  const { name, value, logo, description } = llm;\n  const [currentSettings, setCurrentSettings] = useState(settings);\n\n  useEffect(() => {\n    async function getSettings() {\n      if (isOpen) {\n        const _settings = await System.keys();\n        setCurrentSettings(_settings ?? {});\n      }\n    }\n    getSettings();\n  }, [isOpen]);\n\n  function handleProviderSelection() {\n    // Determine if provider needs additional setup because its minimum required keys are\n    // not yet set in settings.\n    if (!checked) {\n      const requiresAdditionalSetup = (llm.requiredConfig || []).some(\n        (key) => !currentSettings[key]\n      );\n      if (requiresAdditionalSetup) {\n        openModal();\n        return;\n      }\n      onClick(value);\n    }\n  }\n\n  return (\n    <>\n      <div\n        onClick={handleProviderSelection}\n        className={`w-full p-2 rounded-md hover:cursor-pointer hover:bg-theme-bg-secondary ${\n          checked ? \"bg-theme-bg-secondary\" : \"\"\n        }`}\n      >\n        <input\n          type=\"checkbox\"\n          value={value}\n          className=\"peer hidden\"\n          checked={checked}\n          readOnly={true}\n          formNoValidate={true}\n        />\n        <div className=\"flex gap-x-4 items-center justify-between\">\n          <div className=\"flex gap-x-4 items-center\">\n            <img\n              src={logo}\n              alt={`${name} logo`}\n              className=\"w-10 h-10 rounded-md\"\n            />\n            <div className=\"flex flex-col\">\n              <div className=\"text-sm font-semibold text-white\">{name}</div>\n              <div className=\"mt-1 text-xs text-white/60\">{description}</div>\n            </div>\n          </div>\n          {checked && !NO_SETTINGS_NEEDED.includes(value) && (\n            <button\n              onClick={(e) => {\n                e.preventDefault();\n                openModal();\n              }}\n              className=\"p-2 text-white/60 hover:text-white hover:bg-theme-bg-hover rounded-md transition-all duration-300\"\n              title=\"Edit Settings\"\n            >\n              <Gear size={20} weight=\"bold\" />\n            </button>\n          )}\n        </div>\n      </div>\n      <SetupProvider\n        availableLLMs={availableLLMs}\n        isOpen={isOpen}\n        provider={value}\n        closeModal={closeModal}\n        postSubmit={onClick}\n        settings={currentSettings}\n      />\n    </>\n  );\n}\n\nfunction SetupProvider({\n  availableLLMs,\n  isOpen,\n  provider,\n  closeModal,\n  postSubmit,\n  settings,\n}) {\n  if (!isOpen) return null;\n  const LLMOption = availableLLMs.find((llm) => llm.value === provider);\n  if (!LLMOption) return null;\n\n  async function handleUpdate(e) {\n    e.preventDefault();\n    e.stopPropagation();\n    const data = {};\n    const form = new FormData(e.target);\n    for (var [key, value] of form.entries()) data[key] = value;\n    const { error } = await System.updateSystem(data);\n    if (error) {\n      showToast(`Failed to save ${LLMOption.name} settings: ${error}`, \"error\");\n      return;\n    }\n\n    closeModal();\n    postSubmit();\n    return false;\n  }\n\n  // Cannot do nested forms, it will cause all sorts of issues, so we portal this out\n  // to the parent container form so we don't have nested forms.\n  return createPortal(\n    <ModalWrapper isOpen={isOpen}>\n      <div className=\"fixed inset-0 z-50 overflow-auto bg-black bg-opacity-50 flex items-center justify-center\">\n        <div className=\"relative w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border\">\n          <div className=\"relative p-6 border-b rounded-t border-theme-modal-border\">\n            <div className=\"w-full flex gap-x-2 items-center\">\n              <h3 className=\"text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap\">\n                {LLMOption.name} Settings\n              </h3>\n            </div>\n            <button\n              onClick={closeModal}\n              type=\"button\"\n              className=\"absolute top-4 right-4 transition-all duration-300 bg-transparent rounded-lg text-sm p-1 inline-flex items-center hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n            >\n              <X size={24} weight=\"bold\" className=\"text-white\" />\n            </button>\n          </div>\n          <form id=\"provider-form\" onSubmit={handleUpdate}>\n            <div className=\"px-7 py-6\">\n              <div className=\"space-y-6 max-h-[60vh] overflow-y-auto p-1\">\n                <p className=\"text-sm text-white/60\">\n                  To use {LLMOption.name} as this workspace's LLM you need to\n                  set it up first.\n                </p>\n                <div>\n                  {LLMOption.options(settings, { credentialsOnly: true })}\n                </div>\n              </div>\n            </div>\n            <div className=\"flex justify-between items-center mt-6 pt-6 border-t border-theme-modal-border px-7 pb-6\">\n              <button\n                type=\"button\"\n                onClick={closeModal}\n                className=\"transition-all duration-300 text-white hover:bg-zinc-700 px-4 py-2 rounded-lg text-sm\"\n              >\n                Cancel\n              </button>\n              <button\n                type=\"submit\"\n                form=\"provider-form\"\n                className=\"transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm\"\n              >\n                Save settings\n              </button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </ModalWrapper>,\n    document.getElementById(\"workspace-chat-settings-container\")\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/ChatSettings/WorkspaceLLMSelection/index.jsx",
    "content": "import React, { useEffect, useRef, useState } from \"react\";\nimport AnythingLLMIcon from \"@/media/logo/anything-llm-icon.png\";\nimport WorkspaceLLMItem from \"./WorkspaceLLMItem\";\nimport { AVAILABLE_LLM_PROVIDERS } from \"@/pages/GeneralSettings/LLMPreference\";\nimport { CaretUpDown, MagnifyingGlass, X } from \"@phosphor-icons/react\";\nimport ChatModelSelection from \"./ChatModelSelection\";\nimport { useTranslation } from \"react-i18next\";\nimport { Link } from \"react-router-dom\";\nimport paths from \"@/utils/paths\";\n\n// Some providers do not support model selection via /models.\n// In that case we allow the user to enter the model name manually and hope they\n// type it correctly.\nconst FREE_FORM_LLM_SELECTION = [\"bedrock\", \"azure\", \"generic-openai\"];\n\n// Some providers do not support model selection via /models\n// and only have a fixed single-model they can use.\nconst NO_MODEL_SELECTION = [\"default\", \"huggingface\"];\n\n// Some providers we just fully disable for ease of use.\nconst DISABLED_PROVIDERS = [];\n\nconst LLM_DEFAULT = {\n  name: \"System default\",\n  value: \"default\",\n  logo: AnythingLLMIcon,\n  options: () => <React.Fragment />,\n  description: \"Use the system LLM preference for this workspace.\",\n  requiredConfig: [],\n};\n\nconst LLMS = [LLM_DEFAULT, ...AVAILABLE_LLM_PROVIDERS].filter(\n  (llm) => !DISABLED_PROVIDERS.includes(llm.value)\n);\n\nexport default function WorkspaceLLMSelection({\n  settings,\n  workspace,\n  setHasChanges,\n}) {\n  const [filteredLLMs, setFilteredLLMs] = useState([]);\n  const [selectedLLM, setSelectedLLM] = useState(\n    workspace?.chatProvider ?? \"default\"\n  );\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [searchMenuOpen, setSearchMenuOpen] = useState(false);\n  const searchInputRef = useRef(null);\n  const { t } = useTranslation();\n  function updateLLMChoice(selection) {\n    setSearchQuery(\"\");\n    setSelectedLLM(selection);\n    setSearchMenuOpen(false);\n    setHasChanges(true);\n  }\n\n  function handleXButton() {\n    if (searchQuery.length > 0) {\n      setSearchQuery(\"\");\n      if (searchInputRef.current) searchInputRef.current.value = \"\";\n    } else {\n      setSearchMenuOpen(!searchMenuOpen);\n    }\n  }\n\n  useEffect(() => {\n    const filtered = LLMS.filter((llm) =>\n      llm.name.toLowerCase().includes(searchQuery.toLowerCase())\n    );\n    setFilteredLLMs(filtered);\n  }, [LLMS, searchQuery, selectedLLM]);\n  const selectedLLMObject = LLMS.find((llm) => llm.value === selectedLLM);\n\n  return (\n    <div className=\"border-b border-white/40 pb-8\">\n      <div className=\"flex flex-col\">\n        <label htmlFor=\"name\" className=\"block input-label\">\n          {t(\"chat.llm.title\")}\n        </label>\n        <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n          {t(\"chat.llm.description\")}\n        </p>\n      </div>\n\n      <div className=\"relative\">\n        <input type=\"hidden\" name=\"chatProvider\" value={selectedLLM} />\n        {searchMenuOpen && (\n          <div\n            className=\"fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10\"\n            onClick={() => setSearchMenuOpen(false)}\n          />\n        )}\n        {searchMenuOpen ? (\n          <div className=\"absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] min-h-[64px] bg-theme-settings-input-bg rounded-lg flex flex-col justify-between cursor-pointer border-2 border-primary-button z-20\">\n            <div className=\"w-full flex flex-col gap-y-1\">\n              <div className=\"flex items-center sticky top-0 z-10 border-b border-[#9CA3AF] mx-4 bg-theme-settings-input-bg\">\n                <MagnifyingGlass\n                  size={20}\n                  weight=\"bold\"\n                  className=\"absolute left-4 z-30 text-theme-text-primary -ml-4 my-2\"\n                />\n                <input\n                  type=\"text\"\n                  name=\"llm-search\"\n                  autoComplete=\"off\"\n                  placeholder={t(\"chat.llm.search\")}\n                  className=\"border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:outline-primary-button active:outline-primary-button outline-none text-theme-text-primary placeholder:text-theme-text-primary placeholder:font-medium\"\n                  onChange={(e) => setSearchQuery(e.target.value)}\n                  ref={searchInputRef}\n                  onKeyDown={(e) => {\n                    if (e.key === \"Enter\") e.preventDefault();\n                  }}\n                />\n                <X\n                  size={20}\n                  weight=\"bold\"\n                  className=\"cursor-pointer text-theme-text-primary hover:text-x-button\"\n                  onClick={handleXButton}\n                />\n              </div>\n              <div className=\"flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4 max-h-[245px]\">\n                {filteredLLMs.map((llm) => {\n                  return (\n                    <WorkspaceLLMItem\n                      llm={llm}\n                      key={llm.name}\n                      availableLLMs={LLMS}\n                      settings={settings}\n                      checked={selectedLLM === llm.value}\n                      onClick={() => updateLLMChoice(llm.value)}\n                    />\n                  );\n                })}\n              </div>\n            </div>\n          </div>\n        ) : (\n          <button\n            className=\"w-full max-w-[640px] h-[64px] bg-theme-settings-input-bg rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-primary-button transition-all duration-300\"\n            type=\"button\"\n            onClick={() => setSearchMenuOpen(true)}\n          >\n            <div className=\"flex gap-x-4 items-center\">\n              <img\n                src={selectedLLMObject.logo}\n                alt={`${selectedLLMObject.name} logo`}\n                className=\"w-10 h-10 rounded-md\"\n              />\n              <div className=\"flex flex-col text-left\">\n                <div className=\"text-sm font-semibold text-white\">\n                  {selectedLLMObject.name}\n                </div>\n                <div className=\"mt-1 text-xs text-description\">\n                  {selectedLLMObject.description}\n                </div>\n              </div>\n            </div>\n            <CaretUpDown size={24} weight=\"bold\" className=\"text-white\" />\n          </button>\n        )}\n      </div>\n      <ModelSelector\n        selectedLLM={selectedLLM}\n        workspace={workspace}\n        setHasChanges={setHasChanges}\n      />\n    </div>\n  );\n}\n\n// TODO: Add this to agent selector as well as make generic component.\nfunction ModelSelector({ selectedLLM, workspace, setHasChanges }) {\n  if (NO_MODEL_SELECTION.includes(selectedLLM)) {\n    if (selectedLLM !== \"default\") {\n      return (\n        <div className=\"w-full h-10 justify-center items-center flex mt-4\">\n          <p className=\"text-sm font-base text-white text-opacity-60 text-center\">\n            Multi-model support is not supported for this provider yet.\n            <br />\n            This workspace will use{\" \"}\n            <Link to={paths.settings.llmPreference()} className=\"underline\">\n              the model set for the system.\n            </Link>\n          </p>\n        </div>\n      );\n    }\n    return null;\n  }\n\n  if (FREE_FORM_LLM_SELECTION.includes(selectedLLM)) {\n    return (\n      <FreeFormLLMInput workspace={workspace} setHasChanges={setHasChanges} />\n    );\n  }\n\n  return (\n    <ChatModelSelection\n      provider={selectedLLM}\n      workspace={workspace}\n      setHasChanges={setHasChanges}\n    />\n  );\n}\n\nfunction FreeFormLLMInput({ workspace, setHasChanges }) {\n  const { t } = useTranslation();\n  return (\n    <div className=\"mt-4 flex flex-col gap-y-1\">\n      <label className=\"block input-label\">{t(\"chat.model.title\")}</label>\n      <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n        {t(\"chat.model.description\")}\n      </p>\n      <input\n        type=\"text\"\n        name=\"chatModel\"\n        defaultValue={workspace?.chatModel || \"\"}\n        onChange={() => setHasChanges(true)}\n        className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n        placeholder=\"Enter model name exactly as referenced in the API (e.g., gpt-3.5-turbo)\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/ChatSettings/index.jsx",
    "content": "import System from \"@/models/system\";\nimport Workspace from \"@/models/workspace\";\nimport showToast from \"@/utils/toast\";\nimport { castToType } from \"@/utils/types\";\nimport { useEffect, useRef, useState } from \"react\";\nimport ChatHistorySettings from \"./ChatHistorySettings\";\nimport ChatPromptSettings from \"./ChatPromptSettings\";\nimport ChatTemperatureSettings from \"./ChatTemperatureSettings\";\nimport ChatModeSelection from \"./ChatModeSelection\";\nimport WorkspaceLLMSelection from \"./WorkspaceLLMSelection\";\nimport ChatQueryRefusalResponse from \"./ChatQueryRefusalResponse\";\nimport CTAButton from \"@/components/lib/CTAButton\";\n\nexport default function ChatSettings({ workspace }) {\n  const [settings, setSettings] = useState({});\n  const [hasChanges, setHasChanges] = useState(false);\n  const [saving, setSaving] = useState(false);\n\n  const formEl = useRef(null);\n  useEffect(() => {\n    async function fetchSettings() {\n      const _settings = await System.keys();\n      setSettings(_settings ?? {});\n    }\n    fetchSettings();\n  }, []);\n\n  const handleUpdate = async (e) => {\n    e.preventDefault();\n    setSaving(true);\n    const data = {};\n    const form = new FormData(formEl.current);\n    for (var [key, value] of form.entries()) data[key] = castToType(key, value);\n\n    const { workspace: updatedWorkspace, message } = await Workspace.update(\n      workspace.slug,\n      data\n    );\n    if (updatedWorkspace) {\n      showToast(\"Workspace updated!\", \"success\", { clear: true });\n      setHasChanges(false);\n    } else {\n      showToast(`Error: ${message}`, \"error\", { clear: true });\n      // Keep hasChanges true on error so user can retry\n    }\n    setSaving(false);\n  };\n\n  if (!workspace) return null;\n  return (\n    <div id=\"workspace-chat-settings-container\" className=\"relative\">\n      <form\n        ref={formEl}\n        onSubmit={handleUpdate}\n        id=\"chat-settings-form\"\n        className=\"w-1/2 flex flex-col gap-y-6\"\n      >\n        {hasChanges && (\n          <div className=\"absolute top-0 right-0\">\n            <CTAButton type=\"submit\">\n              {saving ? \"Updating...\" : \"Update Workspace\"}\n            </CTAButton>\n          </div>\n        )}\n        <WorkspaceLLMSelection\n          settings={settings}\n          workspace={workspace}\n          setHasChanges={setHasChanges}\n        />\n        <ChatModeSelection\n          workspace={workspace}\n          setHasChanges={setHasChanges}\n        />\n        <ChatHistorySettings\n          workspace={workspace}\n          setHasChanges={setHasChanges}\n        />\n        <ChatPromptSettings\n          workspace={workspace}\n          setHasChanges={setHasChanges}\n          hasChanges={hasChanges}\n        />\n        <ChatQueryRefusalResponse\n          workspace={workspace}\n          setHasChanges={setHasChanges}\n        />\n        <ChatTemperatureSettings\n          settings={settings}\n          workspace={workspace}\n          setHasChanges={setHasChanges}\n        />\n      </form>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/GeneralAppearance/DeleteWorkspace/index.jsx",
    "content": "import { useState } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport Workspace from \"@/models/workspace\";\nimport paths from \"@/utils/paths\";\nimport { useTranslation } from \"react-i18next\";\nimport showToast from \"@/utils/toast\";\n\nexport default function DeleteWorkspace({ workspace }) {\n  const { slug } = useParams();\n  const [deleting, setDeleting] = useState(false);\n  const { t } = useTranslation();\n\n  const deleteWorkspace = async () => {\n    if (\n      !window.confirm(\n        `${t(\"general.delete.confirm-start\")} ${workspace.name} ${t(\n          \"general.delete.confirm-end\"\n        )}`\n      )\n    )\n      return false;\n\n    setDeleting(true);\n    const success = await Workspace.delete(workspace.slug);\n    if (!success) {\n      showToast(\"Workspace could not be deleted!\", \"error\", { clear: true });\n      setDeleting(false);\n      return;\n    }\n\n    workspace.slug === slug\n      ? (window.location = paths.home())\n      : window.location.reload();\n  };\n  return (\n    <div className=\"flex flex-col mt-10\">\n      <label className=\"block input-label\">{t(\"general.delete.title\")}</label>\n      <p className=\"text-theme-text-secondary text-xs font-medium py-1.5\">\n        {t(\"general.delete.description\")}\n      </p>\n      <button\n        disabled={deleting}\n        onClick={deleteWorkspace}\n        type=\"button\"\n        className=\"w-60 mt-4 transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-red-500/25 text-red-200 light:text-red-500 hover:light:text-[#FFFFFF] hover:text-[#FFFFFF] hover:bg-red-600 disabled:bg-red-600 disabled:text-red-200 disabled:animate-pulse\"\n      >\n        {deleting ? t(\"general.delete.deleting\") : t(\"general.delete.delete\")}\n      </button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/GeneralAppearance/SuggestedChatMessages/index.jsx",
    "content": "import PreLoader from \"@/components/Preloader\";\nimport Workspace from \"@/models/workspace\";\nimport showToast from \"@/utils/toast\";\nimport { useEffect, useState } from \"react\";\nimport { Plus, X } from \"@phosphor-icons/react\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function SuggestedChatMessages({ slug }) {\n  const [suggestedMessages, setSuggestedMessages] = useState([]);\n  const [editingIndex, setEditingIndex] = useState(-1);\n  const [newMessage, setNewMessage] = useState({ heading: \"\", message: \"\" });\n  const [hasChanges, setHasChanges] = useState(false);\n  const [loading, setLoading] = useState(true);\n  const { t } = useTranslation();\n  useEffect(() => {\n    async function fetchWorkspace() {\n      if (!slug) return;\n      const suggestedMessages = await Workspace.getSuggestedMessages(slug);\n      setSuggestedMessages(suggestedMessages);\n      setLoading(false);\n    }\n    fetchWorkspace();\n  }, [slug]);\n\n  const handleSaveSuggestedMessages = async () => {\n    const validMessages = suggestedMessages.filter(\n      (msg) => msg?.message?.trim()?.length > 0\n    );\n    const { success, error } = await Workspace.setSuggestedMessages(\n      slug,\n      validMessages\n    );\n    if (!success) {\n      showToast(`Failed to update suggested chat messages: ${error}`, \"error\");\n      return;\n    }\n    setSuggestedMessages(validMessages);\n    setEditingIndex(-1);\n    setHasChanges(false);\n  };\n\n  const addMessage = () => {\n    setEditingIndex(-1);\n    if (suggestedMessages.length >= 4) {\n      showToast(\"Maximum of 4 messages allowed.\", \"warning\");\n      return;\n    }\n    const defaultMessage = {\n      heading: \"\",\n      message: `${t(\"general.message.heading\")} ${t(\"general.message.body\")}`,\n    };\n    setNewMessage(defaultMessage);\n    setSuggestedMessages([...suggestedMessages, { ...defaultMessage }]);\n    setHasChanges(true);\n  };\n\n  const removeMessage = (index) => {\n    const messages = [...suggestedMessages];\n    messages.splice(index, 1);\n    setSuggestedMessages(messages);\n    setHasChanges(true);\n  };\n\n  const startEditing = (e, index) => {\n    e.preventDefault();\n    setEditingIndex(index);\n    const suggestion = suggestedMessages[index];\n    // Legacy messages may have a separate heading field. Merge it into the message\n    // on edit so the user can manage everything in a single input going forward.\n    if (suggestion.heading) {\n      const merged = {\n        heading: \"\",\n        message: `${suggestion.heading} ${suggestion.message}`,\n      };\n      setNewMessage(merged);\n      setSuggestedMessages(\n        suggestedMessages.map((msg, i) => (i === index ? merged : msg))\n      );\n    } else {\n      setNewMessage({ ...suggestion });\n    }\n  };\n\n  const handleRemoveMessage = (index) => {\n    removeMessage(index);\n    setEditingIndex(-1);\n  };\n\n  const onEditChange = (e) => {\n    const updatedNewMessage = {\n      ...newMessage,\n      [e.target.name]: e.target.value,\n    };\n    setNewMessage(updatedNewMessage);\n    const updatedMessages = suggestedMessages.map((message, index) => {\n      if (index === editingIndex) {\n        return { ...message, [e.target.name]: e.target.value };\n      }\n      return message;\n    });\n\n    setSuggestedMessages(updatedMessages);\n    setHasChanges(true);\n  };\n\n  if (loading)\n    return (\n      <div className=\"flex flex-col\">\n        <label className=\"block input-label\">\n          {t(\"general.message.title\")}\n        </label>\n        <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n          {t(\"general.message.description\")}\n        </p>\n        <div className=\"text-white text-opacity-60 text-sm font-medium mt-6\">\n          <PreLoader size=\"4\" />\n        </div>\n      </div>\n    );\n  return (\n    <div className=\"w-full mt-6\">\n      <div className=\"flex flex-col\">\n        <label className=\"block input-label\">\n          {t(\"general.message.title\")}\n        </label>\n        <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n          {t(\"general.message.description\")}\n        </p>\n      </div>\n\n      <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6 text-white/60 text-xs mt-2 w-full justify-center max-w-[600px]\">\n        {suggestedMessages.map((suggestion, index) => (\n          <div key={index} className=\"relative w-full\">\n            <button\n              className=\"transition-all duration-300 absolute z-10 text-neutral-700 bg-white rounded-full hover:bg-zinc-600 hover:border-zinc-600 hover:text-white border-transparent border shadow-lg ml-2\"\n              style={{\n                top: -8,\n                left: 265,\n              }}\n              onClick={() => handleRemoveMessage(index)}\n            >\n              <X className=\"m-[1px]\" size={20} />\n            </button>\n            <button\n              key={index}\n              onClick={(e) => startEditing(e, index)}\n              className={`text-left p-2.5 border rounded-xl w-full border-white/20 bg-theme-settings-input-bg hover:bg-theme-sidebar-item-selected-gradient ${\n                editingIndex === index ? \"border-sky-400\" : \"\"\n              }`}\n            >\n              <p className=\"line-clamp-2 text-theme-text-primary\">\n                {suggestion?.heading ? `${suggestion.heading} ` : \"\"}\n                {suggestion?.message ?? \"\"}\n              </p>\n            </button>\n          </div>\n        ))}\n      </div>\n      {editingIndex >= 0 && (\n        <div className=\"flex flex-col gap-y-4 mr-2 mt-8\">\n          <div className=\"w-1/2\">\n            <label className=\"text-white text-sm font-semibold block mb-2\">\n              Message\n            </label>\n            <input\n              placeholder=\"Message\"\n              className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block p-2.5 w-full\"\n              value={newMessage.message}\n              name=\"message\"\n              onChange={onEditChange}\n            />\n          </div>\n        </div>\n      )}\n      {suggestedMessages.length < 4 && (\n        <button\n          type=\"button\"\n          onClick={addMessage}\n          className=\"flex gap-x-2 items-center justify-center mt-6 text-white text-sm hover:text-sky-400 transition-all duration-300\"\n        >\n          {t(\"general.message.add\")}{\" \"}\n          <Plus className=\"\" size={24} weight=\"fill\" />\n        </button>\n      )}\n\n      {hasChanges && (\n        <div className=\"flex justify-start py-6\">\n          <button\n            type=\"button\"\n            className=\"transition-all duration-300 border border-slate-200 px-4 py-2 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800\"\n            onClick={handleSaveSuggestedMessages}\n          >\n            {t(\"general.message.save\")}\n          </button>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/GeneralAppearance/WorkspaceName/index.jsx",
    "content": "import { useTranslation } from \"react-i18next\";\n\nexport default function WorkspaceName({ workspace, setHasChanges }) {\n  const { t } = useTranslation();\n  return (\n    <div>\n      <div className=\"flex flex-col\">\n        <label htmlFor=\"name\" className=\"block input-label\">\n          {t(\"common.workspaces-name\")}\n        </label>\n        <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n          {t(\"general.names.description\")}\n        </p>\n      </div>\n      <input\n        name=\"name\"\n        type=\"text\"\n        minLength={2}\n        maxLength={80}\n        defaultValue={workspace?.name}\n        className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n        placeholder=\"My Workspace\"\n        required={true}\n        autoComplete=\"off\"\n        onChange={() => setHasChanges(true)}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/GeneralAppearance/index.jsx",
    "content": "import Workspace from \"@/models/workspace\";\nimport { castToType } from \"@/utils/types\";\nimport showToast from \"@/utils/toast\";\nimport { useEffect, useRef, useState } from \"react\";\nimport WorkspaceName from \"./WorkspaceName\";\nimport SuggestedChatMessages from \"./SuggestedChatMessages\";\nimport DeleteWorkspace from \"./DeleteWorkspace\";\nimport CTAButton from \"@/components/lib/CTAButton\";\n\nexport default function GeneralInfo({ slug }) {\n  const [workspace, setWorkspace] = useState(null);\n  const [hasChanges, setHasChanges] = useState(false);\n  const [saving, setSaving] = useState(false);\n  const [loading, setLoading] = useState(true);\n  const formEl = useRef(null);\n\n  useEffect(() => {\n    async function fetchWorkspace() {\n      const workspace = await Workspace.bySlug(slug);\n      setWorkspace(workspace);\n      setLoading(false);\n    }\n    fetchWorkspace();\n  }, [slug]);\n\n  const handleUpdate = async (e) => {\n    setSaving(true);\n    e.preventDefault();\n    const data = {};\n    const form = new FormData(formEl.current);\n    for (var [key, value] of form.entries()) data[key] = castToType(key, value);\n    const { workspace: updatedWorkspace, message } = await Workspace.update(\n      workspace.slug,\n      data\n    );\n    if (!!updatedWorkspace) {\n      showToast(\"Workspace updated!\", \"success\", { clear: true });\n    } else {\n      showToast(`Error: ${message}`, \"error\", { clear: true });\n    }\n    setSaving(false);\n    setHasChanges(false);\n  };\n\n  if (!workspace || loading) return null;\n  return (\n    <div className=\"w-full relative\">\n      <form\n        ref={formEl}\n        onSubmit={handleUpdate}\n        className=\"w-1/2 flex flex-col gap-y-6\"\n      >\n        {hasChanges && (\n          <div className=\"absolute top-0 right-0\">\n            <CTAButton type=\"submit\">\n              {saving ? \"Updating...\" : \"Update Workspace\"}\n            </CTAButton>\n          </div>\n        )}\n        <WorkspaceName\n          key={workspace.slug}\n          workspace={workspace}\n          setHasChanges={setHasChanges}\n        />\n      </form>\n      <SuggestedChatMessages slug={workspace.slug} />\n      <DeleteWorkspace workspace={workspace} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/Members/AddMemberModal/index.jsx",
    "content": "import React, { useState } from \"react\";\nimport { MagnifyingGlass, X } from \"@phosphor-icons/react\";\nimport Admin from \"@/models/admin\";\nimport showToast from \"@/utils/toast\";\n\nexport default function AddMemberModal({ closeModal, workspace, users }) {\n  const [searchTerm, setSearchTerm] = useState(\"\");\n  const [selectedUsers, setSelectedUsers] = useState(workspace?.userIds || []);\n\n  const handleUpdate = async (e) => {\n    e.preventDefault();\n    const { success, error } = await Admin.updateUsersInWorkspace(\n      workspace.id,\n      selectedUsers\n    );\n    if (success) {\n      showToast(\"Users updated successfully.\", \"success\");\n      setTimeout(() => {\n        window.location.reload();\n      }, 1000);\n    }\n    showToast(error, \"error\");\n  };\n\n  const handleUserSelect = (userId) => {\n    setSelectedUsers((prevSelectedUsers) => {\n      if (prevSelectedUsers.includes(userId)) {\n        return prevSelectedUsers.filter((id) => id !== userId);\n      } else {\n        return [...prevSelectedUsers, userId];\n      }\n    });\n  };\n\n  const handleSelectAll = () => {\n    if (selectedUsers.length === filteredUsers.length) {\n      setSelectedUsers([]);\n    } else {\n      setSelectedUsers(filteredUsers.map((user) => user.id));\n    }\n  };\n\n  const handleUnselect = () => {\n    setSelectedUsers([]);\n  };\n\n  const isUserSelected = (userId) => {\n    return selectedUsers.includes(userId);\n  };\n\n  const handleSearch = (event) => {\n    setSearchTerm(event.target.value);\n  };\n\n  const filteredUsers = users\n    .filter((user) =>\n      user.username.toLowerCase().includes(searchTerm.toLowerCase())\n    )\n    .filter((user) => user.role !== \"admin\")\n    .filter((user) => user.role !== \"manager\");\n\n  return (\n    <div className=\"relative w-full max-w-[550px] max-h-full\">\n      <div className=\"w-full max-w-2xl bg-theme-bg-secondary rounded-lg shadow border-2 border-theme-modal-border overflow-hidden\">\n        <div className=\"flex items-center justify-between p-6 border-b rounded-t border-theme-modal-border\">\n          <div className=\"flex items-center gap-x-4\">\n            <h3 className=\"text-base font-semibold text-white\">Users</h3>\n            <div className=\"relative\">\n              <input\n                onChange={handleSearch}\n                className=\"w-[400px] h-[34px] bg-theme-bg-primary rounded-[100px] text-white placeholder:text-theme-text-secondary text-sm px-10 pl-10\"\n                placeholder=\"Search for a user\"\n              />\n              <MagnifyingGlass\n                size={16}\n                weight=\"bold\"\n                className=\"text-white text-lg absolute left-3 top-1/2 transform -translate-y-1/2\"\n              />\n            </div>\n          </div>\n          <button\n            onClick={closeModal}\n            type=\"button\"\n            className=\"border-none bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-theme-modal-border hover:border-theme-modal-border hover:border-opacity-50 border-transparent border\"\n          >\n            <X className=\"text-white text-lg\" />\n          </button>\n        </div>\n        <form onSubmit={handleUpdate}>\n          <div className=\"py-[17px] px-[20px]\">\n            <table className=\"gap-y-[8px] flex flex-col max-h-[385px] overflow-y-auto no-scroll\">\n              {filteredUsers.length > 0 ? (\n                filteredUsers.map((user) => (\n                  <tr\n                    key={user.id}\n                    className=\"flex items-center gap-x-2 cursor-pointer\"\n                    onClick={() => handleUserSelect(user.id)}\n                  >\n                    <div\n                      className=\"shrink-0 w-3 h-3 rounded border-[1px] border-solid border-white light:border-black flex justify-center items-center\"\n                      role=\"checkbox\"\n                      aria-checked={isUserSelected(user.id)}\n                      tabIndex={0}\n                    >\n                      {isUserSelected(user.id) && (\n                        <div className=\"w-2 h-2 bg-white light:bg-black rounded-[2px]\" />\n                      )}\n                    </div>\n                    <p className=\"text-theme-text-primary text-sm font-medium\">\n                      {user.username}\n                    </p>\n                  </tr>\n                ))\n              ) : (\n                <p className=\"text-theme-text-secondary text-sm font-medium \">\n                  No users found\n                </p>\n              )}\n            </table>\n          </div>\n          <div className=\"flex w-full justify-between items-center p-3 space-x-2 border-t rounded-b border-gray-500/50\">\n            <div className=\"flex items-center gap-x-2\">\n              <button\n                type=\"button\"\n                onClick={handleSelectAll}\n                className=\"flex items-center gap-x-2 ml-2\"\n              >\n                <div\n                  className=\"shrink-0 w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer\"\n                  role=\"checkbox\"\n                  aria-checked={selectedUsers.length === filteredUsers.length}\n                  tabIndex={0}\n                >\n                  {selectedUsers.length === filteredUsers.length && (\n                    <div className=\"w-2 h-2 bg-white rounded-[2px]\" />\n                  )}\n                </div>\n                <p className=\"text-white text-sm font-medium\">Select All</p>\n              </button>\n              {selectedUsers.length > 0 && (\n                <button\n                  type=\"button\"\n                  onClick={handleUnselect}\n                  className=\"flex items-center gap-x-2 ml-2\"\n                >\n                  <p className=\"text-theme-text-secondary text-sm font-medium hover:text-theme-text-primary\">\n                    Unselect\n                  </p>\n                </button>\n              )}\n            </div>\n            <button\n              type=\"submit\"\n              className=\"transition-all duration-300 text-xs px-2 py-1 font-semibold rounded-lg bg-primary-button hover:bg-secondary border-2 border-transparent hover:border-primary-button hover:text-white h-[32px] w-[68px] -mr-8 whitespace-nowrap shadow-[0_4px_14px_rgba(0,0,0,0.25)]\"\n            >\n              Save\n            </button>\n          </div>\n        </form>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/Members/WorkspaceMemberRow/index.jsx",
    "content": "import { titleCase } from \"text-case\";\n\nexport default function WorkspaceMemberRow({ user }) {\n  return (\n    <>\n      <tr className=\"bg-transparent text-theme-text-primary text-sm font-medium\">\n        <th scope=\"row\" className=\"px-6 py-4 whitespace-nowrap\">\n          {user.username}\n        </th>\n        <td className=\"px-6 py-4\">{titleCase(user.role)}</td>\n        <td className=\"px-6 py-4\">{user.lastUpdatedAt}</td>\n      </tr>\n    </>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/Members/index.jsx",
    "content": "import ModalWrapper from \"@/components/ModalWrapper\";\nimport { useModal } from \"@/hooks/useModal\";\nimport Admin from \"@/models/admin\";\nimport { useEffect, useState } from \"react\";\nimport * as Skeleton from \"react-loading-skeleton\";\nimport AddMemberModal from \"./AddMemberModal\";\nimport WorkspaceMemberRow from \"./WorkspaceMemberRow\";\nimport CTAButton from \"@/components/lib/CTAButton\";\n\nexport default function Members({ workspace }) {\n  const [loading, setLoading] = useState(true);\n  const [users, setUsers] = useState([]);\n  const [workspaceUsers, setWorkspaceUsers] = useState([]);\n  const [adminWorkspace, setAdminWorkspace] = useState(null);\n\n  const { isOpen, openModal, closeModal } = useModal();\n  useEffect(() => {\n    async function fetchData() {\n      const _users = await Admin.users();\n      const workspaceUsers = await Admin.workspaceUsers(workspace.id);\n      const adminWorkspaces = await Admin.workspaces();\n      setAdminWorkspace(\n        adminWorkspaces.find(\n          (adminWorkspace) => adminWorkspace.id === workspace.id\n        )\n      );\n      setWorkspaceUsers(workspaceUsers);\n      setUsers(_users);\n      setLoading(false);\n    }\n    fetchData();\n  }, [workspace]);\n\n  if (loading) {\n    return (\n      <Skeleton.default\n        height=\"80vh\"\n        width=\"100%\"\n        highlightColor=\"var(--theme-bg-primary)\"\n        baseColor=\"var(--theme-bg-secondary)\"\n        count={1}\n        className=\"w-full p-4 rounded-b-2xl rounded-tr-2xl rounded-tl-sm mt-6\"\n        containerClassName=\"flex w-full\"\n      />\n    );\n  }\n\n  return (\n    <div className=\"flex justify-between -mt-3\">\n      <table className=\"w-full max-w-[700px] text-sm text-left rounded-lg\">\n        <thead className=\"text-white text-opacity-80 text-xs leading-[18px] font-bold uppercase border-white/10 border-b border-opacity-60\">\n          <tr>\n            <th scope=\"col\" className=\"px-6 py-3 rounded-tl-lg\">\n              Username\n            </th>\n            <th scope=\"col\" className=\"px-6 py-3\">\n              Role\n            </th>\n            <th scope=\"col\" className=\"px-6 py-3\">\n              Date Added\n            </th>\n            <th scope=\"col\" className=\"px-6 py-3 rounded-tr-lg\">\n              {\" \"}\n            </th>\n          </tr>\n        </thead>\n        <tbody>\n          {workspaceUsers.length > 0 ? (\n            workspaceUsers.map((user, index) => (\n              <WorkspaceMemberRow key={index} user={user} />\n            ))\n          ) : (\n            <tr>\n              <td className=\"text-center py-4 text-white/80\" colSpan=\"4\">\n                No workspace members\n              </td>\n            </tr>\n          )}\n        </tbody>\n      </table>\n      <CTAButton onClick={openModal}>Manage Users</CTAButton>\n      <ModalWrapper isOpen={isOpen}>\n        <AddMemberModal\n          closeModal={closeModal}\n          users={users}\n          workspace={adminWorkspace}\n        />\n      </ModalWrapper>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/VectorDatabase/DocumentSimilarityThreshold/index.jsx",
    "content": "import { useTranslation } from \"react-i18next\";\n\nexport default function DocumentSimilarityThreshold({\n  workspace,\n  setHasChanges,\n}) {\n  const { t } = useTranslation();\n  return (\n    <div>\n      <div className=\"flex flex-col\">\n        <label htmlFor=\"name\" className=\"block input-label\">\n          {t(\"vector-workspace.doc.title\")}\n        </label>\n        <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n          {t(\"vector-workspace.doc.description\")}\n        </p>\n      </div>\n      <select\n        name=\"similarityThreshold\"\n        defaultValue={workspace?.similarityThreshold ?? 0.25}\n        className=\"border-none bg-theme-settings-input-bg text-white text-sm mt-2 rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n        onChange={() => setHasChanges(true)}\n        required={true}\n      >\n        <option value={0.0}>{t(\"vector-workspace.doc.zero\")}</option>\n        <option value={0.25}>{t(\"vector-workspace.doc.low\")}</option>\n        <option value={0.5}>{t(\"vector-workspace.doc.medium\")}</option>\n        <option value={0.75}>{t(\"vector-workspace.doc.high\")}</option>\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/VectorDatabase/MaxContextSnippets/index.jsx",
    "content": "import { useTranslation } from \"react-i18next\";\n\nexport default function MaxContextSnippets({ workspace, setHasChanges }) {\n  const { t } = useTranslation();\n  return (\n    <div>\n      <div className=\"flex flex-col\">\n        <label htmlFor=\"name\" className=\"block input-label\">\n          {t(\"vector-workspace.snippets.title\")}\n        </label>\n        <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n          {t(\"vector-workspace.snippets.description\")}\n          <br />\n          <i>{t(\"vector-workspace.snippets.recommend\")}</i>\n        </p>\n      </div>\n      <input\n        name=\"topN\"\n        type=\"number\"\n        min={1}\n        max={200}\n        step={1}\n        onWheel={(e) => e.target.blur()}\n        defaultValue={workspace?.topN ?? 4}\n        className=\"border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 mt-2\"\n        placeholder=\"4\"\n        required={true}\n        autoComplete=\"off\"\n        onChange={() => setHasChanges(true)}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/VectorDatabase/ResetDatabase/index.jsx",
    "content": "import { useState } from \"react\";\nimport Workspace from \"@/models/workspace\";\nimport showToast from \"@/utils/toast\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function ResetDatabase({ workspace }) {\n  const [deleting, setDeleting] = useState(false);\n  const { t } = useTranslation();\n  const resetVectorDatabase = async () => {\n    if (!window.confirm(`${t(\"vector-workspace.reset.confirm\")}`)) return false;\n\n    setDeleting(true);\n    const success = await Workspace.wipeVectorDb(workspace.slug);\n    if (!success) {\n      showToast(\n        t(\"vector-workspace.reset.error\"),\n        t(\"vector-workspace.common.error\"),\n        {\n          clear: true,\n        }\n      );\n      setDeleting(false);\n      return;\n    }\n\n    showToast(\n      t(\"vector-workspace.reset.success\"),\n      t(\"vector-workspace.common.success\"),\n      {\n        clear: true,\n      }\n    );\n    setDeleting(false);\n  };\n\n  return (\n    <button\n      disabled={deleting}\n      onClick={resetVectorDatabase}\n      type=\"button\"\n      className=\"w-60 transition-all duration-300 border border-transparent rounded-lg whitespace-nowrap text-sm px-5 py-2.5 focus:z-10 bg-red-500/25 text-red-200 light:text-red-500 hover:light:text-[#FFFFFF] hover:text-[#FFFFFF] hover:bg-red-600 disabled:bg-red-600 disabled:text-red-200 disabled:animate-pulse\"\n    >\n      {deleting\n        ? t(\"vector-workspace.reset.resetting\")\n        : t(\"vector-workspace.reset.reset\")}\n    </button>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/VectorDatabase/VectorCount/index.jsx",
    "content": "import PreLoader from \"@/components/Preloader\";\nimport System from \"@/models/system\";\nimport { useEffect, useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nexport default function VectorCount({ reload, workspace }) {\n  const [totalVectors, setTotalVectors] = useState(null);\n  const { t } = useTranslation();\n\n  useEffect(() => {\n    async function fetchVectorCount() {\n      const totalVectors = await System.totalIndexes(workspace.slug);\n      setTotalVectors(totalVectors);\n    }\n    fetchVectorCount();\n  }, [workspace?.slug, reload]);\n\n  if (totalVectors === null)\n    return (\n      <div>\n        <h3 className=\"input-label\">{t(\"general.vector.title\")}</h3>\n        <p className=\"text-white text-opacity-60 text-xs font-medium py-1\">\n          {t(\"general.vector.description\")}\n        </p>\n        <div className=\"text-white text-opacity-60 text-sm font-medium\">\n          <PreLoader size=\"4\" />\n        </div>\n      </div>\n    );\n  return (\n    <div>\n      <h3 className=\"input-label\">{t(\"general.vector.title\")}</h3>\n      <p className=\"text-white text-opacity-60 text-sm font-medium\">\n        {totalVectors}\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/VectorDatabase/VectorDBIdentifier/index.jsx",
    "content": "import { useTranslation } from \"react-i18next\";\n\nexport default function VectorDBIdentifier({ workspace }) {\n  const { t } = useTranslation();\n  return (\n    <div>\n      <h3 className=\"input-label\">{t(\"vector-workspace.identifier\")}</h3>\n      <p className=\"text-white/60 text-xs font-medium py-1\"> </p>\n      <p className=\"text-white/60 text-sm\">{workspace?.slug}</p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/VectorDatabase/VectorSearchMode/index.jsx",
    "content": "import { useState } from \"react\";\n\n// We dont support all vectorDBs yet for reranking due to complexities of how each provider\n// returns information. We need to normalize the response data so Reranker can be used for each provider.\nconst supportedVectorDBs = [\"lancedb\"];\nconst hint = {\n  default: {\n    title: \"Default\",\n    description:\n      \"This is the fastest performance, but may not return the most relevant results leading to model hallucinations.\",\n  },\n  rerank: {\n    title: \"Accuracy Optimized\",\n    description:\n      \"LLM responses may take longer to generate, but your responses will be more accurate and relevant.\",\n  },\n};\n\nexport default function VectorSearchMode({ workspace, setHasChanges }) {\n  const [selection, setSelection] = useState(\n    workspace?.vectorSearchMode ?? \"default\"\n  );\n  if (!workspace?.vectorDB || !supportedVectorDBs.includes(workspace?.vectorDB))\n    return null;\n\n  return (\n    <div>\n      <div className=\"flex flex-col\">\n        <label htmlFor=\"name\" className=\"block input-label\">\n          Search Preference\n        </label>\n      </div>\n      <select\n        name=\"vectorSearchMode\"\n        value={selection}\n        className=\"border-none bg-theme-settings-input-bg text-white text-sm mt-2 rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5\"\n        onChange={(e) => {\n          setSelection(e.target.value);\n          setHasChanges(true);\n        }}\n        required={true}\n      >\n        <option value=\"default\">Default</option>\n        <option value=\"rerank\">Accuracy Optimized</option>\n      </select>\n      <p className=\"text-white text-opacity-60 text-xs font-medium py-1.5\">\n        {hint[selection]?.description}\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/VectorDatabase/index.jsx",
    "content": "import Workspace from \"@/models/workspace\";\nimport showToast from \"@/utils/toast\";\nimport { castToType } from \"@/utils/types\";\nimport { useRef, useState } from \"react\";\nimport VectorDBIdentifier from \"./VectorDBIdentifier\";\nimport MaxContextSnippets from \"./MaxContextSnippets\";\nimport DocumentSimilarityThreshold from \"./DocumentSimilarityThreshold\";\nimport ResetDatabase from \"./ResetDatabase\";\nimport VectorCount from \"./VectorCount\";\nimport VectorSearchMode from \"./VectorSearchMode\";\nimport CTAButton from \"@/components/lib/CTAButton\";\n\nexport default function VectorDatabase({ workspace }) {\n  const [hasChanges, setHasChanges] = useState(false);\n  const [saving, setSaving] = useState(false);\n  const formEl = useRef(null);\n\n  const handleUpdate = async (e) => {\n    setSaving(true);\n    e.preventDefault();\n    const data = {};\n    const form = new FormData(formEl.current);\n    for (var [key, value] of form.entries()) data[key] = castToType(key, value);\n    const { workspace: updatedWorkspace, message } = await Workspace.update(\n      workspace.slug,\n      data\n    );\n    if (!!updatedWorkspace) {\n      showToast(\"Workspace updated!\", \"success\", { clear: true });\n    } else {\n      showToast(`Error: ${message}`, \"error\", { clear: true });\n    }\n    setSaving(false);\n    setHasChanges(false);\n  };\n\n  if (!workspace) return null;\n  return (\n    <div className=\"w-full relative\">\n      <form\n        ref={formEl}\n        onSubmit={handleUpdate}\n        className=\"w-1/2 flex flex-col gap-y-6\"\n      >\n        {hasChanges && (\n          <div className=\"absolute top-0 right-0\">\n            <CTAButton type=\"submit\">\n              {saving ? \"Updating...\" : \"Update Workspace\"}\n            </CTAButton>\n          </div>\n        )}\n        <div className=\"flex items-start gap-x-5\">\n          <VectorDBIdentifier workspace={workspace} />\n          <VectorCount reload={true} workspace={workspace} />\n        </div>\n        <VectorSearchMode workspace={workspace} setHasChanges={setHasChanges} />\n        <MaxContextSnippets\n          workspace={workspace}\n          setHasChanges={setHasChanges}\n        />\n        <DocumentSimilarityThreshold\n          workspace={workspace}\n          setHasChanges={setHasChanges}\n        />\n        <ResetDatabase workspace={workspace} />\n      </form>\n    </div>\n  );\n}\n"
  },
  {
    "path": "frontend/src/pages/WorkspaceSettings/index.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { useParams } from \"react-router-dom\";\nimport Sidebar from \"@/components/Sidebar\";\nimport Workspace from \"@/models/workspace\";\nimport PasswordModal, { usePasswordModal } from \"@/components/Modals/Password\";\nimport { isMobile } from \"react-device-detect\";\nimport { FullScreenLoader } from \"@/components/Preloader\";\nimport {\n  ArrowUUpLeft,\n  ChatText,\n  Database,\n  Robot,\n  User,\n  Wrench,\n} from \"@phosphor-icons/react\";\nimport paths from \"@/utils/paths\";\nimport { Link } from \"react-router-dom\";\nimport { NavLink } from \"react-router-dom\";\nimport GeneralAppearance from \"./GeneralAppearance\";\nimport ChatSettings from \"./ChatSettings\";\nimport VectorDatabase from \"./VectorDatabase\";\nimport Members from \"./Members\";\nimport WorkspaceAgentConfiguration from \"./AgentConfig\";\nimport useUser from \"@/hooks/useUser\";\nimport { useTranslation } from \"react-i18next\";\nimport System from \"@/models/system\";\n\nconst TABS = {\n  \"general-appearance\": GeneralAppearance,\n  \"chat-settings\": ChatSettings,\n  \"vector-database\": VectorDatabase,\n  members: Members,\n  \"agent-config\": WorkspaceAgentConfiguration,\n};\n\nexport default function WorkspaceSettings() {\n  const { loading, requiresAuth, mode } = usePasswordModal();\n\n  if (loading) return <FullScreenLoader />;\n  if (requiresAuth !== false) {\n    return <>{requiresAuth !== null && <PasswordModal mode={mode} />}</>;\n  }\n\n  return <ShowWorkspaceChat />;\n}\n\nfunction ShowWorkspaceChat() {\n  const { t } = useTranslation();\n  const { slug, tab } = useParams();\n  const { user } = useUser();\n  const [workspace, setWorkspace] = useState(null);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    async function getWorkspace() {\n      if (!slug) return;\n      const _workspace = await Workspace.bySlug(slug);\n      if (!_workspace) {\n        setLoading(false);\n        return;\n      }\n\n      const _settings = await System.keys();\n      const suggestedMessages = await Workspace.getSuggestedMessages(slug);\n      setWorkspace({\n        ..._workspace,\n        vectorDB: _settings?.VectorDB,\n        suggestedMessages,\n      });\n      setLoading(false);\n    }\n    getWorkspace();\n  }, [slug, tab]);\n\n  if (loading) return <FullScreenLoader />;\n\n  const TabContent = TABS[tab];\n  return (\n    <div className=\"w-screen h-screen overflow-hidden bg-zinc-950 light:bg-slate-50 flex\">\n      {!isMobile && <Sidebar />}\n      <div\n        style={{ height: isMobile ? \"100%\" : \"calc(100% - 32px)\" }}\n        className=\"transition-all duration-500 relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] bg-theme-bg-secondary w-full h-full overflow-y-scroll\"\n      >\n        <div className=\"flex gap-x-10 pt-6 pb-4 ml-16 mr-8 border-b-2 border-white light:border-theme-chat-input-border border-opacity-10\">\n          <Link\n            to={paths.workspace.chat(slug)}\n            className=\"absolute top-2 left-2 md:top-4 md:left-4 transition-all duration-300 p-2 rounded-full text-white bg-theme-sidebar-footer-icon hover:bg-theme-sidebar-footer-icon-hover z-10\"\n          >\n            <ArrowUUpLeft className=\"h-5 w-5\" weight=\"fill\" />\n          </Link>\n          <TabItem\n            title={t(\"workspaces—settings.general\")}\n            icon={<Wrench className=\"h-6 w-6\" />}\n            to={paths.workspace.settings.generalAppearance(slug)}\n          />\n          <TabItem\n            title={t(\"workspaces—settings.chat\")}\n            icon={<ChatText className=\"h-6 w-6\" />}\n            to={paths.workspace.settings.chatSettings(slug)}\n          />\n          <TabItem\n            title={t(\"workspaces—settings.vector\")}\n            icon={<Database className=\"h-6 w-6\" />}\n            to={paths.workspace.settings.vectorDatabase(slug)}\n          />\n          <TabItem\n            title={t(\"workspaces—settings.members\")}\n            icon={<User className=\"h-6 w-6\" />}\n            to={paths.workspace.settings.members(slug)}\n            visible={[\"admin\", \"manager\"].includes(user?.role)}\n          />\n          <TabItem\n            title={t(\"workspaces—settings.agent\")}\n            icon={<Robot className=\"h-6 w-6\" />}\n            to={paths.workspace.settings.agentConfig(slug)}\n          />\n        </div>\n        <div className=\"px-16 py-6\">\n          <TabContent slug={slug} workspace={workspace} />\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction TabItem({ title, icon, to, visible = true }) {\n  if (!visible) return null;\n  return (\n    <NavLink\n      to={to}\n      className={({ isActive }) =>\n        `${\n          isActive\n            ? \"text-sky-400 pb-4 border-b-[4px] -mb-[19px] border-sky-400\"\n            : \"text-white/60 hover:text-sky-400\"\n        } ` + \" flex gap-x-2 items-center font-medium\"\n      }\n    >\n      {icon}\n      <div>{title}</div>\n    </NavLink>\n  );\n}\n"
  },
  {
    "path": "frontend/src/utils/chat/agent.js",
    "content": "import { v4 } from \"uuid\";\nimport { safeJsonParse } from \"../request\";\nimport { saveAs } from \"file-saver\";\nimport { API_BASE } from \"../constants\";\nimport { useEffect, useState } from \"react\";\n\nexport const AGENT_SESSION_START = \"agentSessionStart\";\nexport const AGENT_SESSION_END = \"agentSessionEnd\";\nconst handledEvents = [\n  \"statusResponse\",\n  \"fileDownload\",\n  \"awaitingFeedback\",\n  \"wssFailure\",\n  \"rechartVisualize\",\n  // Streaming events\n  \"reportStreamEvent\",\n];\n\nexport function websocketURI() {\n  const wsProtocol = window.location.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n  if (API_BASE === \"/api\") return `${wsProtocol}//${window.location.host}`;\n  return `${wsProtocol}//${new URL(import.meta.env.VITE_API_BASE).host}`;\n}\n\nexport default function handleSocketResponse(socket, event, setChatHistory) {\n  const data = safeJsonParse(event.data, null);\n  if (data === null) return;\n\n  // No message type is defined then this is a generic message\n  // that we need to print to the user as a system response\n  if (!data.hasOwnProperty(\"type\") && !socket.supportsAgentStreaming) {\n    return setChatHistory((prev) => {\n      return [\n        ...prev.filter((msg) => !!msg.content),\n        {\n          uuid: v4(),\n          content: data.content,\n          role: \"assistant\",\n          sources: [],\n          closed: true,\n          error: null,\n          animate: false,\n          pending: false,\n          metrics: {},\n        },\n      ];\n    });\n  }\n\n  if (!handledEvents.includes(data.type) || !data.content) return;\n\n  if (data.type === \"reportStreamEvent\") {\n    // Enable agent streaming for the next message so we can handle streaming or non-streaming responses\n    // If we get this message we know the provider supports agentic streaming\n    socket.supportsAgentStreaming = true;\n\n    return setChatHistory((prev) => {\n      if (data.content.type === \"removeStatusResponse\")\n        return [...prev.filter((msg) => msg.uuid !== data.content.uuid)];\n\n      const knownMessage = data.content.uuid\n        ? prev.find((msg) => msg.uuid === data.content.uuid)\n        : null;\n      if (!knownMessage) {\n        if (data.content.type === \"fullTextResponse\") {\n          return [\n            ...prev.filter((msg) => !!msg.content),\n            {\n              uuid: data.content.uuid,\n              type: \"textResponse\",\n              content: data.content.content,\n              role: \"assistant\",\n              sources: [],\n              closed: true,\n              error: null,\n              animate: false,\n              pending: false,\n              metrics: {},\n            },\n          ];\n        }\n\n        // Handle textResponseChunk initialization as textResponse instead of statusResponse.\n        // Without this the first chunk creates a statusResponse (thought bubble) by falling through to the default case.\n        // Providers like Gemini send large chunks and can complete in a single chunk before the update logic can convert it.\n        // Other providers send many small chunks so the second chunk triggers the update logic to fix the type.\n        if (data.content.type === \"textResponseChunk\") {\n          return [\n            ...prev.filter((msg) => !!msg.content),\n            {\n              uuid: data.content.uuid,\n              type: \"textResponse\",\n              content: data.content.content,\n              role: \"assistant\",\n              sources: [],\n              closed: true,\n              error: null,\n              animate: false,\n              pending: false,\n              metrics: {},\n            },\n          ];\n        }\n\n        return [\n          ...prev.filter((msg) => !!msg.content),\n          {\n            uuid: data.content.uuid,\n            type: \"statusResponse\",\n            content: data.content.content,\n            role: \"assistant\",\n            sources: [],\n            closed: true,\n            error: null,\n            animate: false,\n            pending: false,\n            metrics: {},\n          },\n        ];\n      } else {\n        const { type, content, uuid } = data.content;\n        // For tool call invocations, we need to update the existing message entirely since it is accumulated\n        // and we dont know if the function will have arguments or not while streaming - so replace the existing message entirely\n        if (type === \"toolCallInvocation\") {\n          const knownMessage = prev.find((msg) => msg.uuid === uuid);\n          if (!knownMessage)\n            return [...prev, { uuid, type: \"toolCallInvocation\", content }]; // If the message is not known, add it to the end of the list\n          return [\n            ...prev.filter((msg) => msg.uuid !== uuid),\n            { ...knownMessage, content },\n          ]; // If the message is known, replace it with the new content\n        }\n\n        if (type === \"usageMetrics\") {\n          if (!data.content.metrics) return prev;\n          return prev.map((msg) =>\n            msg.uuid === uuid ? { ...msg, metrics: data.content.metrics } : msg\n          );\n        }\n\n        if (type === \"citations\") {\n          if (!data.content.citations) return prev;\n          return prev.map((msg) =>\n            msg.uuid === uuid\n              ? {\n                  ...msg,\n                  sources: [...(msg.sources || []), ...data.content.citations],\n                }\n              : msg\n          );\n        }\n\n        if (type === \"textResponseChunk\") {\n          return prev\n            .map((msg) =>\n              msg.uuid === uuid\n                ? {\n                    ...msg,\n                    type: \"textResponse\",\n                    content: msg.content + content,\n                  }\n                : msg?.content\n                  ? msg\n                  : null\n            )\n            .filter((msg) => !!msg);\n        }\n\n        // Generic text response - will be put in the agent thought bubble\n        return prev.map((msg) =>\n          msg.uuid === data.content.uuid\n            ? { ...msg, content: msg.content + data.content.content }\n            : msg\n        );\n      }\n    });\n  }\n\n  if (data.type === \"fileDownload\") {\n    saveAs(data.content.b64Content, data.content.filename ?? \"unknown.txt\");\n    return;\n  }\n\n  if (data.type === \"rechartVisualize\") {\n    return setChatHistory((prev) => {\n      return [\n        ...prev.filter((msg) => !!msg.content),\n        {\n          type: \"rechartVisualize\",\n          uuid: v4(),\n          content: data.content,\n          role: \"assistant\",\n          sources: [],\n          closed: true,\n          error: null,\n          animate: false,\n          pending: false,\n          metrics: data.metrics || {},\n        },\n      ];\n    });\n  }\n\n  if (data.type === \"wssFailure\") {\n    return setChatHistory((prev) => {\n      return [\n        ...prev.filter((msg) => !!msg.content),\n        {\n          uuid: v4(),\n          content: data.content,\n          role: \"assistant\",\n          sources: [],\n          closed: true,\n          error: data.content,\n          animate: false,\n          pending: false,\n          metrics: {},\n        },\n      ];\n    });\n  }\n\n  return setChatHistory((prev) => {\n    return [\n      ...prev.filter((msg) => !!msg.content),\n      {\n        uuid: v4(),\n        type: data.type,\n        content: data.content,\n        role: \"assistant\",\n        sources: [],\n        closed: true,\n        error: null,\n        animate: data?.animate || false,\n        pending: false,\n        metrics: data.metrics || {},\n      },\n    ];\n  });\n}\n\nlet _agentSessionActive = false;\nexport function setAgentSessionActive(value) {\n  _agentSessionActive = value;\n}\nexport function getAgentSessionActive() {\n  return _agentSessionActive;\n}\n\nexport function useIsAgentSessionActive() {\n  const [activeSession, setActiveSession] = useState(\n    () => !!getAgentSessionActive()\n  );\n  useEffect(() => {\n    function listenForAgentSession() {\n      if (!window) return;\n      window.addEventListener(AGENT_SESSION_START, () =>\n        setActiveSession(true)\n      );\n      window.addEventListener(AGENT_SESSION_END, () => setActiveSession(false));\n    }\n    listenForAgentSession();\n  }, []);\n\n  return activeSession;\n}\n"
  },
  {
    "path": "frontend/src/utils/chat/hljs-libraries/svelte.js",
    "content": "export default function hljsDefineSvelte(hljs) {\n  return {\n    subLanguage: \"xml\",\n    contains: [\n      hljs.COMMENT(\"<!--\", \"-->\", {\n        relevance: 10,\n      }),\n      {\n        begin: /^(\\s*)(<script(\\s*context=\"module\")?>)/gm,\n        end: /^(\\s*)(<\\/script>)/gm,\n        subLanguage: \"javascript\",\n        excludeBegin: true,\n        excludeEnd: true,\n        contains: [\n          {\n            begin: /^(\\s*)(\\$:)/gm,\n            end: /(\\s*)/gm,\n            className: \"keyword\",\n          },\n        ],\n      },\n      {\n        begin: /^(\\s*)(<style.*>)/gm,\n        end: /^(\\s*)(<\\/style>)/gm,\n        subLanguage: \"css\",\n        excludeBegin: true,\n        excludeEnd: true,\n      },\n      {\n        begin: /\\{/gm,\n        end: /\\}/gm,\n        subLanguage: \"javascript\",\n        contains: [\n          {\n            begin: /[\\{]/,\n            end: /[\\}]/,\n            skip: true,\n          },\n          {\n            begin: /([#:\\/@])(if|else|each|await|then|catch|debug|html)/gm,\n            className: \"keyword\",\n            relevance: 10,\n          },\n        ],\n      },\n    ],\n  };\n}\n"
  },
  {
    "path": "frontend/src/utils/chat/index.js",
    "content": "import { THREAD_RENAME_EVENT } from \"@/components/Sidebar/ActiveWorkspaces/ThreadContainer\";\nimport { emitAssistantMessageCompleteEvent } from \"@/components/contexts/TTSProvider\";\nexport const ABORT_STREAM_EVENT = \"abort-chat-stream\";\n\n// For handling of chat responses in the frontend by their various types.\nexport default function handleChat(\n  chatResult,\n  setLoadingResponse,\n  setChatHistory,\n  remHistory,\n  _chatHistory,\n  setWebsocket\n) {\n  const {\n    uuid,\n    textResponse,\n    type,\n    sources = [],\n    error,\n    close,\n    animate = false,\n    chatId = null,\n    action = null,\n    metrics = {},\n  } = chatResult;\n\n  if (type === \"abort\" || type === \"statusResponse\") {\n    setLoadingResponse(false);\n    setChatHistory([\n      ...remHistory,\n      {\n        type,\n        uuid,\n        content: textResponse,\n        role: \"assistant\",\n        sources,\n        closed: true,\n        error,\n        animate,\n        pending: false,\n        metrics,\n      },\n    ]);\n    _chatHistory.push({\n      type,\n      uuid,\n      content: textResponse,\n      role: \"assistant\",\n      sources,\n      closed: true,\n      error,\n      animate,\n      pending: false,\n      metrics,\n    });\n  } else if (type === \"textResponse\") {\n    setLoadingResponse(false);\n    setChatHistory([\n      ...remHistory,\n      {\n        uuid,\n        content: textResponse,\n        role: \"assistant\",\n        sources,\n        closed: close,\n        error,\n        animate: !close,\n        pending: false,\n        chatId,\n        metrics,\n      },\n    ]);\n    _chatHistory.push({\n      uuid,\n      content: textResponse,\n      role: \"assistant\",\n      sources,\n      closed: close,\n      error,\n      animate: !close,\n      pending: false,\n      chatId,\n      metrics,\n    });\n    emitAssistantMessageCompleteEvent(chatId);\n  } else if (\n    type === \"textResponseChunk\" ||\n    type === \"finalizeResponseStream\"\n  ) {\n    const chatIdx = _chatHistory.findIndex((chat) => chat.uuid === uuid);\n    if (chatIdx !== -1) {\n      const existingHistory = { ..._chatHistory[chatIdx] };\n      let updatedHistory;\n\n      // If the response is finalized, we can set the loading state to false.\n      // and append the metrics to the history.\n      if (type === \"finalizeResponseStream\") {\n        updatedHistory = {\n          ...existingHistory,\n          closed: close,\n          animate: !close,\n          pending: false,\n          chatId,\n          metrics,\n        };\n\n        _chatHistory[chatIdx - 1] = { ..._chatHistory[chatIdx - 1], chatId }; // update prompt with chatID\n\n        emitAssistantMessageCompleteEvent(chatId);\n        setLoadingResponse(false);\n      } else {\n        updatedHistory = {\n          ...existingHistory,\n          content: existingHistory.content + textResponse,\n          ...(sources && sources.length > 0 ? { sources } : {}),\n          error,\n          closed: close,\n          animate: !close,\n          pending: false,\n          chatId,\n          metrics,\n        };\n      }\n      _chatHistory[chatIdx] = updatedHistory;\n    } else {\n      _chatHistory.push({\n        uuid,\n        sources,\n        error,\n        content: textResponse,\n        role: \"assistant\",\n        closed: close,\n        animate: !close,\n        pending: false,\n        chatId,\n        metrics,\n      });\n    }\n    setChatHistory([..._chatHistory]);\n  } else if (type === \"agentInitWebsocketConnection\") {\n    setWebsocket(chatResult.websocketUUID);\n  } else if (type === \"stopGeneration\") {\n    const chatIdx = _chatHistory.length - 1;\n    const existingHistory = { ..._chatHistory[chatIdx] };\n    const updatedHistory = {\n      ...existingHistory,\n      sources: [],\n      closed: true,\n      error: null,\n      animate: false,\n      pending: false,\n      metrics,\n    };\n    _chatHistory[chatIdx] = updatedHistory;\n\n    setChatHistory([..._chatHistory]);\n    setLoadingResponse(false);\n  }\n\n  // Action Handling via special 'action' attribute on response.\n  if (action === \"reset_chat\") setChatHistory([]);\n\n  // If thread was updated automatically based on chat prompt\n  // then we can handle the updating of the thread here.\n  if (action === \"rename_thread\") {\n    if (!!chatResult?.thread?.slug && chatResult.thread.name) {\n      window.dispatchEvent(\n        new CustomEvent(THREAD_RENAME_EVENT, {\n          detail: {\n            threadSlug: chatResult.thread.slug,\n            newName: chatResult.thread.name,\n          },\n        })\n      );\n    }\n  }\n}\n\nexport function getWorkspaceSystemPrompt(workspace) {\n  return (\n    workspace?.openAiPrompt ??\n    \"Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.\"\n  );\n}\n\nexport function chatQueryRefusalResponse(workspace) {\n  return (\n    workspace?.queryRefusalResponse ??\n    \"There is no relevant information in this workspace to answer your query.\"\n  );\n}\n"
  },
  {
    "path": "frontend/src/utils/chat/markdown.js",
    "content": "import { encode as HTMLEncode } from \"he\";\nimport markdownIt from \"markdown-it\";\nimport markdownItKatexPlugin from \"./plugins/markdown-katex\";\nimport Appearance from \"@/models/appearance\";\nimport hljs from \"highlight.js\";\nimport \"./themes/github-dark.css\";\nimport \"./themes/github.css\";\nimport { v4 } from \"uuid\";\n\n// Register custom lanaguages\nimport hljsDefineSvelte from \"./hljs-libraries/svelte\";\nhljs.registerLanguage(\"svelte\", hljsDefineSvelte);\n\nconst markdown = markdownIt({\n  html: Appearance.get(\"renderHTML\") ?? false,\n  typographer: true,\n  highlight: function (code, lang) {\n    const uuid = v4();\n    const theme =\n      window.localStorage.getItem(\"theme\") === \"light\"\n        ? \"github\"\n        : \"github-dark\";\n\n    if (lang && hljs.getLanguage(lang)) {\n      try {\n        return (\n          `<div class=\"whitespace-pre-line w-full max-w-[65vw] hljs ${theme} light:border-solid light:border light:border-gray-700 rounded-lg relative font-mono font-normal text-sm text-slate-200\">\n            <div class=\"w-full flex items-center sticky top-0 text-slate-200 light:bg-sky-800 bg-stone-800 px-4 py-2 text-xs font-sans justify-between rounded-t-md -mt-5\">\n              <div class=\"flex gap-2\">\n                <code class=\"text-xs\">${lang || \"\"}</code>\n              </div>\n              <button data-code-snippet data-code=\"code-${uuid}\" class=\"flex items-center gap-x-1\">\n                <svg stroke=\"currentColor\" fill=\"none\" stroke-width=\"2\" viewBox=\"0 0 24 24\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"h-3 w-3\" height=\"1em\" width=\"1em\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"></path><rect x=\"8\" y=\"2\" width=\"8\" height=\"4\" rx=\"1\" ry=\"1\"></rect></svg>\n                <p class=\"text-xs\" style=\"margin: 0px;padding: 0px;\">Copy block</p>\n              </button>\n            </div>\n            <pre class=\"whitespace-pre-wrap px-4 pb-4\">` +\n          hljs.highlight(code, { language: lang, ignoreIllegals: true }).value +\n          \"</pre></div>\"\n        );\n      } catch {}\n    }\n\n    return (\n      `<div class=\"whitespace-pre-line w-full max-w-[65vw] hljs ${theme} light:border-solid light:border light:border-gray-700 rounded-lg relative font-mono font-normal text-sm text-slate-200\">\n        <div class=\"w-full flex items-center sticky top-0 text-slate-200 bg-stone-800 px-4 py-2 text-xs font-sans justify-between rounded-t-md -mt-5\">\n          <div class=\"flex gap-2\"><code class=\"text-xs\"></code></div>\n          <button data-code-snippet data-code=\"code-${uuid}\" class=\"flex items-center gap-x-1\">\n            <svg stroke=\"currentColor\" fill=\"none\" stroke-width=\"2\" viewBox=\"0 0 24 24\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"h-3 w-3\" height=\"1em\" width=\"1em\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"></path><rect x=\"8\" y=\"2\" width=\"8\" height=\"4\" rx=\"1\" ry=\"1\"></rect></svg>\n            <p class=\"text-xs\" style=\"margin: 0px;padding: 0px;\">Copy block</p>\n          </button>\n        </div>\n        <pre class=\"whitespace-pre-wrap px-4 pb-4\">` +\n      HTMLEncode(code) +\n      \"</pre></div>\"\n    );\n  },\n});\n\n// Add custom renderer for strong tags to handle theme colors\nmarkdown.renderer.rules.strong_open = () => '<strong class=\"text-white\">';\nmarkdown.renderer.rules.strong_close = () => \"</strong>\";\nmarkdown.renderer.rules.link_open = (tokens, idx) => {\n  const token = tokens[idx];\n  const href = token.attrs.find((attr) => attr[0] === \"href\");\n  return `<a href=\"${href[1]}\" target=\"_blank\" rel=\"noopener noreferrer\">`;\n};\n\n// Custom renderer for responsive images rendered in markdown\nmarkdown.renderer.rules.image = function (tokens, idx) {\n  const token = tokens[idx];\n  const srcIndex = token.attrIndex(\"src\");\n  const src = token.attrs[srcIndex][1];\n  const alt = token.content || \"\";\n\n  return `<div class=\"w-full max-w-[800px]\"><img src=\"${src}\" alt=\"${alt}\" class=\"w-full h-auto\" /></div>`;\n};\n\nmarkdown.use(markdownItKatexPlugin);\n\nexport default function renderMarkdown(text = \"\") {\n  return markdown.render(text);\n}\n"
  },
  {
    "path": "frontend/src/utils/chat/plugins/markdown-katex.js",
    "content": "import katex from \"katex\";\n\n// Test if potential opening or closing delimieter\n// Assumes that there is a \"$\" at state.src[pos]\nfunction isValidDelim(state, pos) {\n  var prevChar,\n    nextChar,\n    max = state.posMax,\n    can_open = true,\n    can_close = true;\n\n  prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1;\n  nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1;\n\n  // Only apply whitespace rules if we're dealing with $ delimiter\n  if (state.src[pos] === \"$\") {\n    if (\n      prevChar === 0x20 /* \" \" */ ||\n      prevChar === 0x09 /* \\t */ ||\n      (nextChar >= 0x30 /* \"0\" */ && nextChar <= 0x39) /* \"9\" */\n    ) {\n      can_close = false;\n    }\n    if (nextChar === 0x20 /* \" \" */ || nextChar === 0x09 /* \\t */) {\n      can_open = false;\n    }\n  }\n\n  return {\n    can_open: can_open,\n    can_close: can_close,\n  };\n}\n\nfunction math_inline(state, silent) {\n  var start, match, token, res, pos;\n\n  // Only process $ and \\( delimiters for inline math\n  if (\n    state.src[state.pos] !== \"$\" &&\n    (state.src[state.pos] !== \"\\\\\" || state.src[state.pos + 1] !== \"(\")\n  ) {\n    return false;\n  }\n\n  // Handle \\( ... \\) case separately\n  if (state.src[state.pos] === \"\\\\\" && state.src[state.pos + 1] === \"(\") {\n    start = state.pos + 2;\n    match = start;\n    while ((match = state.src.indexOf(\"\\\\)\", match)) !== -1) {\n      pos = match - 1;\n      while (state.src[pos] === \"\\\\\") {\n        pos -= 1;\n      }\n      if ((match - pos) % 2 == 1) {\n        break;\n      }\n      match += 1;\n    }\n\n    if (match === -1) {\n      if (!silent) {\n        state.pending += \"\\\\(\";\n      }\n      state.pos = start;\n      return true;\n    }\n\n    if (!silent) {\n      token = state.push(\"math_inline\", \"math\", 0);\n      token.markup = \"\\\\(\";\n      token.content = state.src.slice(start, match);\n    }\n\n    state.pos = match + 2;\n    return true;\n  }\n\n  res = isValidDelim(state, state.pos);\n  if (!res.can_open) {\n    if (!silent) {\n      state.pending += \"$\";\n    }\n    state.pos += 1;\n    return true;\n  }\n\n  // First check for and bypass all properly escaped delimieters\n  // This loop will assume that the first leading backtick can not\n  // be the first character in state.src, which is known since\n  // we have found an opening delimieter already.\n  start = state.pos + 1;\n  match = start;\n  while ((match = state.src.indexOf(\"$\", match)) !== -1) {\n    // Found potential $, look for escapes, pos will point to\n    // first non escape when complete\n    pos = match - 1;\n    while (state.src[pos] === \"\\\\\") {\n      pos -= 1;\n    }\n\n    // Even number of escapes, potential closing delimiter found\n    if ((match - pos) % 2 == 1) {\n      break;\n    }\n    match += 1;\n  }\n\n  // No closing delimiter found.  Consume $ and continue.\n  if (match === -1) {\n    if (!silent) {\n      state.pending += \"$\";\n    }\n    state.pos = start;\n    return true;\n  }\n\n  // Check if we have empty content, ie: $$.  Do not parse.\n  if (match - start === 0) {\n    if (!silent) {\n      state.pending += \"$$\";\n    }\n    state.pos = start + 1;\n    return true;\n  }\n\n  // Check for valid closing delimiter\n  res = isValidDelim(state, match);\n  if (!res.can_close) {\n    if (!silent) {\n      state.pending += \"$\";\n    }\n    state.pos = start;\n    return true;\n  }\n\n  if (!silent) {\n    token = state.push(\"math_inline\", \"math\", 0);\n    token.markup = \"$\";\n    token.content = state.src.slice(start, match);\n  }\n\n  state.pos = match + 1;\n  return true;\n}\n\nfunction math_block(state, start, end, silent) {\n  var firstLine,\n    lastLine,\n    next,\n    lastPos,\n    found = false,\n    token,\n    pos = state.bMarks[start] + state.tShift[start],\n    max = state.eMarks[start];\n\n  // Check for $$, \\[, or standalone [ as opening delimiters\n  if (pos + 1 > max) {\n    return false;\n  }\n\n  let openDelim = state.src.slice(pos, pos + 2);\n  let isDoubleDollar = openDelim === \"$$\";\n  let isLatexBracket = openDelim === \"\\\\[\";\n\n  if (!isDoubleDollar && !isLatexBracket) {\n    return false;\n  }\n\n  // Determine the closing delimiter and position adjustment\n  let delimiter, posAdjust;\n  if (isDoubleDollar) {\n    delimiter = \"$$\";\n    posAdjust = 2;\n  } else if (isLatexBracket) {\n    delimiter = \"\\\\]\";\n    posAdjust = 2;\n  }\n\n  pos += posAdjust;\n  firstLine = state.src.slice(pos, max);\n\n  if (silent) {\n    return true;\n  }\n  if (firstLine.trim().slice(-delimiter.length) === delimiter) {\n    // Single line expression\n    firstLine = firstLine.trim().slice(0, -delimiter.length);\n    found = true;\n  }\n\n  for (next = start; !found; ) {\n    next++;\n\n    if (next >= end) {\n      break;\n    }\n\n    pos = state.bMarks[next] + state.tShift[next];\n    max = state.eMarks[next];\n\n    if (pos < max && state.tShift[next] < state.blkIndent) {\n      // non-empty line with negative indent should stop the list:\n      break;\n    }\n\n    if (\n      state.src.slice(pos, max).trim().slice(-delimiter.length) === delimiter\n    ) {\n      lastPos = state.src.slice(0, max).lastIndexOf(delimiter);\n      lastLine = state.src.slice(pos, lastPos);\n      found = true;\n    }\n  }\n\n  state.line = next + 1;\n\n  token = state.push(\"math_block\", \"math\", 0);\n  token.block = true;\n  token.content =\n    (firstLine && firstLine.trim() ? firstLine + \"\\n\" : \"\") +\n    state.getLines(start + 1, next, state.tShift[start], true) +\n    (lastLine && lastLine.trim() ? lastLine : \"\");\n  token.map = [start, state.line];\n  token.markup = delimiter;\n  return true;\n}\n\nexport default function math_plugin(md, options) {\n  // Default options\n  options = options || {};\n\n  var katexInline = function (latex) {\n    options.displayMode = false;\n    try {\n      latex = latex\n        .replace(/^\\[(.*)\\]$/, \"$1\")\n        .replace(/^\\\\\\((.*)\\\\\\)$/, \"$1\")\n        .replace(/^\\\\\\[(.*)\\\\\\]$/, \"$1\");\n      return katex.renderToString(latex, options);\n    } catch (error) {\n      if (options.throwOnError) {\n        console.log(error);\n      }\n      return latex;\n    }\n  };\n\n  var inlineRenderer = function (tokens, idx) {\n    return katexInline(tokens[idx].content);\n  };\n\n  var katexBlock = function (latex) {\n    options.displayMode = true;\n    try {\n      // Remove surrounding delimiters if present\n      latex = latex.replace(/^\\[(.*)\\]$/, \"$1\").replace(/^\\\\\\[(.*)\\\\\\]$/, \"$1\");\n      return \"<p>\" + katex.renderToString(latex, options) + \"</p>\";\n    } catch (error) {\n      if (options.throwOnError) {\n        console.log(error);\n      }\n      return latex;\n    }\n  };\n\n  var blockRenderer = function (tokens, idx) {\n    return katexBlock(tokens[idx].content) + \"\\n\";\n  };\n\n  md.inline.ruler.after(\"escape\", \"math_inline\", math_inline);\n  md.block.ruler.after(\"blockquote\", \"math_block\", math_block, {\n    alt: [\"paragraph\", \"reference\", \"blockquote\", \"list\"],\n  });\n  md.renderer.rules.math_inline = inlineRenderer;\n  md.renderer.rules.math_block = blockRenderer;\n}\n"
  },
  {
    "path": "frontend/src/utils/chat/purify.js",
    "content": "import createDOMPurify from \"dompurify\";\n\nconst DOMPurify = createDOMPurify(window);\nDOMPurify.setConfig({\n  ADD_ATTR: [\"target\", \"rel\"],\n});\n\nexport default DOMPurify;\n"
  },
  {
    "path": "frontend/src/utils/chat/themes/github-dark.css",
    "content": "/*!\n  Theme: GitHub Dark\n  Description: Dark theme as seen on github.com\n  Author: github.com\n  Maintainer: @Hirse\n  Updated: 2021-05-15\n\n  Outdated base version: https://github.com/primer/github-syntax-dark\n  Current colors taken from GitHub's CSS\n*/\n\n.github-dark.hljs {\n  color: #c9d1d9;\n  background: #0d1117;\n}\n\n.github-dark .hljs-doctag,\n.github-dark .hljs-keyword,\n.github-dark .hljs-meta .hljs-keyword,\n.github-dark .hljs-template-tag,\n.github-dark .hljs-template-variable,\n.github-dark .hljs-type,\n.github-dark .hljs-variable.language_ {\n  /* prettylights-syntax-keyword */\n  color: #ff7b72;\n}\n\n.github-dark .hljs-title,\n.github-dark .hljs-title.class_,\n.github-dark .hljs-title.class_.inherited__,\n.github-dark .hljs-title.function_ {\n  /* prettylights-syntax-entity */\n  color: #d2a8ff;\n}\n\n.github-dark .hljs-attr,\n.github-dark .hljs-attribute,\n.github-dark .hljs-literal,\n.github-dark .hljs-meta,\n.github-dark .hljs-number,\n.github-dark .hljs-operator,\n.github-dark .hljs-variable,\n.github-dark .hljs-selector-attr,\n.github-dark .hljs-selector-class,\n.github-dark .hljs-selector-id {\n  /* prettylights-syntax-constant */\n  color: #79c0ff;\n}\n\n.github-dark .hljs-regexp,\n.github-dark .hljs-string,\n.github-dark .hljs-meta .hljs-string {\n  /* prettylights-syntax-string */\n  color: #a5d6ff;\n}\n\n.github-dark .hljs-built_in,\n.github-dark .hljs-symbol {\n  /* prettylights-syntax-variable */\n  color: #ffa657;\n}\n\n.github-dark .hljs-comment,\n.github-dark .hljs-code,\n.github-dark .hljs-formula {\n  /* prettylights-syntax-comment */\n  color: #8b949e;\n}\n\n.github-dark .hljs-name,\n.github-dark .hljs-quote,\n.github-dark .hljs-selector-tag,\n.github-dark .hljs-selector-pseudo {\n  /* prettylights-syntax-entity-tag */\n  color: #7ee787;\n}\n\n.github-dark .hljs-subst {\n  /* prettylights-syntax-storage-modifier-import */\n  color: #c9d1d9;\n}\n\n.github-dark .hljs-section {\n  /* prettylights-syntax-markup-heading */\n  color: #1f6feb;\n  font-weight: bold;\n}\n\n.github-dark .hljs-bullet {\n  /* prettylights-syntax-markup-list */\n  color: #f2cc60;\n}\n\n.github-dark .hljs-emphasis {\n  /* prettylights-syntax-markup-italic */\n  color: #c9d1d9;\n  font-style: italic;\n}\n\n.github-dark .hljs-strong {\n  /* prettylights-syntax-markup-bold */\n  color: #c9d1d9;\n  font-weight: bold;\n}\n\n.github-dark .hljs-addition {\n  /* prettylights-syntax-markup-inserted */\n  color: #aff5b4;\n  background-color: #033a16;\n}\n\n.github-dark .hljs-deletion {\n  /* prettylights-syntax-markup-deleted */\n  color: #ffdcd7;\n  background-color: #67060c;\n}\n\n.github-dark .hljs-char.escape_,\n.github-dark .hljs-link,\n.github-dark .hljs-params,\n.github-dark .hljs-property,\n.github-dark .hljs-punctuation,\n.github-dark .hljs-tag {\n  /* purposely ignored */\n}\n"
  },
  {
    "path": "frontend/src/utils/chat/themes/github.css",
    "content": "/*!\n  Theme: GitHub\n  Description: Light theme as seen on github.com\n  Author: github.com\n  Maintainer: @Hirse\n  Updated: 2021-05-15\n\n  Outdated base version: https://github.com/primer/github-syntax-light\n  Current colors taken from GitHub's CSS\n*/\n\n.github.hljs {\n  color: #24292e;\n  background: #ffffff;\n}\n\n.github .hljs-doctag,\n.github .hljs-keyword,\n.github .hljs-meta .hljs-keyword,\n.github .hljs-template-tag,\n.github .hljs-template-variable,\n.github .hljs-type,\n.github .hljs-variable.language_ {\n  /* prettylights-syntax-keyword */\n  color: #d73a49;\n}\n\n.github .hljs-title,\n.github .hljs-title.class_,\n.github .hljs-title.class_.inherited__,\n.github .hljs-title.function_ {\n  /* prettylights-syntax-entity */\n  color: #6f42c1;\n}\n\n.github .hljs-attr,\n.github .hljs-attribute,\n.github .hljs-literal,\n.github .hljs-meta,\n.github .hljs-number,\n.github .hljs-operator,\n.github .hljs-variable,\n.github .hljs-selector-attr,\n.github .hljs-selector-class,\n.github .hljs-selector-id {\n  /* prettylights-syntax-constant */\n  color: #005cc5;\n}\n\n.github .hljs-regexp,\n.github .hljs-string,\n.github .hljs-meta .hljs-string {\n  /* prettylights-syntax-string */\n  color: #032f62;\n}\n\n.github .hljs-built_in,\n.github .hljs-symbol {\n  /* prettylights-syntax-variable */\n  color: #e36209;\n}\n\n.github .hljs-comment,\n.github .hljs-code,\n.github .hljs-formula {\n  /* prettylights-syntax-comment */\n  color: #6a737d;\n}\n\n.github .hljs-name,\n.github .hljs-quote,\n.github .hljs-selector-tag,\n.github .hljs-selector-pseudo {\n  /* prettylights-syntax-entity-tag */\n  color: #22863a;\n}\n\n.github .hljs-subst {\n  /* prettylights-syntax-storage-modifier-import */\n  color: #24292e;\n}\n\n.github .hljs-section {\n  /* prettylights-syntax-markup-heading */\n  color: #005cc5;\n  font-weight: bold;\n}\n\n.github .hljs-bullet {\n  /* prettylights-syntax-markup-list */\n  color: #735c0f;\n}\n\n.github .hljs-emphasis {\n  /* prettylights-syntax-markup-italic */\n  color: #24292e;\n  font-style: italic;\n}\n\n.github .hljs-strong {\n  /* prettylights-syntax-markup-bold */\n  color: #24292e;\n  font-weight: bold;\n}\n\n.github .hljs-addition {\n  /* prettylights-syntax-markup-inserted */\n  color: #22863a;\n  background-color: #f0fff4;\n}\n\n.github .hljs-deletion {\n  /* prettylights-syntax-markup-deleted */\n  color: #b31d28;\n  background-color: #ffeef0;\n}\n\n.github .hljs-char.escape_,\n.github .hljs-link,\n.github .hljs-params,\n.github .hljs-property,\n.github .hljs-punctuation,\n.github .hljs-tag {\n  /* purposely ignored */\n}\n"
  },
  {
    "path": "frontend/src/utils/constants.js",
    "content": "export const API_BASE = import.meta.env.VITE_API_BASE || \"/api\";\nexport const ONBOARDING_SURVEY_URL = \"https://onboarding.anythingllm.com\";\n\nexport const AUTH_USER = \"anythingllm_user\";\nexport const AUTH_TOKEN = \"anythingllm_authToken\";\nexport const AUTH_TIMESTAMP = \"anythingllm_authTimestamp\";\nexport const COMPLETE_QUESTIONNAIRE = \"anythingllm_completed_questionnaire\";\nexport const SEEN_DOC_PIN_ALERT = \"anythingllm_pinned_document_alert\";\nexport const SEEN_WATCH_ALERT = \"anythingllm_watched_document_alert\";\nexport const LAST_VISITED_WORKSPACE = \"anythingllm_last_visited_workspace\";\nexport const USER_PROMPT_INPUT_MAP = \"anythingllm_user_prompt_input_map\";\nexport const PENDING_HOME_MESSAGE = \"anythingllm_pending_home_message\";\n\nexport const APPEARANCE_SETTINGS = \"anythingllm_appearance_settings\";\n\nexport const OLLAMA_COMMON_URLS = [\n  \"http://127.0.0.1:11434\",\n  \"http://host.docker.internal:11434\",\n  \"http://172.17.0.1:11434\",\n];\n\nexport const LMSTUDIO_COMMON_URLS = [\n  \"http://localhost:1234/v1\",\n  \"http://127.0.0.1:1234/v1\",\n  \"http://host.docker.internal:1234/v1\",\n  \"http://172.17.0.1:1234/v1\",\n];\n\nexport const KOBOLDCPP_COMMON_URLS = [\n  \"http://127.0.0.1:5000/v1\",\n  \"http://localhost:5000/v1\",\n  \"http://host.docker.internal:5000/v1\",\n  \"http://172.17.0.1:5000/v1\",\n];\n\nexport const LOCALAI_COMMON_URLS = [\n  \"http://127.0.0.1:8080/v1\",\n  \"http://localhost:8080/v1\",\n  \"http://host.docker.internal:8080/v1\",\n  \"http://172.17.0.1:8080/v1\",\n];\n\nexport const DPAIS_COMMON_URLS = [\n  \"http://127.0.0.1:8553/v1/openai\",\n  \"http://0.0.0.0:8553/v1/openai\",\n  \"http://localhost:8553/v1/openai\",\n  \"http://host.docker.internal:8553/v1/openai\",\n];\n\nexport const NVIDIA_NIM_COMMON_URLS = [\n  \"http://127.0.0.1:8000/v1/version\",\n  \"http://localhost:8000/v1/version\",\n  \"http://host.docker.internal:8000/v1/version\",\n  \"http://172.17.0.1:8000/v1/version\",\n];\n\nexport const DOCKER_MODEL_RUNNER_COMMON_URLS = [\n  \"http://localhost:12434/engines/llama.cpp/v1\",\n  \"http://127.0.0.1:12434/engines/llama.cpp/v1\",\n  \"http://model-runner.docker.internal/engines/llama.cpp/v1\",\n  \"http://host.docker.internal:12434/engines/llama.cpp/v1\",\n  \"http://172.17.0.1:12434/engines/llama.cpp/v1\",\n];\n\nexport const LEMONADE_COMMON_URLS = [\n  \"http://localhost:8000/live\",\n  \"http://127.0.0.1:8000/live\",\n  \"http://host.docker.internal:8000/live\",\n  \"http://172.17.0.1:8000/live\",\n];\n\nexport function fullApiUrl() {\n  if (API_BASE !== \"/api\") return API_BASE;\n  return `${window.location.origin}/api`;\n}\n\nexport const POPUP_BROWSER_EXTENSION_EVENT = \"NEW_BROWSER_EXTENSION_CONNECTION\";\n"
  },
  {
    "path": "frontend/src/utils/directories.js",
    "content": "import moment from \"moment\";\n\nexport function formatDate(dateString) {\n  const date = isNaN(new Date(dateString).getTime())\n    ? new Date()\n    : new Date(dateString);\n  const options = { year: \"numeric\", month: \"short\", day: \"numeric\" };\n  const formattedDate = date.toLocaleDateString(\"en-US\", options);\n  return formattedDate;\n}\n\nexport function formatDateTimeAsMoment(dateString, format = \"LLL\") {\n  if (!dateString) return moment().format(format);\n  try {\n    return moment(dateString).format(format);\n  } catch {\n    return moment().format(format);\n  }\n}\n\nexport function getFileExtension(path) {\n  const hasExtension = path?.includes(\".\");\n  if (!hasExtension) return \"FILE\";\n  const extension = path?.split(\".\")?.slice(-1)?.[0];\n  return extension?.toUpperCase() || \"FILE\";\n}\n\nexport function middleTruncate(str, n) {\n  const fileExtensionPattern = /([^.]*)$/;\n  const extensionMatch = str.includes(\".\") && str.match(fileExtensionPattern);\n\n  if (str.length <= n) return str;\n\n  if (extensionMatch && extensionMatch[1]) {\n    const extension = extensionMatch[1];\n    const nameWithoutExtension = str.replace(fileExtensionPattern, \"\");\n    const truncationPoint = Math.max(0, n - extension.length - 4);\n    const truncatedName =\n      nameWithoutExtension.substr(0, truncationPoint) +\n      \"...\" +\n      nameWithoutExtension.slice(-4);\n\n    return truncatedName + extension;\n  } else {\n    return str.length > n ? str.substr(0, n - 8) + \"...\" + str.slice(-4) : str;\n  }\n}\n"
  },
  {
    "path": "frontend/src/utils/keyboardShortcuts.js",
    "content": "import paths from \"./paths\";\nimport { useEffect } from \"react\";\nimport { userFromStorage } from \"./request\";\nimport { TOGGLE_LLM_SELECTOR_EVENT } from \"@/components/WorkspaceChat/ChatContainer/PromptInput/LLMSelector/action\";\n\nexport const KEYBOARD_SHORTCUTS_HELP_EVENT = \"keyboard-shortcuts-help\";\nexport const isMac = navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0;\nexport const SHORTCUTS = {\n  \"⌘ + ,\": {\n    translationKey: \"settings\",\n    action: () => {\n      window.location.href = paths.settings.interface();\n    },\n  },\n  \"⌘ + H\": {\n    translationKey: \"home\",\n    action: () => {\n      window.location.href = paths.home();\n    },\n  },\n  \"⌘ + I\": {\n    translationKey: \"workspaces\",\n    action: () => {\n      window.location.href = paths.settings.workspaces();\n    },\n  },\n  \"⌘ + K\": {\n    translationKey: \"apiKeys\",\n    action: () => {\n      window.location.href = paths.settings.apiKeys();\n    },\n  },\n  \"⌘ + L\": {\n    translationKey: \"llmPreferences\",\n    action: () => {\n      window.location.href = paths.settings.llmPreference();\n    },\n  },\n  \"⌘ + Shift + C\": {\n    translationKey: \"chatSettings\",\n    action: () => {\n      window.location.href = paths.settings.chat();\n    },\n  },\n  \"⌘ + Shift + ?\": {\n    translationKey: \"help\",\n    action: () => {\n      window.dispatchEvent(\n        new CustomEvent(KEYBOARD_SHORTCUTS_HELP_EVENT, {\n          detail: { show: true },\n        })\n      );\n    },\n  },\n  F1: {\n    translationKey: \"help\",\n    action: () => {\n      window.dispatchEvent(\n        new CustomEvent(KEYBOARD_SHORTCUTS_HELP_EVENT, {\n          detail: { show: true },\n        })\n      );\n    },\n  },\n  \"⌘ + Shift + L\": {\n    translationKey: \"showLLMSelector\",\n    action: () => {\n      window.dispatchEvent(new Event(TOGGLE_LLM_SELECTOR_EVENT));\n    },\n  },\n};\n\nconst LISTENERS = {};\nconst modifier = isMac ? \"meta\" : \"ctrl\";\nfor (const key in SHORTCUTS) {\n  const listenerKey = key\n    .replace(\"⌘\", modifier)\n    .replaceAll(\" \", \"\")\n    .toLowerCase();\n  LISTENERS[listenerKey] = SHORTCUTS[key].action;\n}\n\n// Convert keyboard event to shortcut key\nfunction getShortcutKey(event) {\n  let key = \"\";\n  if (event.metaKey || event.ctrlKey) key += modifier + \"+\";\n  if (event.shiftKey) key += \"shift+\";\n  if (event.altKey) key += \"alt+\";\n\n  // Handle special keys\n  if (event.key === \",\") key += \",\";\n  // Handle question mark or slash for help shortcut\n  else if (event.key === \"?\" || event.key === \"/\") key += \"?\";\n  else if (event.key === \"Control\")\n    return \"\"; // Ignore Control key by itself\n  else if (event.key === \"Shift\")\n    return \"\"; // Ignore Shift key by itself\n  else key += event.key.toLowerCase();\n  return key;\n}\n\n// Initialize keyboard shortcuts\nexport function initKeyboardShortcuts() {\n  function handleKeyDown(event) {\n    const shortcutKey = getShortcutKey(event);\n    if (!shortcutKey) return;\n\n    const action = LISTENERS[shortcutKey];\n    if (action) {\n      event.preventDefault();\n      action();\n    }\n  }\n\n  window.addEventListener(\"keydown\", handleKeyDown);\n  return () => window.removeEventListener(\"keydown\", handleKeyDown);\n}\n\nfunction useKeyboardShortcuts() {\n  useEffect(() => {\n    // If there is a user and the user is not an admin do not register the event listener\n    // since some of the shortcuts are only available in multi-user mode as admin\n    const user = userFromStorage();\n    if (!!user && user?.role !== \"admin\") return;\n    const cleanup = initKeyboardShortcuts();\n\n    return () => cleanup();\n  }, []);\n  return;\n}\n\nexport function KeyboardShortcutWrapper({ children }) {\n  useKeyboardShortcuts();\n  return children;\n}\n"
  },
  {
    "path": "frontend/src/utils/numbers.js",
    "content": "const Formatter = Intl.NumberFormat(\"en\", { notation: \"compact\" });\n\nexport function numberWithCommas(input) {\n  return input.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n}\n\nexport function nFormatter(input) {\n  return Formatter.format(input);\n}\n\nexport function dollarFormat(input) {\n  return new Intl.NumberFormat(\"en-us\", {\n    style: \"currency\",\n    currency: \"USD\",\n  }).format(input);\n}\n\nexport function toPercentString(input = null, decimals = 0) {\n  if (isNaN(input) || input === null) return \"\";\n  const percentage = Math.round(input * 100);\n  return (\n    (decimals > 0 ? percentage.toFixed(decimals) : percentage.toString()) + \"%\"\n  );\n}\n\nexport function humanFileSize(bytes, si = false, dp = 1) {\n  const thresh = si ? 1000 : 1024;\n\n  if (Math.abs(bytes) < thresh) {\n    return bytes + \" B\";\n  }\n\n  const units = si\n    ? [\"kB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"]\n    : [\"KiB\", \"MiB\", \"GiB\", \"TiB\", \"PiB\", \"EiB\", \"ZiB\", \"YiB\"];\n  let u = -1;\n  const r = 10 ** dp;\n\n  do {\n    bytes /= thresh;\n    ++u;\n  } while (\n    Math.round(Math.abs(bytes) * r) / r >= thresh &&\n    u < units.length - 1\n  );\n\n  return bytes.toFixed(dp) + \" \" + units[u];\n}\n\nexport function milliToHms(milli = 0) {\n  const d = parseFloat(milli) / 1_000.0;\n  var h = Math.floor(d / 3600);\n  var m = Math.floor((d % 3600) / 60);\n  var s = parseFloat((d % 3600.0) % 60);\n\n  var hDisplay = h >= 1 ? h + \"h \" : \"\";\n  var mDisplay = m >= 1 ? m + \"m \" : \"\";\n  var sDisplay = s >= 0.01 ? s.toFixed(2) + \"s\" : \"\";\n  return hDisplay + mDisplay + sDisplay;\n}\n"
  },
  {
    "path": "frontend/src/utils/paths.js",
    "content": "import { API_BASE } from \"./constants\";\n\nfunction applyOptions(path, options = {}) {\n  let updatedPath = path;\n  if (!options || Object.keys(options).length === 0) return updatedPath;\n\n  if (options.search) {\n    const searchParams = new URLSearchParams(options.search);\n    updatedPath += `?${searchParams.toString()}`;\n  }\n  return updatedPath;\n}\n\nexport default {\n  home: () => {\n    return \"/\";\n  },\n  login: (noTry = false) => {\n    return `/login${noTry ? \"?nt=1\" : \"\"}`;\n  },\n  sso: {\n    login: () => {\n      return \"/sso/simple\";\n    },\n  },\n  onboarding: {\n    home: () => {\n      return \"/onboarding\";\n    },\n    survey: () => {\n      return \"/onboarding/survey\";\n    },\n    llmPreference: () => {\n      return \"/onboarding/llm-preference\";\n    },\n    embeddingPreference: () => {\n      return \"/onboarding/embedding-preference\";\n    },\n    vectorDatabase: () => {\n      return \"/onboarding/vector-database\";\n    },\n    userSetup: () => {\n      return \"/onboarding/user-setup\";\n    },\n    dataHandling: () => {\n      return \"/onboarding/data-handling\";\n    },\n  },\n  github: () => {\n    return \"https://github.com/Mintplex-Labs/anything-llm\";\n  },\n  discord: () => {\n    return \"https://discord.com/invite/6UyHPeGZAC\";\n  },\n  docs: () => {\n    return \"https://docs.anythingllm.com\";\n  },\n  chatModes: () => {\n    return \"https://docs.anythingllm.com/features/chat-modes\";\n  },\n  mailToMintplex: () => {\n    return \"mailto:team@mintplexlabs.com\";\n  },\n  hosting: () => {\n    return \"https://my.mintplexlabs.com/aio-checkout?product=anythingllm\";\n  },\n  workspace: {\n    chat: (slug, options = {}) => {\n      return applyOptions(`/workspace/${slug}`, options);\n    },\n    settings: {\n      generalAppearance: (slug) => {\n        return `/workspace/${slug}/settings/general-appearance`;\n      },\n      chatSettings: function (slug, options = {}) {\n        return applyOptions(\n          `/workspace/${slug}/settings/chat-settings`,\n          options\n        );\n      },\n      vectorDatabase: (slug) => {\n        return `/workspace/${slug}/settings/vector-database`;\n      },\n      members: (slug) => {\n        return `/workspace/${slug}/settings/members`;\n      },\n      agentConfig: (slug) => {\n        return `/workspace/${slug}/settings/agent-config`;\n      },\n    },\n    thread: (wsSlug, threadSlug) => {\n      return `/workspace/${wsSlug}/t/${threadSlug}`;\n    },\n  },\n  apiDocs: () => {\n    return `${API_BASE}/docs`;\n  },\n  settings: {\n    users: () => {\n      return `/settings/users`;\n    },\n    invites: () => {\n      return `/settings/invites`;\n    },\n    workspaces: () => {\n      return `/settings/workspaces`;\n    },\n    chats: () => {\n      return \"/settings/workspace-chats\";\n    },\n    llmPreference: () => {\n      return \"/settings/llm-preference\";\n    },\n    transcriptionPreference: () => {\n      return \"/settings/transcription-preference\";\n    },\n    audioPreference: () => {\n      return \"/settings/audio-preference\";\n    },\n    defaultSystemPrompt: () => {\n      return \"/settings/default-system-prompt\";\n    },\n    embedder: {\n      modelPreference: () => \"/settings/embedding-preference\",\n      chunkingPreference: () => \"/settings/text-splitter-preference\",\n    },\n    embeddingPreference: () => {\n      return \"/settings/embedding-preference\";\n    },\n    vectorDatabase: () => {\n      return \"/settings/vector-database\";\n    },\n    security: () => {\n      return \"/settings/security\";\n    },\n    interface: () => {\n      return \"/settings/interface\";\n    },\n    branding: () => {\n      return \"/settings/branding\";\n    },\n    agentSkills: () => {\n      return \"/settings/agents\";\n    },\n    chat: () => {\n      return \"/settings/chat\";\n    },\n    apiKeys: () => {\n      return \"/settings/api-keys\";\n    },\n    systemPromptVariables: () => \"/settings/system-prompt-variables\",\n    logs: () => {\n      return \"/settings/event-logs\";\n    },\n    privacy: () => {\n      return \"/settings/privacy\";\n    },\n    embedChatWidgets: () => {\n      return `/settings/embed-chat-widgets`;\n    },\n    browserExtension: () => {\n      return `/settings/browser-extension`;\n    },\n    mobile: () => {\n      return `/settings/mobile-connections`;\n    },\n    experimental: () => {\n      return `/settings/beta-features`;\n    },\n    mobileConnections: () => {\n      return `/settings/mobile-connections`;\n    },\n  },\n  agents: {\n    builder: () => {\n      return `/settings/agents/builder`;\n    },\n    editAgent: (uuid) => {\n      return `/settings/agents/builder/${uuid}`;\n    },\n  },\n  communityHub: {\n    website: () => {\n      return import.meta.env.DEV\n        ? `http://localhost:5173`\n        : `https://hub.anythingllm.com`;\n    },\n    /**\n     * View more items of a given type on the community hub.\n     * @param {string} type - The type of items to view more of. Should be kebab-case.\n     * @returns {string} The path to view more items of the given type.\n     */\n    viewMoreOfType: function (type) {\n      return `${this.website()}/list/${type}`;\n    },\n    viewItem: function (type, id) {\n      return `${this.website()}/i/${type}/${id}`;\n    },\n    trending: () => {\n      return `/settings/community-hub/trending`;\n    },\n    authentication: () => {\n      return `/settings/community-hub/authentication`;\n    },\n    importItem: (importItemId) => {\n      return `/settings/community-hub/import-item${importItemId ? `?id=${importItemId}` : \"\"}`;\n    },\n    profile: function (username) {\n      if (username) return `${this.website()}/u/${username}`;\n      return `${this.website()}/me`;\n    },\n    noPrivateItems: () => {\n      return \"https://docs.anythingllm.com/community-hub/faq#no-private-items\";\n    },\n  },\n\n  // TODO: Migrate all docs.anythingllm.com links to the new docs.\n  documentation: {\n    mobileIntroduction: () => {\n      return \"https://docs.anythingllm.com/mobile/overview\";\n    },\n    contextWindows: () => {\n      return \"https://docs.anythingllm.com/chatting-with-documents/introduction#you-exceed-the-context-window---what-now\";\n    },\n  },\n\n  experimental: {\n    liveDocumentSync: {\n      manage: () => `/settings/beta-features/live-document-sync/manage`,\n    },\n  },\n};\n"
  },
  {
    "path": "frontend/src/utils/piperTTS/index.js",
    "content": "import showToast from \"../toast\";\n\nexport default class PiperTTSClient {\n  static _instance;\n  voiceId = \"en_US-hfc_female-medium\";\n  worker = null;\n\n  constructor({ voiceId } = { voiceId: null }) {\n    if (PiperTTSClient._instance) {\n      this.voiceId = voiceId !== null ? voiceId : this.voiceId;\n      return PiperTTSClient._instance;\n    }\n\n    this.voiceId = voiceId !== null ? voiceId : this.voiceId;\n    PiperTTSClient._instance = this;\n    return this;\n  }\n\n  #getWorker() {\n    if (!this.worker)\n      this.worker = new Worker(new URL(\"./worker.js\", import.meta.url), {\n        type: \"module\",\n      });\n    return this.worker;\n  }\n\n  /**\n   * Get all available voices for a client\n   * @returns {Promise<import(\"@mintplex-labs/piper-tts-web/dist/types\").Voice[]}>}\n   */\n  static async voices() {\n    const tmpWorker = new Worker(new URL(\"./worker.js\", import.meta.url), {\n      type: \"module\",\n    });\n    tmpWorker.postMessage({ type: \"voices\" });\n    return new Promise((resolve, reject) => {\n      let timeout = null;\n      const handleMessage = (event) => {\n        if (event.data.type !== \"voices\") {\n          console.log(\"PiperTTSWorker debug event:\", event.data);\n          return;\n        }\n        resolve(event.data.voices);\n        tmpWorker.removeEventListener(\"message\", handleMessage);\n        timeout && clearTimeout(timeout);\n        tmpWorker.terminate();\n      };\n\n      timeout = setTimeout(() => {\n        reject(\"TTS Worker timed out.\");\n      }, 30_000);\n      tmpWorker.addEventListener(\"message\", handleMessage);\n    });\n  }\n\n  static async flush() {\n    const tmpWorker = new Worker(new URL(\"./worker.js\", import.meta.url), {\n      type: \"module\",\n    });\n    tmpWorker.postMessage({ type: \"flush\" });\n    return new Promise((resolve, reject) => {\n      let timeout = null;\n      const handleMessage = (event) => {\n        if (event.data.type !== \"flush\") {\n          console.log(\"PiperTTSWorker debug event:\", event.data);\n          return;\n        }\n        resolve(event.data.flushed);\n        tmpWorker.removeEventListener(\"message\", handleMessage);\n        timeout && clearTimeout(timeout);\n        tmpWorker.terminate();\n      };\n\n      timeout = setTimeout(() => {\n        reject(\"TTS Worker timed out.\");\n      }, 30_000);\n      tmpWorker.addEventListener(\"message\", handleMessage);\n    });\n  }\n\n  /**\n   * Runs prediction via webworker so we can get an audio blob back.\n   * @returns {Promise<{blobURL: string|null, error: string|null}>} objectURL blob: type.\n   */\n  async waitForBlobResponse() {\n    return new Promise((resolve) => {\n      let timeout = null;\n      const handleMessage = (event) => {\n        if (event.data.type === \"error\") {\n          this.worker.removeEventListener(\"message\", handleMessage);\n          timeout && clearTimeout(timeout);\n          return resolve({ blobURL: null, error: event.data.message });\n        }\n\n        if (event.data.type !== \"result\") {\n          console.log(\"PiperTTSWorker debug event:\", event.data);\n          return;\n        }\n        resolve({\n          blobURL: URL.createObjectURL(event.data.audio),\n          error: null,\n        });\n        this.worker.removeEventListener(\"message\", handleMessage);\n        timeout && clearTimeout(timeout);\n      };\n\n      timeout = setTimeout(() => {\n        resolve({ blobURL: null, error: \"PiperTTSWorker Worker timed out.\" });\n      }, 30_000);\n      this.worker.addEventListener(\"message\", handleMessage);\n    });\n  }\n\n  async getAudioBlobForText(textToSpeak, voiceId = null) {\n    const primaryWorker = this.#getWorker();\n    primaryWorker.postMessage({\n      type: \"init\",\n      text: String(textToSpeak),\n      voiceId: voiceId ?? this.voiceId,\n      // Don't reference WASM because in the docker image\n      // the user will be connected to internet (mostly)\n      // and it bloats the app size on the frontend or app significantly\n      // and running the docker image fully offline is not an intended use-case unlike the app.\n    });\n\n    const { blobURL, error } = await this.waitForBlobResponse();\n    if (!!error) {\n      showToast(\n        `Could not generate voice prediction. Error: ${error}`,\n        \"error\",\n        { clear: true }\n      );\n      return;\n    }\n\n    return blobURL;\n  }\n}\n"
  },
  {
    "path": "frontend/src/utils/piperTTS/worker.js",
    "content": "import * as TTS from \"@mintplex-labs/piper-tts-web\";\n\n/** @type {import(\"@mintplexlabs/piper-web-tts\").TtsSession | null} */\nlet PIPER_SESSION = null;\n\n/**\n * @typedef PredictionRequest\n * @property {('init')} type\n * @property {string} text - the text to inference on\n * @property {import('@mintplexlabs/piper-web-tts').VoiceId} voiceId - the voiceID key to use.\n * @property {string|null} baseUrl - the base URL to fetch WASMs from.\n */\n/**\n * @typedef PredictionRequestResponse\n * @property {('result')} type\n * @property {Blob} audio - the text to inference on\n */\n\n/**\n * @typedef VoicesRequest\n * @property {('voices')} type\n * @property {string|null} baseUrl - the base URL to fetch WASMs from.\n */\n/**\n * @typedef VoicesRequestResponse\n * @property {('voices')} type\n * @property {[import(\"@mintplex-labs/piper-tts-web/dist/types\")['Voice']]} voices - available voices in array\n */\n\n/**\n * @typedef FlushRequest\n * @property {('flush')} type\n */\n/**\n * @typedef FlushRequestResponse\n * @property {('flush')} type\n * @property {true} flushed\n */\n\n/**\n * Web worker for generating client-side PiperTTS predictions\n * @param {MessageEvent<PredictionRequest | VoicesRequest | FlushRequest>} event - The event object containing the prediction request\n * @returns {Promise<PredictionRequestResponse|VoicesRequestResponse|FlushRequestResponse>}\n */\nasync function main(event) {\n  if (event.data.type === \"voices\") {\n    const stored = await TTS.stored();\n    const voices = await TTS.voices();\n    voices.forEach((voice) => (voice.is_stored = stored.includes(voice.key)));\n\n    self.postMessage({ type: \"voices\", voices });\n    return;\n  }\n\n  if (event.data.type === \"flush\") {\n    await TTS.flush();\n    self.postMessage({ type: \"flush\", flushed: true });\n    return;\n  }\n\n  if (event.data?.type !== \"init\") return;\n  if (!PIPER_SESSION) {\n    PIPER_SESSION = new TTS.TtsSession({\n      voiceId: event.data.voiceId,\n      progress: (e) => self.postMessage(JSON.stringify(e)),\n      logger: (msg) => self.postMessage(msg),\n      ...(!!event.data.baseUrl\n        ? {\n            wasmPaths: {\n              onnxWasm: `${event.data.baseUrl}/piper/ort/`,\n              piperData: `${event.data.baseUrl}/piper/piper_phonemize.data`,\n              piperWasm: `${event.data.baseUrl}/piper/piper_phonemize.wasm`,\n            },\n          }\n        : {}),\n    });\n  }\n\n  if (event.data.voiceId && PIPER_SESSION.voiceId !== event.data.voiceId)\n    PIPER_SESSION.voiceId = event.data.voiceId;\n\n  PIPER_SESSION.predict(event.data.text)\n    .then((res) => {\n      if (res instanceof Blob) {\n        self.postMessage({ type: \"result\", audio: res });\n        return;\n      }\n    })\n    .catch((error) => {\n      self.postMessage({ type: \"error\", message: error.message, error }); // Will be an error.\n    });\n}\n\nself.addEventListener(\"message\", main);\n"
  },
  {
    "path": "frontend/src/utils/request.js",
    "content": "import { AUTH_TOKEN, AUTH_USER } from \"./constants\";\n\n// Sets up the base headers for all authenticated requests so that we are able to prevent\n// basic spoofing since a valid token is required and that cannot be spoofed\nexport function userFromStorage() {\n  const userString = window.localStorage.getItem(AUTH_USER);\n  if (!userString) return null;\n  return safeJsonParse(userString, null);\n}\n\nexport function baseHeaders(providedToken = null) {\n  const token = providedToken || window.localStorage.getItem(AUTH_TOKEN);\n  return {\n    Authorization: token ? `Bearer ${token}` : null,\n  };\n}\n\nexport function safeJsonParse(jsonString, fallback = null) {\n  try {\n    if (jsonString === null || jsonString === undefined) return fallback;\n    return JSON.parse(jsonString);\n  } catch {}\n  return fallback;\n}\n"
  },
  {
    "path": "frontend/src/utils/session.js",
    "content": "import { API_BASE } from \"./constants\";\nimport { baseHeaders } from \"./request\";\n\n// Checks current localstorage and validates the session based on that.\nexport default async function validateSessionTokenForUser() {\n  const isValidSession = await fetch(`${API_BASE}/system/check-token`, {\n    method: \"GET\",\n    cache: \"default\",\n    headers: baseHeaders(),\n  })\n    .then((res) => res.status === 200)\n    .catch(() => false);\n\n  return isValidSession;\n}\n"
  },
  {
    "path": "frontend/src/utils/toast.js",
    "content": "import { toast } from \"react-toastify\";\n\n// Additional Configs (opts)\n// You can also pass valid ReactToast params to override the defaults.\n// clear: false, // Will dismiss all visible toasts before rendering next toast\nconst showToast = (message, type = \"default\", opts = {}) => {\n  const theme = localStorage?.getItem(\"theme\") || \"default\";\n  const options = {\n    position: \"bottom-center\",\n    autoClose: 5000,\n    hideProgressBar: false,\n    closeOnClick: true,\n    pauseOnHover: true,\n    draggable: true,\n    theme: theme === \"default\" ? \"dark\" : \"light\",\n    ...opts,\n  };\n\n  if (opts?.clear === true) toast.dismiss();\n\n  switch (type) {\n    case \"success\":\n      toast.success(message, options);\n      break;\n    case \"error\":\n      toast.error(message, options);\n      break;\n    case \"info\":\n      toast.info(message, options);\n      break;\n    case \"warning\":\n      toast.warn(message, options);\n      break;\n    default:\n      toast(message, options);\n  }\n};\n\nexport default showToast;\n"
  },
  {
    "path": "frontend/src/utils/types.js",
    "content": "export function castToType(key, value) {\n  const definitions = {\n    openAiTemp: {\n      cast: (value) => Number(value),\n    },\n    openAiHistory: {\n      cast: (value) => Number(value),\n    },\n    similarityThreshold: {\n      cast: (value) => parseFloat(value),\n    },\n    topN: {\n      cast: (value) => Number(value),\n    },\n  };\n\n  if (!definitions.hasOwnProperty(key)) return value;\n  return definitions[key].cast(value);\n}\n"
  },
  {
    "path": "frontend/src/utils/username.js",
    "content": "/**\n * Unix-style username validation utilities\n *\n * Requirements:\n * - 2-32 characters long\n * - Must start with a lowercase letter\n * - Can contain lowercase letters, digits, underscores, hyphens, @ signs, and periods\n */\n\nexport const USERNAME_REGEX = /^[a-z][a-z0-9._@-]*$/;\nexport const USERNAME_MIN_LENGTH = 2;\nexport const USERNAME_MAX_LENGTH = 32;\n\n/**\n * HTML5 pattern attribute for username inputs (without ^ and $)\n */\nexport const USERNAME_PATTERN = \"[a-z][a-z0-9._@-]*\";\n"
  },
  {
    "path": "frontend/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n  darkMode: \"class\",\n  content: {\n    relative: true,\n    files: [\n      \"./src/components/**/*.{js,jsx}\",\n      \"./src/hooks/**/*.js\",\n      \"./src/models/**/*.js\",\n      \"./src/pages/**/*.{js,jsx}\",\n      \"./src/utils/**/*.js\",\n      \"./src/*.jsx\",\n      \"./index.html\",\n      \"./node_modules/@tremor/**/*.{js,ts,jsx,tsx}\"\n    ]\n  },\n  theme: {\n    extend: {\n      rotate: {\n        \"270\": \"270deg\",\n        \"360\": \"360deg\"\n      },\n      colors: {\n        \"black-900\": \"#141414\",\n        accent: \"#3D4147\",\n        \"sidebar-button\": \"#31353A\",\n        sidebar: \"#25272C\",\n        \"historical-msg-system\": \"rgba(255, 255, 255, 0.05);\",\n        \"historical-msg-user\": \"#2C2F35\",\n        outline: \"#4E5153\",\n        \"primary-button\": \"var(--theme-button-primary)\",\n        \"cta-button\": \"var(--theme-button-cta)\",\n        secondary: \"#2C2F36\",\n        \"dark-input\": \"#18181B\",\n        \"mobile-onboarding\": \"#2C2F35\",\n        \"dark-highlight\": \"#1C1E21\",\n        \"dark-text\": \"#222628\",\n        description: \"#D2D5DB\",\n        \"x-button\": \"#9CA3AF\",\n        royalblue: \"#065986\",\n        purple: \"#4A1FB8\",\n        magenta: \"#9E165F\",\n        danger: \"#F04438\",\n        error: \"#B42318\",\n        warn: \"#854708\",\n        success: \"#05603A\",\n        darker: \"#F4F4F4\",\n        teal: \"#0BA5EC\",\n\n        // Generic theme colors\n        theme: {\n          bg: {\n            primary: 'var(--theme-bg-primary)',\n            secondary: 'var(--theme-bg-secondary)',\n            sidebar: 'var(--theme-bg-sidebar)',\n            container: 'var(--theme-bg-container)',\n            chat: 'var(--theme-bg-chat)',\n            \"chat-input\": 'var(--theme-bg-chat-input)',\n            \"popup-menu\": 'var(--theme-popup-menu-bg)',\n          },\n          text: {\n            primary: 'var(--theme-text-primary)',\n            secondary: 'var(--theme-text-secondary)',\n            placeholder: 'var(--theme-placeholder)',\n          },\n          sidebar: {\n            item: {\n              default: 'var(--theme-sidebar-item-default)',\n              selected: 'var(--theme-sidebar-item-selected)',\n              hover: 'var(--theme-sidebar-item-hover)',\n            },\n            subitem: {\n              default: 'var(--theme-sidebar-subitem-default)',\n              selected: 'var(--theme-sidebar-subitem-selected)',\n              hover: 'var(--theme-sidebar-subitem-hover)',\n            },\n            footer: {\n              icon: 'var(--theme-sidebar-footer-icon)',\n              'icon-hover': 'var(--theme-sidebar-footer-icon-hover)',\n            },\n            border: 'var(--theme-sidebar-border)',\n          },\n          \"chat-input\": {\n            border: 'var(--theme-chat-input-border)',\n          },\n          \"action-menu\": {\n            bg: 'var(--theme-action-menu-bg)',\n            \"item-hover\": 'var(--theme-action-menu-item-hover)',\n          },\n          settings: {\n            input: {\n              bg: 'var(--theme-settings-input-bg)',\n              active: 'var(--theme-settings-input-active)',\n              placeholder: 'var(--theme-settings-input-placeholder)',\n              text: 'var(--theme-settings-input-text)',\n            }\n          },\n          modal: {\n            border: 'var(--theme-modal-border)',\n          },\n          \"file-picker\": {\n            hover: 'var(--theme-file-picker-hover)',\n          },\n          attachment: {\n            bg: 'var(--theme-attachment-bg)',\n            'error-bg': 'var(--theme-attachment-error-bg)',\n            'success-bg': 'var(--theme-attachment-success-bg)',\n            text: 'var(--theme-attachment-text)',\n            'text-secondary': 'var(--theme-attachment-text-secondary)',\n            'icon': 'var(--theme-attachment-icon)',\n            'icon-spinner': 'var(--theme-attachment-icon-spinner)',\n            'icon-spinner-bg': 'var(--theme-attachment-icon-spinner-bg)',\n          },\n          home: {\n            text: 'var(--theme-home-text)',\n            \"text-secondary\": 'var(--theme-home-text-secondary)',\n            \"bg-card\": 'var(--theme-home-bg-card)',\n            \"bg-button\": 'var(--theme-home-bg-button)',\n            border: 'var(--theme-home-border)',\n            \"button-primary\": 'var(--theme-home-button-primary)',\n            \"button-primary-hover\": 'var(--theme-home-button-primary-hover)',\n            \"button-secondary\": 'var(--theme-home-button-secondary)',\n            \"button-secondary-hover\": 'var(--theme-home-button-secondary-hover)',\n            \"button-secondary-text\": 'var(--theme-home-button-secondary-text)',\n            \"button-secondary-hover-text\": 'var(--theme-home-button-secondary-hover-text)',\n            \"button-secondary-border\": 'var(--theme-home-button-secondary-border)',\n            \"button-secondary-border-hover\": 'var(--theme-home-button-secondary-border-hover)',\n            \"update-card-bg\": 'var(--theme-home-update-card-bg)',\n            \"update-card-hover\": 'var(--theme-home-update-card-hover)',\n            \"update-source\": 'var(--theme-home-update-source)',\n          },\n          checklist: {\n            \"item-bg\": 'var(--theme-checklist-item-bg)',\n            \"item-bg-hover\": 'var(--theme-checklist-item-bg-hover)',\n            \"item-text\": 'var(--theme-checklist-item-text)',\n            \"item-completed-bg\": 'var(--theme-checklist-item-completed-bg)',\n            \"item-completed-text\": 'var(--theme-checklist-item-completed-text)',\n            \"item-hover\": 'var(--theme-checklist-item-hover)',\n            \"checkbox-border\": 'var(--theme-checklist-checkbox-border)',\n            \"checkbox-fill\": 'var(--theme-checklist-checkbox-fill)',\n            \"checkbox-text\": 'var(--theme-checklist-checkbox-text)',\n            \"button-border\": 'var(--theme-checklist-button-border)',\n            \"button-text\": 'var(--theme-checklist-button-text)',\n            \"button-hover-bg\": 'var(--theme-checklist-button-hover-bg)',\n            \"button-hover-border\": 'var(--theme-checklist-button-hover-border)',\n          },\n          button: {\n            text: 'var(--theme-button-text)',\n            'code-hover-text': 'var(--theme-button-code-hover-text)',\n            'code-hover-bg': 'var(--theme-button-code-hover-bg)',\n            'disable-hover-text': 'var(--theme-button-disable-hover-text)',\n            'disable-hover-bg': 'var(--theme-button-disable-hover-bg)',\n            'delete-hover-text': 'var(--theme-button-delete-hover-text)',\n            'delete-hover-bg': 'var(--theme-button-delete-hover-bg)',\n          },\n        },\n      },\n      backgroundImage: {\n        \"preference-gradient\":\n          \"linear-gradient(180deg, #5A5C63 0%, rgba(90, 92, 99, 0.28) 100%);\",\n        \"chat-msg-user-gradient\":\n          \"linear-gradient(180deg, #3D4147 0%, #2C2F35 100%);\",\n        \"selected-preference-gradient\":\n          \"linear-gradient(180deg, #313236 0%, rgba(63.40, 64.90, 70.13, 0) 100%);\",\n        \"main-gradient\": \"linear-gradient(180deg, #3D4147 0%, #2C2F35 100%)\",\n        \"modal-gradient\": \"linear-gradient(180deg, #3D4147 0%, #2C2F35 100%)\",\n        \"sidebar-gradient\": \"linear-gradient(90deg, #5B616A 0%, #3F434B 100%)\",\n        \"login-gradient\": \"linear-gradient(180deg, #3D4147 0%, #2C2F35 100%)\",\n        \"menu-item-gradient\":\n          \"linear-gradient(90deg, #3D4147 0%, #2C2F35 100%)\",\n        \"menu-item-selected-gradient\":\n          \"linear-gradient(90deg, #5B616A 0%, #3F434B 100%)\",\n        \"workspace-item-gradient\":\n          \"linear-gradient(90deg, #3D4147 0%, #2C2F35 100%)\",\n        \"workspace-item-selected-gradient\":\n          \"linear-gradient(90deg, #5B616A 0%, #3F434B 100%)\",\n        \"switch-selected\": \"linear-gradient(146deg, #5B616A 0%, #3F434B 100%)\"\n      },\n      fontFamily: {\n        sans: [\n          \"plus-jakarta-sans\",\n          \"ui-sans-serif\",\n          \"system-ui\",\n          \"-apple-system\",\n          \"BlinkMacSystemFont\",\n          '\"Segoe UI\"',\n          \"Roboto\",\n          '\"Helvetica Neue\"',\n          \"Arial\",\n          '\"Noto Sans\"',\n          \"sans-serif\",\n          '\"Apple Color Emoji\"',\n          '\"Segoe UI Emoji\"',\n          '\"Segoe UI Symbol\"',\n          '\"Noto Color Emoji\"'\n        ]\n      },\n      animation: {\n        sweep: \"sweep 0.5s ease-in-out\",\n        \"pulse-glow\": \"pulse-glow 1.5s infinite\",\n        'fade-in': 'fade-in 0.3s ease-out',\n        'slide-up': 'slide-up 0.4s ease-out forwards',\n        'bounce-subtle': 'bounce-subtle 2s ease-in-out infinite'\n      },\n      keyframes: {\n        sweep: {\n          \"0%\": { transform: \"scaleX(0)\", transformOrigin: \"bottom left\" },\n          \"100%\": { transform: \"scaleX(1)\", transformOrigin: \"bottom left\" }\n        },\n        fadeIn: {\n          \"0%\": { opacity: 0 },\n          \"100%\": { opacity: 1 }\n        },\n        fadeOut: {\n          \"0%\": { opacity: 1 },\n          \"100%\": { opacity: 0 }\n        },\n        \"pulse-glow\": {\n          \"0%\": {\n            opacity: 1,\n            transform: \"scale(1)\",\n            boxShadow: \"0 0 0 rgba(255, 255, 255, 0.0)\",\n            backgroundColor: \"rgba(255, 255, 255, 0.0)\"\n          },\n          \"50%\": {\n            opacity: 1,\n            transform: \"scale(1.1)\",\n            boxShadow: \"0 0 15px rgba(255, 255, 255, 0.2)\",\n            backgroundColor: \"rgba(255, 255, 255, 0.1)\"\n          },\n          \"100%\": {\n            opacity: 1,\n            transform: \"scale(1)\",\n            boxShadow: \"0 0 0 rgba(255, 255, 255, 0.0)\",\n            backgroundColor: \"rgba(255, 255, 255, 0.0)\"\n          }\n        },\n        'fade-in': {\n          '0%': { opacity: '0' },\n          '100%': { opacity: '1' }\n        },\n        'slide-up': {\n          '0%': { transform: 'translateY(10px)', opacity: '0' },\n          '100%': { transform: 'translateY(0)', opacity: '1' }\n        },\n        'bounce-subtle': {\n          '0%, 100%': { transform: 'translateY(0)' },\n          '50%': { transform: 'translateY(-2px)' }\n        }\n      }\n    }\n  },\n  variants: {\n    extend: {\n      backgroundColor: ['light'],\n      textColor: ['light'],\n    }\n  },\n  // Required for rechart styles to show since they can be rendered dynamically and will be tree-shaken if not safe-listed.\n  safelist: [\n    {\n      pattern:\n        /^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,\n      variants: [\"hover\", \"ui-selected\"]\n    },\n    {\n      pattern:\n        /^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,\n      variants: [\"hover\", \"ui-selected\"]\n    },\n    {\n      pattern:\n        /^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,\n      variants: [\"hover\", \"ui-selected\"]\n    },\n    {\n      pattern:\n        /^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/\n    },\n    {\n      pattern:\n        /^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/\n    },\n    {\n      pattern:\n        /^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/\n    }\n  ],\n  plugins: [\n    function ({ addVariant }) {\n      addVariant('light', '.light &') // Add the `light:` variant\n      addVariant('pwa', '.pwa &') // Add the `pwa:` variant\n    },\n  ]\n}\n"
  },
  {
    "path": "frontend/vite.config.js",
    "content": "import { defineConfig } from \"vite\"\nimport { fileURLToPath, URL } from \"url\"\nimport postcss from \"./postcss.config.js\"\nimport react from \"@vitejs/plugin-react\"\nimport dns from \"dns\"\nimport { visualizer } from \"rollup-plugin-visualizer\"\n\ndns.setDefaultResultOrder(\"verbatim\")\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  assetsInclude: [\n    './public/piper/ort-wasm-simd-threaded.wasm',\n    './public/piper/piper_phonemize.wasm',\n    './public/piper/piper_phonemize.data',\n  ],\n  worker: {\n    format: 'es'\n  },\n  server: {\n    port: 3000,\n    host: \"localhost\"\n  },\n  define: {\n    \"process.env\": process.env\n  },\n  css: {\n    postcss\n  },\n  plugins: [\n    react(),\n    visualizer({\n      template: \"treemap\", // or sunburst\n      open: false,\n      gzipSize: true,\n      brotliSize: true,\n      filename: \"bundleinspector.html\" // will be saved in project's root\n    })\n  ],\n  resolve: {\n    alias: [\n      {\n        find: \"@\",\n        replacement: fileURLToPath(new URL(\"./src\", import.meta.url))\n      },\n      {\n        process: \"process/browser\",\n        stream: \"stream-browserify\",\n        zlib: \"browserify-zlib\",\n        util: \"util\",\n        find: /^~.+/,\n        replacement: (val) => {\n          return val.replace(/^~/, \"\")\n        }\n      }\n    ]\n  },\n  build: {\n    rollupOptions: {\n      output: {\n        // These settings ensure the primary JS and CSS file references are always index.{js,css}\n        // so we can SSR the index.html as text response from server/index.js without breaking references each build.\n        entryFileNames: 'index.js',\n        assetFileNames: (assetInfo) => {\n          if (assetInfo.name === 'index.css') return `index.css`;\n          return assetInfo.name;\n        },\n      },\n      external: [\n        // Reduces transformation time by 50% and we don't even use this variant, so we can ignore.\n        /@phosphor-icons\\/react\\/dist\\/ssr/,\n      ]\n    },\n    commonjsOptions: {\n      transformMixedEsModules: true\n    }\n  },\n  optimizeDeps: {\n    include: [\"@mintplex-labs/piper-tts-web\"],\n    esbuildOptions: {\n      define: {\n        global: \"globalThis\"\n      },\n      plugins: []\n    }\n  }\n})\n"
  },
  {
    "path": "locales/README.fa-IR.md",
    "content": "<a name=\"readme-top\"></a>\n\n<p align=\"center\">\n  <a href=\"https://anythingllm.com\"><img src=\"https://github.com/Mintplex-Labs/anything-llm/blob/master/images/wordmark.png?raw=true\" alt=\"AnythingLLM logo\"></a>\n</p>\n\n<div align='center'>\n<a href=\"https://trendshift.io/repositories/2415\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/2415\" alt=\"Mintplex-Labs%2Fanything-llm | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n</div>\n\n<p align=\"center\" dir=\"rtl\">\n    <b>AnythingLLM:</b> اپلیکیشن همه‌کاره هوش مصنوعی که دنبالش بودید.<br />\n    با اسناد خود چت کنید، از عامل‌های هوش مصنوعی استفاده کنید، با قابلیت پیکربندی بالا، چند کاربره، و بدون نیاز به تنظیمات پیچیده.\n</p>\n\n<p align=\"center\">\n  <a href=\"https://discord.gg/6UyHPeGZAC\" target=\"_blank\">\n      <img src=\"https://img.shields.io/badge/chat-mintplex_labs-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAH1UExURQAAAP////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////r6+ubn5+7u7/3+/v39/enq6urq6/v7+97f39rb26eoqT1BQ0pOT4+Rkuzs7cnKykZKS0NHSHl8fdzd3ejo6UxPUUBDRdzc3RwgIh8jJSAkJm5xcvHx8aanqB4iJFBTVezt7V5hYlJVVuLj43p9fiImKCMnKZKUlaaoqSElJ21wcfT09O3u7uvr6zE0Nr6/wCUpK5qcnf7+/nh7fEdKTHx+f0tPUOTl5aipqiouMGtubz5CRDQ4OsTGxufn515hY7a3uH1/gXBydIOFhlVYWvX29qaoqCQoKs7Pz/Pz87/AwUtOUNfY2dHR0mhrbOvr7E5RUy8zNXR2d/f39+Xl5UZJSx0hIzQ3Odra2/z8/GlsbaGjpERHSezs7L/BwScrLTQ4Odna2zM3Obm7u3x/gKSmp9jZ2T1AQu/v71pdXkVISr2+vygsLiInKTg7PaOlpisvMcXGxzk8PldaXPLy8u7u7rm6u7S1tsDBwvj4+MPExbe4ueXm5s/Q0Kyf7ewAAAAodFJOUwAABClsrNjx/QM2l9/7lhmI6jTB/kA1GgKJN+nea6vy/MLZQYeVKK3rVA5tAAAAAWJLR0QB/wIt3gAAAAd0SU1FB+cKBAAmMZBHjXIAAAISSURBVDjLY2CAAkYmZhZWNnYODnY2VhZmJkYGVMDIycXNw6sBBbw8fFycyEoYGfkFBDVQgKAAPyMjQl5IWEQDDYgIC8FUMDKKsmlgAWyiEBWMjGJY5YEqxMAqGMWFNXAAYXGgAkYJSQ2cQFKCkYFRShq3AmkpRgYJbghbU0tbB0Tr6ukbgGhDI10gySfBwCwDUWBsYmpmDqQtLK2sbTQ0bO3sHYA8GWYGWWj4WTs6Obu4ami4OTm7exhqeHp5+4DCVJZBDmqdr7ufn3+ArkZgkJ+fU3CIRmgYWFiOARYGvo5OQUHhEUAFTkF+kVHRsLBgkIeyYmLjwoOc4hMSk5JTnINS06DC8gwcEEZ6RqZGlpOfc3ZObl5+gZ+TR2ERWFyBQQFMF5eklmqUpQb5+ReU61ZUOvkFVVXXQBSAraitq29o1GiKcfLzc29u0mjxBzq0tQ0kww5xZHtHUGeXhkZhdxBYgZ4d0LI6c4gjwd7siQQraOp1AivQ6CuAKZCDBBRQQQNQgUb/BGf3cqCCiZOcnCe3QQIKHNRTpk6bDgpZjRkzg3pBQTBrdtCcuZCgluAD0vPmL1gIdvSixUuWgqNs2YJ+DUhkEYxuggkGmOQUcckrioPTJCOXEnZ5JS5YslbGnuyVERlDDFvGEUPOWvwqaH6RVkHKeuDMK6SKnHlVhTgx8jeTmqy6Eij7K6nLqiGyPwChsa1MUrnq1wAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0xMC0wNFQwMDozODo0OSswMDowMB9V0a8AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMTAtMDRUMDA6Mzg6NDkrMDA6MDBuCGkTAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDIzLTEwLTA0VDAwOjM4OjQ5KzAwOjAwOR1IzAAAAABJRU5ErkJggg==\" alt=\"Discord\">\n  </a> |\n  <a href=\"https://github.com/Mintplex-Labs/anything-llm/blob/master/LICENSE\" target=\"_blank\">\n      <img src=\"https://img.shields.io/static/v1?label=license&message=MIT&color=white\" alt=\"License\">\n  </a> |\n  <a href=\"https://docs.anythingllm.com\" target=\"_blank\">\n    Docs\n  </a> |\n   <a href=\"https://my.mintplexlabs.com/aio-checkout?product=anythingllm\" target=\"_blank\">\n    Hosted Instance\n  </a>\n</p>\n\n<p align=\"center\" dir=\"rtl\">\n  <b>English</b> · <a href='./locales/README.zh-CN.md'>简体中文</a> · <a href='./locales/README.ja-JP.md'>日本語</a> · <b>فارسی</b>\n</p>\n\n<p align=\"center\" dir=\"rtl\">\n👈 AnythingLLM برای دسکتاپ (مک، ویندوز و لینوکس)! <a href=\"https://anythingllm.com/download\" target=\"_blank\">دانلود کنید</a>\n</p>\n\n<div dir=\"rtl\">\nیک اپلیکیشن کامل که به شما امکان می‌دهد هر سند، منبع یا محتوایی را به زمینه‌ای تبدیل کنید که هر LLM می‌تواند در حین گفتگو به عنوان مرجع از آن استفاده کند. این برنامه به شما اجازه می‌دهد LLM یا پایگاه داده برداری مورد نظر خود را انتخاب کنید و همچنین از مدیریت چند کاربره و مجوزها پشتیبانی می‌کند.\n</div>\n\n![Chatting](https://github.com/Mintplex-Labs/anything-llm/releases/download/v1.11.2/AnythingLLM720p.gif)\n\n<details>\n<summary><kbd>دموی ویدیویی را تماشا کنید!</kbd></summary>\n\n[![Watch the video](/images/youtube.png)](https://youtu.be/f95rGD9trL0)\n\n</details>\n<div dir=\"rtl\">\n\n### نمای کلی محصول\n\nAnythingLLM اپلیکیشن همه‌کاره هوش مصنوعی است که به دنبال آن بودید. AnythingLLM شامل همه چیزهایی است که برای ساخت یک ChatGPT خصوصی بدون سازش با استفاده از ارائه‌دهندگان LLM محلی یا ابری مورد علاقه خود نیاز دارید. AnythingLLM بسیار قابل پیکربندی است، اما همه چیزهایی که برای شروع کار نیاز دارید از جمله عامل‌های داخلی، پشتیبانی چند کاربره، پایگاه‌های داده برداری، خطوط لوله دریافت اسناد و موارد دیگر را به صورت آماده ارائه می‌دهد.\n\nAnythingLLM همچنین از چندین کاربر پشتیبانی می‌کند که می‌توانید دسترسی و تجربه هر کاربر را بدون به خطر انداختن امنیت یا حریم خصوصی نمونه یا مالکیت فکری خود کنترل کنید.\n\n</div>\n<div dir=\"rtl\">\n\n## ویژگی‌های جذاب AnythingLLM\n\n- 🆕 [**عامل‌های هوش مصنوعی سفارشی**](https://docs.anythingllm.com/agent/custom/introduction)\n- 🖼️ **پشتیبانی از چند مدل (هم LLMهای متن‌باز و هم تجاری!)**\n- 👤 پشتیبانی از چند کاربر و سیستم مجوزها _فقط در نسخه Docker_\n- 🦾 عامل‌ها در فضای کاری شما (مرور وب، اجرای کد و غیره)\n- 💬 [ویجت چت قابل جاسازی سفارشی برای وب‌سایت شما](../embed/README.md) _فقط در نسخه Docker_\n- 📖 پشتیبانی از انواع مختلف سند (PDF، TXT، DOCX و غیره)\n- رابط کاربری ساده چت با قابلیت کشیدن و رها کردن و استنادهای واضح\n- ۱۰۰٪ آماده استقرار در فضای ابری\n- سازگار با تمام [ارائه‌دهندگان محبوب LLM متن‌باز و تجاری](#supported-llms-embedder-models-speech-models-and-vector-databases)\n- دارای اقدامات داخلی صرفه‌جویی در هزینه و زمان برای مدیریت اسناد بسیار بزرگ در مقایسه با سایر رابط‌های کاربری چت\n- API کامل توسعه‌دهنده برای یکپارچه‌سازی‌های سفارشی!\n- و موارد بیشتر... نصب کنید و کشف کنید!\n\n### LLMها، مدل‌های Embedder، مدل‌های گفتاری و پایگاه‌های داده برداری پشتیبانی شده\n\n**مدل‌های زبانی بزرگ (LLMs):**\n\n- [Any open-source llama.cpp compatible model](/server/storage/models/README.md#text-generation-llm-selection)\n- [OpenAI](https://openai.com)\n- [OpenAI (Generic)](https://openai.com)\n- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)\n- [AWS Bedrock](https://aws.amazon.com/bedrock/)\n- [Anthropic](https://www.anthropic.com/)\n- [NVIDIA NIM (chat models)](https://build.nvidia.com/explore/discover)\n- [Google Gemini Pro](https://ai.google.dev/)\n- [Hugging Face (chat models)](https://huggingface.co/)\n- [Ollama (chat models)](https://ollama.ai/)\n- [LM Studio (all models)](https://lmstudio.ai)\n- [LocalAi (all models)](https://localai.io/)\n- [Together AI (chat models)](https://www.together.ai/)\n- [Fireworks AI (chat models)](https://fireworks.ai/)\n- [Perplexity (chat models)](https://www.perplexity.ai/)\n- [OpenRouter (chat models)](https://openrouter.ai/)\n- [DeepSeek (chat models)](https://deepseek.com/)\n- [Mistral](https://mistral.ai/)\n- [Groq](https://groq.com/)\n- [Cohere](https://cohere.com/)\n- [KoboldCPP](https://github.com/LostRuins/koboldcpp)\n- [LiteLLM](https://github.com/BerriAI/litellm)\n- [Text Generation Web UI](https://github.com/oobabooga/text-generation-webui)\n- [Apipie](https://apipie.ai/)\n- [xAI](https://x.ai/)\n- [Z.AI (chat models)](https://z.ai/model-api)\n- [Novita AI (chat models)](https://novita.ai/model-api/product/llm-api?utm_source=github_anything-llm&utm_medium=github_readme&utm_campaign=link)\n- [PPIO](https://ppinfra.com?utm_source=github_anything-llm)\n- [Docker Model Runner](https://docs.docker.com/ai/model-runner/)\n- [PrivateModeAI (chat models)](https://privatemode.ai/)\n- [SambaNova Cloud (chat models)](https://cloud.sambanova.ai/)\n- [Lemonade by AMD](https://lemonade-server.ai)\n\n<div dir=\"rtl\">\n\n**مدل‌های Embedder:**\n\n- [AnythingLLM Native Embedder](/server/storage/models/README.md) (پیش‌فرض)\n- [OpenAI](https://openai.com)\n- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)\n- [LocalAi (همه)](https://localai.io/)\n- [Ollama (همه)](https://ollama.ai/)\n- [LM Studio (همه)](https://lmstudio.ai)\n- [Cohere](https://cohere.com/)\n\n**مدل‌های رونویسی صوتی:**\n\n- [AnythingLLM Built-in](https://github.com/Mintplex-Labs/anything-llm/tree/master/server/storage/models#audiovideo-transcription) (پیش‌فرض)\n- [OpenAI](https://openai.com/)\n\n**پشتیبانی TTS (تبدیل متن به گفتار):**\n\n- امکانات داخلی مرورگر (پیش‌فرض)\n- [PiperTTSLocal - اجرا در مرورگر](https://github.com/rhasspy/piper)\n- [OpenAI TTS](https://platform.openai.com/docs/guides/text-to-speech/voice-options)\n- [ElevenLabs](https://elevenlabs.io/)\n- هر سرویس TTS سازگار با OpenAI\n\n**پشتیبانی STT (تبدیل گفتار به متن):**\n\n- امکانات داخلی مرورگر (پیش‌فرض)\n\n**پایگاه‌های داده برداری:**\n\n- [LanceDB](https://github.com/lancedb/lancedb) (پیش‌فرض)\n- [PGVector](https://github.com/pgvector/pgvector)\n- [Astra DB](https://www.datastax.com/products/datastax-astra)\n- [Pinecone](https://pinecone.io)\n- [Chroma](https://trychroma.com)\n- [Weaviate](https://weaviate.io)\n- [Qdrant](https://qdrant.tech)\n- [Milvus](https://milvus.io)\n- [Zilliz](https://zilliz.com)\n\n### نمای کلی فنی\n\nاین مخزن شامل سه بخش اصلی است:\n\n- `frontend`: یک رابط کاربری viteJS + React که می‌توانید برای ایجاد و مدیریت آسان تمام محتوای قابل استفاده توسط LLM اجرا کنید.\n- `server`: یک سرور NodeJS express برای مدیریت تمام تعاملات و انجام مدیریت vectorDB و تعاملات LLM.\n- `collector`: سرور NodeJS express که اسناد را از رابط کاربری پردازش و تجزیه می‌کند.\n- `docker`: دستورالعمل‌های Docker و فرآیند ساخت + اطلاعات برای ساخت از منبع.\n- `embed`: زیرماژول برای تولید و ایجاد [ویجت قابل جاسازی وب](https://github.com/Mintplex-Labs/anythingllm-embed).\n- `browser-extension`: زیرماژول برای [افزونه مرورگر کروم](https://github.com/Mintplex-Labs/anythingllm-extension).\n\n</div>\n\n## 🛳 میزبانی شخصی\n\n<div dir=\"rtl\">\n\nMintplex Labs و جامعه کاربران، روش‌ها، اسکریپت‌ها و قالب‌های متعددی را برای اجرای AnythingLLM به صورت محلی نگهداری می‌کنند. برای مطالعه نحوه استقرار در محیط مورد نظر خود یا استقرار خودکار، به جدول زیر مراجعه کنید.\n\n</div>\n\n| Docker                                           | AWS                                     | GCP                                     | Digital Ocean                                  | Render.com                                           |\n| ------------------------------------------------ | --------------------------------------- | --------------------------------------- | ---------------------------------------------- | ---------------------------------------------------- |\n| [![Deploy on Docker][docker-btn]][docker-deploy] | [![Deploy on AWS][aws-btn]][aws-deploy] | [![Deploy on GCP][gcp-btn]][gcp-deploy] | [![Deploy on DigitalOcean][do-btn]][do-deploy] | [![Deploy on Render.com][render-btn]][render-deploy] |\n\n| Railway                                             | RepoCloud                                                 | Elestio                                             |\n| --------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------- |\n| [![Deploy on Railway][railway-btn]][railway-deploy] | [![Deploy on RepoCloud][repocloud-btn]][repocloud-deploy] | [![Deploy on Elestio][elestio-btn]][elestio-deploy] |\n\n<div dir=\"rtl\">\n\n[یا راه‌اندازی نمونه تولیدی AnythingLLM بدون Docker →](../BARE_METAL.md)\n\n## راه‌اندازی برای توسعه\n\n- `yarn setup` برای پر کردن فایل‌های `.env` مورد نیاز در هر بخش از برنامه (از ریشه مخزن).\n  - قبل از ادامه، آن‌ها را پر کنید. اطمینان حاصل کنید که `server/.env.development` پر شده است، در غیر این صورت همه چیز درست کار نخواهد کرد.\n- `yarn dev:server` برای راه‌اندازی سرور به صورت محلی (از ریشه مخزن).\n- `yarn dev:frontend` برای راه‌اندازی فرانت‌اند به صورت محلی (از ریشه مخزن).\n- `yarn dev:collector` برای اجرای جمع‌کننده اسناد (از ریشه مخزن).\n\n[درباره اسناد بیشتر بدانید](../server/storage/documents/DOCUMENTS.md)\n\n## تله‌متری و حریم خصوصی\n\nAnythingLLM توسط Mintplex Labs Inc دارای ویژگی تله‌متری است که اطلاعات استفاده ناشناس را جمع‌آوری می‌کند.\n\n<details>\n<summary><kbd>اطلاعات بیشتر درباره تله‌متری و حریم خصوصی AnythingLLM</kbd></summary>\n\n### چرا؟\n\n<div dir=\"rtl\">\nما از این اطلاعات برای درک نحوه استفاده از AnythingLLM، اولویت‌بندی کار روی ویژگی‌های جدید و رفع اشکالات، و بهبود عملکرد و پایداری AnythingLLM استفاده می‌کنیم.\n</div>\n\n### غیرفعال کردن\n\n<div dir=\"rtl\">\nبرای غیرفعال کردن تله‌متری، `DISABLE_TELEMETRY` را در تنظیمات .env سرور یا داکر خود روی \"true\" تنظیم کنید. همچنین می‌توانید این کار را در برنامه با رفتن به نوار کناری > `حریم خصوصی` و غیرفعال کردن تله‌متری انجام دهید.\n</div>\n\n### دقیقاً چه چیزی را ردیابی می‌کنید؟\n\n<div dir=\"rtl\">\nما فقط جزئیات استفاده‌ای را که به ما در تصمیم‌گیری‌های محصول و نقشه راه کمک می‌کند، ردیابی می‌کنیم، به طور خاص:\n\n- نوع نصب شما (Docker یا Desktop)\n- زمانی که سندی اضافه یا حذف می‌شود. هیچ اطلاعاتی _درباره_ سند نداریم. فقط رویداد ثبت می‌شود.\n- نوع پایگاه داده برداری در حال استفاده. به ما کمک می‌کند بدانیم کدام ارائه‌دهنده بیشتر استفاده می‌شود.\n- نوع LLM در حال استفاده. به ما کمک می‌کند محبوب‌ترین انتخاب را بشناسیم.\n- ارسال چت. این معمول‌ترین \"رویداد\" است و به ما ایده‌ای از فعالیت روزانه می‌دهد.\n\nمی‌توانید این ادعاها را با پیدا کردن تمام مکان‌هایی که `Telemetry.sendTelemetry` فراخوانی می‌شود، تأیید کنید. ارائه‌دهنده تله‌متری [PostHog](https://posthog.com/) است.\n\n[مشاهده همه رویدادهای تله‌متری در کد منبع](https://github.com/search?q=repo%3AMintplex-Labs%2Fanything-llm%20.sendTelemetry(&type=code)\n\n</div>\n\n</details>\n\n## 👋 مشارکت\n\n<div dir=\"rtl\">\n\n- ایجاد issue\n- ایجاد PR با فرمت نام شاخه `<شماره issue>-<نام کوتاه>`\n- تأیید از تیم اصلی\n</div>\n\n## 🌟 مشارکت‌کنندگان\n\n[![مشارکت‌کنندگان anythingllm](https://contrib.rocks/image?repo=mintplex-labs/anything-llm)](https://github.com/mintplex-labs/anything-llm/graphs/contributors)\n\n[![نمودار تاریخچه ستاره‌ها](https://api.star-history.com/svg?repos=mintplex-labs/anything-llm&type=Timeline)](https://star-history.com/#mintplex-labs/anything-llm&Date)\n\n## 🔗 محصولات بیشتر\n\n<div dir=\"rtl\">\n\n- **[VectorAdmin][vector-admin]:** یک رابط کاربری و مجموعه ابزار همه‌کاره برای مدیریت پایگاه‌های داده برداری.\n- **[OpenAI Assistant Swarm][assistant-swarm]:** تبدیل کل کتابخانه دستیاران OpenAI به یک ارتش واحد تحت فرمان یک عامل.\n</div>\n\n<div align=\"right\">\n\n[![][back-to-top]](#readme-top)\n\n</div>\n\n---\n\n<div dir=\"ltr\" align=\"left\">\n\nCopyright © 2026 [Mintplex Labs][profile-link]. <br />\nThis project is [MIT](../LICENSE) licensed.\n\n</div>\n<!-- LINK GROUP -->\n\n[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-222628?style=flat-square\n[profile-link]: https://github.com/mintplex-labs\n[vector-admin]: https://github.com/mintplex-labs/vector-admin\n[assistant-swarm]: https://github.com/Mintplex-Labs/openai-assistant-swarm\n[docker-btn]: ./images/deployBtns/docker.png\n[docker-deploy]: ./docker/HOW_TO_USE_DOCKER.md\n[aws-btn]: ./images/deployBtns/aws.png\n[aws-deploy]: ./cloud-deployments/aws/cloudformation/DEPLOY.md\n[gcp-btn]: https://deploy.cloud.run/button.svg\n[gcp-deploy]: ./cloud-deployments/gcp/deployment/DEPLOY.md\n[do-btn]: https://www.deploytodo.com/do-btn-blue.svg\n[do-deploy]: ./cloud-deployments/digitalocean/terraform/DEPLOY.md\n[render-btn]: https://render.com/images/deploy-to-render-button.svg\n[render-deploy]: https://render.com/deploy?repo=https://github.com/Mintplex-Labs/anything-llm&branch=render\n[render-btn]: https://render.com/images/deploy-to-render-button.svg\n[render-deploy]: https://render.com/deploy?repo=https://github.com/Mintplex-Labs/anything-llm&branch=render\n[railway-btn]: https://railway.app/button.svg\n[railway-deploy]: https://railway.app/template/HNSCS1?referralCode=WFgJkn\n[repocloud-btn]: https://d16t0pc4846x52.cloudfront.net/deploylobe.svg\n[repocloud-deploy]: https://repocloud.io/details/?app_id=276\n[elestio-btn]: https://elest.io/images/logos/deploy-to-elestio-btn.png\n[elestio-deploy]: https://elest.io/open-source/anythingllm\n"
  },
  {
    "path": "locales/README.ja-JP.md",
    "content": "<a name=\"readme-top\"></a>\n\n<p align=\"center\">\n  <a href=\"https://anythingllm.com\"><img src=\"https://github.com/Mintplex-Labs/anything-llm/blob/master/images/wordmark.png?raw=true\" alt=\"AnythingLLM logo\"></a>\n</p>\n\n<div align='center'>\n<a href=\"https://trendshift.io/repositories/2415\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/2415\" alt=\"Mintplex-Labs%2Fanything-llm | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n</div>\n\n<p align=\"center\">\n    <b>AnythingLLM:</b> あなたが探していたオールインワンAIアプリ。<br />\n    ドキュメントとチャットし、AIエージェントを使用し、高度にカスタマイズ可能で、複数ユーザー対応、面倒な設定は不要です。\n</p>\n\n<p align=\"center\">\n  <a href=\"https://discord.gg/6UyHPeGZAC\" target=\"_blank\">\n      <img src=\"https://img.shields.io/badge/chat-mintplex_labs-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAH1UExURQAAAP////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////r6+ubn5+7u7/3+/v39/enq6urq6/v7+97f39rb26eoqT1BQ0pOT4+Rkuzs7cnKykZKS0NHSHl8fdzd3ejo6UxPUUBDRdzc3RwgIh8jJSAkJm5xcvHx8aanqB4iJFBTVezt7V5hYlJVVuLj43p9fiImKCMnKZKUlaaoqSElJ21wcfT09O3u7uvr6zE0Nr6/wCUpK5qcnf7+/nh7fEdKTHx+f0tPUOTl5aipqiouMGtubz5CRDQ4OsTGxufn515hY7a3uH1/gXBydIOFhlVYWvX29qaoqCQoKs7Pz/Pz87/AwUtOUNfY2dHR0mhrbOvr7E5RUy8zNXR2d/f39+Xl5UZJSx0hIzQ3Odra2/z8/GlsbaGjpERHSezs7L/BwScrLTQ4Odna2zM3Obm7u3x/gKSmp9jZ2T1AQu/v71pdXkVISr2+vygsLiInKTg7PaOlpisvMcXGxzk8PldaXPLy8u7u7rm6u7S1tsDBwvj4+MPExbe4ueXm5s/Q0Kyf7ewAAAAodFJOUwAABClsrNjx/QM2l9/7lhmI6jTB/kA1GgKJN+nea6vy/MLZQYeVKK3rVA5tAAAAAWJLR0QB/wIt3gAAAAd0SU1FB+cKBAAmMZBHjXIAAAISSURBVDjLY2CAAkYmZhZWNnYODnY2VhZmJkYGVMDIycXNw6sBBbw8fFycyEoYGfkFBDVQgKAAPyMjQl5IWEQDDYgIC8FUMDKKsmlgAWyiEBWMjGJY5YEqxMAqGMWFNXAAYXGgAkYJSQ2cQFKCkYFRShq3AmkpRgYJbghbU0tbB0Tr6ukbgGhDI10gySfBwCwDUWBsYmpmDqQtLK2sbTQ0bO3sHYA8GWYGWWj4WTs6Obu4ami4OTm7exhqeHp5+4DCVJZBDmqdr7ufn3+ArkZgkJ+fU3CIRmgYWFiOARYGvo5OQUHhEUAFTkF+kVHRsLBgkIeyYmLjwoOc4hMSk5JTnINS06DC8gwcEEZ6RqZGlpOfc3ZObl5+gZ+TR2ERWFyBQQFMF5eklmqUpQb5+ReU61ZUOvkFVVXXQBSAraitq29o1GiKcfLzc29u0mjxBzq0tQ0kww5xZHtHUGeXhkZhdxBYgZ4d0LI6c4gjwd7siQQraOp1AivQ6CuAKZCDBBRQQQNQgUb/BGf3cqCCiZOcnCe3QQIKHNRTpk6bDgpZjRkzg3pBQTBrdtCcuZCgluAD0vPmL1gIdvSixUuWgqNs2YJ+DUhkEYxuggkGmOQUcckrioPTJCOXEnZ5JS5YslbGnuyVERlDDFvGEUPOWvwqaH6RVkHKeuDMK6SKnHlVhTgx8jeTmqy6Eij7K6nLqiGyPwChsa1MUrnq1wAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0xMC0wNFQwMDozODo0OSswMDowMB9V0a8AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMTAtMDRUMDA6Mzg6NDkrMDA6MDBuCGkTAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDIzLTEwLTA0VDAwOjM4OjQ5KzAwOjAwOR1IzAAAAABJRU5ErkJggg==\" alt=\"Discord\">\n  </a> |\n  <a href=\"https://github.com/Mintplex-Labs/anything-llm/blob/master/LICENSE\" target=\"_blank\">\n      <img src=\"https://img.shields.io/static/v1?label=license&message=MIT&color=white\" alt=\"ライセンス\">\n  </a> |\n  <a href=\"https://docs.anythingllm.com\" target=\"_blank\">\n    ドキュメント\n  </a> |\n   <a href=\"https://my.mintplexlabs.com/aio-checkout?product=anythingllm\" target=\"_blank\">\n    ホストされたインスタンス\n  </a>\n</p>\n\n<p align=\"center\">\n  <a href='../README.md'>English</a> · <a href='./README.zh-CN.md'>简体中文</a> · <b>日本語</b>\n</p>\n\n<p align=\"center\">\n👉 デスクトップ用AnythingLLM（Mac、Windows、Linux対応）！<a href=\"https://anythingllm.com/download\" target=\"_blank\">今すぐダウンロード</a>\n</p>\n\nこれは、任意のドキュメント、リソース、またはコンテンツの断片を、チャット中にLLMが参照として使用できるコンテキストに変換できるフルスタックアプリケーションです。このアプリケーションを使用すると、使用するLLMまたはベクトルデータベースを選択し、マルチユーザー管理と権限をサポートできます。\n\n![Chatting](https://github.com/Mintplex-Labs/anything-llm/releases/download/v1.11.2/AnythingLLM720p.gif)\n\n<details>\n<summary><kbd>デモを見る！</kbd></summary>\n\n[![ビデオを見る](/images/youtube.png)](https://youtu.be/f95rGD9trL0)\n\n</details>\n\n### 製品概要\n\nAnythingLLMは、あなたが探していたオールインワンAIアプリです。AnythingLLMには、お気に入りのローカルまたはクラウドLLMプロバイダーを使用して、妥協のないプライベートChatGPTを構築するために必要なすべてが含まれています。AnythingLLMは高度にカスタマイズ可能でありながら、ビルトインエージェント、マルチユーザーサポート、ベクトルデータベース、ドキュメント取り込みパイプラインなど、すぐに使い始めるために必要なすべてが揃っています。\n\nAnythingLLMは複数ユーザーもサポートしており、インスタンスのセキュリティやプライバシー、知的財産を損なうことなく、ユーザーごとにアクセスと体験を制御できます。\n\n## AnythingLLMのいくつかのクールな機能\n\n- **マルチユーザーインスタンスのサポートと権限付与**\n- ワークスペース内のエージェント（ウェブを閲覧、コードを実行など）\n- [ウェブサイト用のカスタム埋め込み可能なチャットウィジェット](https://github.com/Mintplex-Labs/anythingllm-embed/blob/main/README.md)\n- 複数のドキュメントタイプのサポート（PDF、TXT、DOCXなど）\n- シンプルなUIからベクトルデータベース内のドキュメントを管理\n- 2つのチャットモード`会話`と`クエリ`。会話は以前の質問と修正を保持します。クエリはドキュメントに対するシンプルなQAです\n- チャット中の引用\n- 100%クラウドデプロイメント対応。\n- 「独自のLLMを持参」モデル。\n- 大規模なドキュメントを管理するための非常に効率的なコスト削減策。巨大なドキュメントやトランスクリプトを埋め込むために一度以上支払うことはありません。他のドキュメントチャットボットソリューションよりも90%コスト効率が良いです。\n- カスタム統合のための完全な開発者API！\n\n### サポートされているLLM、埋め込みモデル、音声モデル、およびベクトルデータベース\n\n**言語学習モデル：**\n\n- [llama.cpp互換の任意のオープンソースモデル](/server/storage/models/README.md#text-generation-llm-selection)\n- [OpenAI](https://openai.com)\n- [OpenAI (汎用)](https://openai.com)\n- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)\n- [Anthropic](https://www.anthropic.com/)\n- [Google Gemini Pro](https://ai.google.dev/)\n- [Hugging Face (チャットモデル)](https://huggingface.co/)\n- [Ollama (チャットモデル)](https://ollama.ai/)\n- [LM Studio (すべてのモデル)](https://lmstudio.ai)\n- [LocalAi (すべてのモデル)](https://localai.io/)\n- [Together AI (チャットモデル)](https://www.together.ai/)\n- [Fireworks AI (チャットモデル)](https://fireworks.ai/)\n- [Perplexity (チャットモデル)](https://www.perplexity.ai/)\n- [OpenRouter (チャットモデル)](https://openrouter.ai/)\n- [Novita AI (チャットモデル)](https://novita.ai/model-api/product/llm-api?utm_source=github_anything-llm&utm_medium=github_readme&utm_campaign=link)\n- [Mistral](https://mistral.ai/)\n- [Groq](https://groq.com/)\n- [Cohere](https://cohere.com/)\n- [KoboldCPP](https://github.com/LostRuins/koboldcpp)\n- [xAI](https://x.ai/)\n- [Z.AI (チャットモデル)](https://z.ai/model-api)\n- [PPIO](https://ppinfra.com?utm_source=github_anything-llm)\n- [CometAPI (チャットモデル)](https://api.cometapi.com/)\n- [Docker Model Runner](https://docs.docker.com/ai/model-runner/)\n- [PrivateModeAI (chat models)](https://privatemode.ai/)\n- [SambaNova Cloud (chat models)](https://cloud.sambanova.ai/)\n- [Lemonade by AMD](https://lemonade-server.ai)\n\n**埋め込みモデル：**\n\n- [AnythingLLMネイティブ埋め込み](/server/storage/models/README.md)（デフォルト）\n- [OpenAI](https://openai.com)\n- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)\n- [LocalAi (すべて)](https://localai.io/)\n- [Ollama (すべて)](https://ollama.ai/)\n- [LM Studio (すべて)](https://lmstudio.ai)\n- [Cohere](https://cohere.com/)\n\n**音声変換モデル：**\n\n- [AnythingLLM内蔵](https://github.com/Mintplex-Labs/anything-llm/tree/master/server/storage/models#audiovideo-transcription)（デフォルト）\n- [OpenAI](https://openai.com/)\n\n**TTS（テキストから音声へ）サポート：**\n\n- ネイティブブラウザ内蔵（デフォルト）\n- [OpenAI TTS](https://platform.openai.com/docs/guides/text-to-speech/voice-options)\n- [ElevenLabs](https://elevenlabs.io/)\n\n**STT（音声からテキストへ）サポート：**\n\n- ネイティブブラウザ内蔵（デフォルト）\n\n**ベクトルデータベース：**\n\n- [LanceDB](https://github.com/lancedb/lancedb)（デフォルト）\n- [PGVector](https://github.com/pgvector/pgvector)\n- [Astra DB](https://www.datastax.com/products/datastax-astra)\n- [Pinecone](https://pinecone.io)\n- [Chroma](https://trychroma.com)\n- [Weaviate](https://weaviate.io)\n- [QDrant](https://qdrant.tech)\n- [Milvus](https://milvus.io)\n- [Zilliz](https://zilliz.com)\n\n### 技術概要\n\nこのモノレポは、主に3つのセクションで構成されています：\n\n- `frontend`: LLMが使用できるすべてのコンテンツを簡単に作成および管理できるviteJS + Reactフロントエンド。\n- `server`: すべてのインタラクションを処理し、すべてのベクトルDB管理およびLLMインタラクションを行うNodeJS expressサーバー。\n- `collector`: UIからドキュメントを処理および解析するNodeJS expressサーバー。\n- `docker`: Dockerの指示およびビルドプロセス + ソースからのビルド情報。\n- `embed`: [埋め込みウィジェット](../embed/README.md)の生成に特化したコード。\n\n## 🛳 セルフホスティング\n\nMintplex Labsおよびコミュニティは、AnythingLLMをローカルで実行できる多数のデプロイメント方法、スクリプト、テンプレートを維持しています。以下の表を参照して、お好みの環境でのデプロイ方法を読むか、自動デプロイを行ってください。\n| Docker | AWS | GCP | Digital Ocean | Render.com |\n|----------------------------------------|----|-----|---------------|------------|\n| [![Docker上でデプロイ][docker-btn]][docker-deploy] | [![AWS上でデプロイ][aws-btn]][aws-deploy] | [![GCP上でデプロイ][gcp-btn]][gcp-deploy] | [![DigitalOcean上でデプロイ][do-btn]][do-deploy] | [![Render.com上でデプロイ][render-btn]][render-deploy] |\n\n| Railway                                               |\n| ----------------------------------------------------- |\n| [![Railway上でデプロイ][railway-btn]][railway-deploy] |\n\n[Dockerを使用せずに本番環境のAnythingLLMインスタンスを設定する →](../BARE_METAL.md)\n\n## 開発環境のセットアップ方法\n\n- `yarn setup` 各アプリケーションセクションに必要な`.env`ファイルを入力します（リポジトリのルートから）。\n  - 次に進む前にこれらを入力してください。`server/.env.development`が入力されていないと正しく動作しません。\n- `yarn dev:server` ローカルでサーバーを起動します（リポジトリのルートから）。\n- `yarn dev:frontend` ローカルでフロントエンドを起動します（リポジトリのルートから）。\n- `yarn dev:collector` ドキュメントコレクターを実行します（リポジトリのルートから）。\n\n[ドキュメントについて学ぶ](../server/storage/documents/DOCUMENTS.md)\n\n## 貢献する方法\n\n- issueを作成する\n- `<issue number>-<short name>`の形式のブランチ名でPRを作成する\n- マージしましょう\n\n## テレメトリーとプライバシー\n\nMintplex Labs Inc.によって開発されたAnythingLLMには、匿名の使用情報を収集するテレメトリー機能が含まれています。\n\n<details>\n<summary><kbd>AnythingLLMのテレメトリーとプライバシーについての詳細</kbd></summary>\n\n### なぜ？\n\nこの情報を使用して、AnythingLLMの使用方法を理解し、新機能とバグ修正の優先順位を決定し、AnythingLLMのパフォーマンスと安定性を向上させるのに役立てます。\n\n### オプトアウト\n\nサーバーまたはdockerの.env設定で`DISABLE_TELEMETRY`を「true」に設定して、テレメトリーからオプトアウトします。アプリ内でも、サイドバー > `プライバシー`に移動してテレメトリーを無効にすることができます。\n\n### 明示的に追跡するもの\n\n製品およびロードマップの意思決定に役立つ使用詳細のみを追跡します。具体的には：\n\n- インストールのタイプ（Dockerまたはデスクトップ）\n- ドキュメントが追加または削除されたとき。ドキュメントについての情報はありません。イベントが発生したことのみを知ります。これにより、使用状況を把握できます。\n- 使用中のベクトルデータベースのタイプ。どのベクトルデータベースプロバイダーが最も使用されているかを知り、更新があったときに優先して変更を行います。\n- 使用中のLLMのタイプ。最も人気のある選択肢を知り、更新があったときに優先して変更を行います。\n- チャットが送信された。これは最も一般的な「イベント」であり、すべてのインストールでのこのプロジェクトの日常的な「アクティビティ」についてのアイデアを提供します。再び、イベントのみが送信され、チャット自体の性質や内容に関する情報はありません。\n\nこれらの主張を検証するには、`Telemetry.sendTelemetry`が呼び出されるすべての場所を見つけてください。また、これらのイベントは出力ログに書き込まれるため、送信された具体的なデータも確認できます。IPアドレスやその他の識別情報は収集されません。テレメトリープロバイダーは[PostHog](https://posthog.com/)です。\n\n[ソースコード内のすべてのテレメトリーイベントを表示](https://github.com/search?q=repo%3AMintplex-Labs%2Fanything-llm%20.sendTelemetry(&type=code)\n\n</details>\n\n## 🔗 その他の製品\n\n- **[VectorAdmin][vector-admin]**：ベクトルデータベースを管理するためのオールインワンGUIおよびツールスイート。\n- **[OpenAI Assistant Swarm][assistant-swarm]**：単一のエージェントから指揮できるOpenAIアシスタントの軍隊に、ライブラリ全体を変換します。\n\n<div align=\"right\">\n\n[![][back-to-top]](#readme-top)\n\n</div>\n\n---\n\nCopyright © 2026 [Mintplex Labs][profile-link]。<br />\nこのプロジェクトは[MIT](https://github.com/Mintplex-Labs/anything-llm/blob/master/LICENSE)ライセンスの下でライセンスされています。\n\n<!-- LINK GROUP -->\n\n[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-222628?style=flat-square\n[profile-link]: https://github.com/mintplex-labs\n[vector-admin]: https://github.com/mintplex-labs/vector-admin\n[assistant-swarm]: https://github.com/Mintplex-Labs/openai-assistant-swarm\n[docker-btn]: ./images/deployBtns/docker.png\n[docker-deploy]: ./docker/HOW_TO_USE_DOCKER.md\n[aws-btn]: ./images/deployBtns/aws.png\n[aws-deploy]: ./cloud-deployments/aws/cloudformation/DEPLOY.md\n[gcp-btn]: https://deploy.cloud.run/button.svg\n[gcp-deploy]: ./cloud-deployments/gcp/deployment/DEPLOY.md\n[do-btn]: https://www.deploytodo.com/do-btn-blue.svg\n[do-deploy]: ./cloud-deployments/digitalocean/terraform/DEPLOY.md\n[render-btn]: https://render.com/images/deploy-to-render-button.svg\n[render-deploy]: https://render.com/deploy?repo=https://github.com/Mintplex-Labs/anything-llm&branch=render\n[render-btn]: https://render.com/images/deploy-to-render-button.svg\n[render-deploy]: https://render.com/deploy?repo=https://github.com/Mintplex-Labs/anything-llm&branch=render\n[railway-btn]: https://railway.app/button.svg\n[railway-deploy]: https://railway.app/template/HNSCS1?referralCode=WFgJkn\n"
  },
  {
    "path": "locales/README.tr-TR.md",
    "content": "<a name=\"readme-top\"></a>\n\n<p align=\"center\">\n  <a href=\"https://anythingllm.com\"><img src=\"https://github.com/Mintplex-Labs/anything-llm/blob/master/images/wordmark.png?raw=true\" alt=\"AnythingLLM logo\"></a>\n</p>\n\n<div align='center'>\n<a href=\"https://trendshift.io/repositories/2415\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/2415\" alt=\"Mintplex-Labs%2Fanything-llm | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n</div>\n\n<p align=\"center\">\n<b>AnythingLLM:</b> Aradığınız hepsi bir arada yapay zeka uygulaması.<br />\nBelgelerinizle sohbet edin, yapay zeka ajanlarını kullanın, son derece özelleştirilebilir, çok kullanıcılı ve zahmetsiz kurulum!\n</p>\n\n<p align=\"center\">\n  <a href=\"https://discord.gg/6UyHPeGZAC\" target=\"_blank\">\n      <img src=\"https://img.shields.io/badge/chat-mintplex_labs-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAH1UExURQAAAP////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////r6+ubn5+7u7/3+/v39/enq6urq6/v7+97f39rb26eoqT1BQ0pOT4+Rkuzs7cnKykZKS0NHSHl8fdzd3ejo6UxPUUBDRdzc3RwgIh8jJSAkJm5xcvHx8aanqB4iJFBTVezt7V5hYlJVVuLj43p9fiImKCMnKZKUlaaoqSElJ21wcfT09O3u7uvr6zE0Nr6/wCUpK5qcnf7+/nh7fEdKTHx+f0tPUOTl5aipqiouMGtubz5CRDQ4OsTGxufn515hY7a3uH1/gXBydIOFhlVYWvX29qaoqCQoKs7Pz/Pz87/AwUtOUNfY2dHR0mhrbOvr7E5RUy8zNXR2d/f39+Xl5UZJSx0hIzQ3Odra2/z8/GlsbaGjpERHSezs7L/BwScrLTQ4Odna2zM3Obm7u3x/gKSmp9jZ2T1AQu/v71pdXkVISr2+vygsLiInKTg7PaOlpisvMcXGxzk8PldaXPLy8u7u7rm6u7S1tsDBwvj4+MPExbe4ueXm5s/Q0Kyf7ewAAAAodFJOUwAABClsrNjx/QM2l9/7lhmI6jTB/kA1GgKJN+nea6vy/MLZQYeVKK3rVA5tAAAAAWJLR0QB/wIt3gAAAAd0SU1FB+cKBAAmMZBHjXIAAAISSURBVDjLY2CAAkYmZhZWNnYODnY2VhZmJkYGVMDIycXNw6sBBbw8fFycyEoYGfkFBDVQgKAAPyMjQl5IWEQDDYgIC8FUMDKKsmlgAWyiEBWMjGJY5YEqxMAqGMWFNXAAYXGgAkYJSQ2cQFKCkYFRShq3AmkpRgYJbghbU0tbB0Tr6ukbgGhDI10gySfBwCwDUWBsYmpmDqQtLK2sbTQ0bO3sHYA8GWYGWWj4WTs6Obu4ami4OTm7exhqeHp5+4DCVJZBDmqdr7ufn3+ArkZgkJ+fU3CIRmgYWFiOARYGvo5OQUHhEUAFTkF+kVHRsLBgkIeyYmLjwoOc4hMSk5JTnINS06DC8gwcEEZ6RqZGlpOfc3ZObl5+gZ+TR2ERWFyBQQFMF5eklmqUpQb5+ReU61ZUOvkFVVXXQBSAraitq29o1GiKcfLzc29u0mjxBzq0tQ0kww5xZHtHUGeXhkZhdxBYgZ4d0LI6c4gjwd7siQQraOp1AivQ6CuAKZCDBBRQQQNQgUb/BGf3cqCCiZOcnCe3QQIKHNRTpk6bDgpZjRkzg3pBQTBrdtCcuZCgluAD0vPmL1gIdvSixUuWgqNs2YJ+DUhkEYxuggkGmOQUcckrioPTJCOXEnZ5JS5YslbGnuyVERlDDFvGEUPOWvwqaH6RVkHKeuDMK6SKnHlVhTgx8jeTmqy6Eij7K6nLqiGyPwChsa1MUrnq1wAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0xMC0wNFQwMDozODo0OSswMDowMB9V0a8AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMTAtMDRUMDA6Mzg6NDkrMDA6MDBuCGkTAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDIzLTEwLTA0VDAwOjM4OjQ5KzAwOjAwOR1IzAAAAABJRU5ErkJggg==\" alt=\"Discord\">\n  </a> |\n  <a href=\"https://github.com/Mintplex-Labs/anything-llm/blob/master/LICENSE\" target=\"_blank\">\n      <img src=\"https://img.shields.io/static/v1?label=license&message=MIT&color=white\" alt=\"License\">\n  </a> |\n  <a href=\"https://docs.anythingllm.com\" target=\"_blank\">\n    Docs\n  </a> |\n   <a href=\"https://my.mintplexlabs.com/aio-checkout?product=anythingllm\" target=\"_blank\">\n    Hosted Instance\n  </a>\n</p>\n\n<p align=\"center\">\n  <b>English</b> · <a href='./locales/README.zh-CN.md'>简体中文</a> · <a href='./locales/README.ja-JP.md'>日本語</a> · <a href='./locales/README.tr-TR.md'>Turkish</a>\n</p>\n\n<p align=\"center\">\n👉 Masaüstü için AnythingLLM (Mac, Windows ve Linux)! <a href=\"https://anythingllm.com/download\" target=\"_blank\"> Şimdi İndir</a>\n</p>\n\nHerhangi bir belgeyi, kaynağı veya içeriği sohbet sırasında herhangi bir büyük dil modelinin referans olarak kullanabileceği bir bağlama dönüştürmenizi sağlayan tam kapsamlı bir uygulama. Bu uygulama, kullanmak istediğiniz LLM veya Vektör Veritabanını seçmenize olanak tanırken, çok kullanıcılı yönetim ve yetkilendirme desteği de sunar.\n\n![Chatting](https://github.com/Mintplex-Labs/anything-llm/releases/download/v1.11.2/AnythingLLM720p.gif)\n\n<details>\n<summary><kbd>Demoyu izle!</kbd></summary>\n\n[![Video'yu izle](/images/youtube.png)](https://youtu.be/f95rGD9trL0)\n\n</details>\n\n### Ürün Genel Bakışı\n\nAnythingLLM, aradığınız hepsi bir arada yapay zeka uygulamasıdır. AnythingLLM, favori yerel veya bulut LLM sağlayıcılarınızı kullanarak hiçbir ödün vermeden özel bir ChatGPT oluşturmak için ihtiyacınız olan her şeyi içerir. AnythingLLM son derece özelleştirilebilir olmakla birlikte, yerleşik ajanlar, çok kullanıcılı destek, vektör veritabanları, belge alma işlem hatları ve daha fazlası gibi hemen başlamak için ihtiyacınız olan her şeyle birlikte gelir.\n\nAnythingLLM ayrıca birden fazla kullanıcıyı da destekler; burada örneğin güvenliğini veya gizliliğini ya da fikri mülkiyetinizi tehlikeye atmadan kullanıcı başına erişimi ve deneyimi kontrol edebilirsiniz.\n\n## AnythingLLM’in Harika Özellikleri\n\n- 🆕 [**Özel Yapay Zeka Ajanları**](https://docs.anythingllm.com/agent/custom/introduction)\n- 🆕 [**Kod yazmadan AI Ajanı oluşturma aracı**](https://docs.anythingllm.com/agent-flows/overview)\n- 🖼️ **Çoklu-mod desteği (hem kapalı kaynak hem de açık kaynak LLM'ler!)**\n- 👤 Çok kullanıcılı destek ve yetkilendirme _(Yalnızca Docker sürümünde)_\n- 🦾 Çalışma alanı içinde ajanlar (web'de gezinme vb.)\n- 💬 [Web sitenize gömülebilir özel sohbet aracı](https://github.com/Mintplex-Labs/anythingllm-embed/blob/main/README.md) _(Yalnızca Docker sürümünde)_\n- 📖 Çoklu belge türü desteği (PDF, TXT, DOCX vb.)\n- Sade ve kullanışlı sohbet arayüzü, sürükle-bırak özelliği ve net kaynak gösterimi.\n- %100 bulut konuşlandırmaya hazır.\n- [Tüm popüler kapalı ve açık kaynak LLM sağlayıcılarıyla](#supported-llms-embedder-models-speech-models-and-vector-databases) uyumlu.\n- Büyük belgeleri yönetirken zaman ve maliyet tasarrufu sağlayan dahili optimizasyonlar.\n- Özel entegrasyonlar için tam kapsamlı Geliştirici API’si.\n- Ve çok daha fazlası... Kurup keşfedin!\n\n### Desteklenen LLM'ler, Embedding Modelleri, Konuşma Modelleri ve Vektör Veritabanları\n\n**Büyük Dil Modelleri (LLMs):**\n\n- [Any open-source llama.cpp compatible model](/server/storage/models/README.md#text-generation-llm-selection)\n- [OpenAI](https://openai.com)\n- [OpenAI (Generic)](https://openai.com)\n- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)\n- [AWS Bedrock](https://aws.amazon.com/bedrock/)\n- [Anthropic](https://www.anthropic.com/)\n- [NVIDIA NIM (chat models)](https://build.nvidia.com/explore/discover)\n- [Google Gemini Pro](https://ai.google.dev/)\n- [Hugging Face (chat models)](https://huggingface.co/)\n- [Ollama (chat models)](https://ollama.ai/)\n- [LM Studio (all models)](https://lmstudio.ai)\n- [LocalAi (all models)](https://localai.io/)\n- [Together AI (chat models)](https://www.together.ai/)\n- [Fireworks AI (chat models)](https://fireworks.ai/)\n- [Perplexity (chat models)](https://www.perplexity.ai/)\n- [OpenRouter (chat models)](https://openrouter.ai/)\n- [DeepSeek (chat models)](https://deepseek.com/)\n- [Mistral](https://mistral.ai/)\n- [Groq](https://groq.com/)\n- [Cohere](https://cohere.com/)\n- [KoboldCPP](https://github.com/LostRuins/koboldcpp)\n- [LiteLLM](https://github.com/BerriAI/litellm)\n- [Text Generation Web UI](https://github.com/oobabooga/text-generation-webui)\n- [Apipie](https://apipie.ai/)\n- [xAI](https://x.ai/)\n- [Z.AI (chat models)](https://z.ai/model-api)\n- [Novita AI (chat models)](https://novita.ai/model-api/product/llm-api?utm_source=github_anything-llm&utm_medium=github_readme&utm_campaign=link)\n- [PPIO](https://ppinfra.com?utm_source=github_anything-llm)\n- [Docker Model Runner](https://docs.docker.com/ai/model-runner/)\n- [PrivateModeAI (chat models)](https://privatemode.ai/)\n- [SambaNova Cloud (chat models)](https://cloud.sambanova.ai/)\n- [Lemonade by AMD](https://lemonade-server.ai)\n\n**Embedder modelleri:**\n\n- [AnythingLLM Native Embedder](/server/storage/models/README.md) (default)\n- [OpenAI](https://openai.com)\n- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)\n- [LocalAi (all)](https://localai.io/)\n- [Ollama (all)](https://ollama.ai/)\n- [LM Studio (all)](https://lmstudio.ai)\n- [Cohere](https://cohere.com/)\n\n**Ses Transkripsiyon Modelleri:**\n\n- [AnythingLLM Built-in](https://github.com/Mintplex-Labs/anything-llm/tree/master/server/storage/models#audiovideo-transcription) (default)\n- [OpenAI](https://openai.com/)\n\n**TTS (text-to-speech) desteği:**\n\n- Native Browser Built-in (default)\n- [PiperTTSLocal - runs in browser](https://github.com/rhasspy/piper)\n- [OpenAI TTS](https://platform.openai.com/docs/guides/text-to-speech/voice-options)\n- [ElevenLabs](https://elevenlabs.io/)\n- Any OpenAI Compatible TTS service.\n\n**STT (speech-to-text) desteği:**\n\n- Native Browser Built-in (default)\n\n**Vektör Databases:**\n\n- [LanceDB](https://github.com/lancedb/lancedb) (default)\n- [PGVector](https://github.com/pgvector/pgvector)\n- [Astra DB](https://www.datastax.com/products/datastax-astra)\n- [Pinecone](https://pinecone.io)\n- [Chroma](https://trychroma.com)\n- [Weaviate](https://weaviate.io)\n- [Qdrant](https://qdrant.tech)\n- [Milvus](https://milvus.io)\n- [Zilliz](https://zilliz.com)\n\n### Teknik Genel Bakış\n\nBu monorepo üç ana bölümden oluşmaktadır:\n\n- **`frontend`**: ViteJS + React tabanlı bir ön yüz, LLM'in kullanabileceği tüm içeriği kolayca oluşturup yönetmenizi sağlar.\n- **`server`**: NodeJS ve Express tabanlı bir sunucu, tüm etkileşimleri yönetir ve vektör veritabanı işlemleri ile LLM entegrasyonlarını gerçekleştirir.\n- **`collector`**: Kullanıcı arayüzünden gelen belgeleri işleyen ve ayrıştıran NodeJS Express tabanlı bir sunucu.\n- **`docker`**: Docker kurulum talimatları, derleme süreci ve kaynak koddan nasıl derleneceğine dair bilgiler içerir.\n- **`embed`**: [Web gömme widget’ı](https://github.com/Mintplex-Labs/anythingllm-embed) oluşturma ve entegrasyonu için alt modül.\n- **`browser-extension`**: [Chrome tarayıcı eklentisi](https://github.com/Mintplex-Labs/anythingllm-extension) için alt modül.\n\n## 🛳 Kendi Sunucunuzda Barındırma\n\nMintplex Labs ve topluluk, AnythingLLM'i yerel olarak çalıştırmak için çeşitli dağıtım yöntemleri, betikler ve şablonlar sunmaktadır. Aşağıdaki tabloya göz atarak tercih ettiğiniz ortamda nasıl dağıtım yapabileceğinizi öğrenebilir veya otomatik dağıtım seçeneklerini keşfedebilirsiniz.\n| Docker | AWS | GCP | Digital Ocean | Render.com |\n|----------------------------------------|----|-----|---------------|------------|\n| [![Deploy on Docker][docker-btn]][docker-deploy] | [![Deploy on AWS][aws-btn]][aws-deploy] | [![Deploy on GCP][gcp-btn]][gcp-deploy] | [![Deploy on DigitalOcean][do-btn]][do-deploy] | [![Deploy on Render.com][render-btn]][render-deploy] |\n\n| Railway                                             | RepoCloud                                                 | Elestio                                             |\n| --------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------- |\n| [![Deploy on Railway][railway-btn]][railway-deploy] | [![Deploy on RepoCloud][repocloud-btn]][repocloud-deploy] | [![Deploy on Elestio][elestio-btn]][elestio-deploy] |\n\n[veya Docker kullanmadan üretim ortamında AnythingLLM kurun →](../BARE_METAL.md)\n\n## Geliştirme İçin Kurulum\n\n- `yarn setup` → Uygulamanın her bileşeni için gerekli `.env` dosyalarını oluşturur (repo’nun kök dizininden çalıştırılmalıdır).\n  - Devam etmeden önce bu dosyaları doldurun. **Özellikle `server/.env.development` dosyasının doldurulduğundan emin olun**, aksi takdirde sistem düzgün çalışmaz.\n- `yarn dev:server` → Sunucuyu yerel olarak başlatır (repo’nun kök dizininden çalıştırılmalıdır).\n- `yarn dev:frontend` → Ön yüzü yerel olarak çalıştırır (repo’nun kök dizininden çalıştırılmalıdır).\n- `yarn dev:collector` → Belge toplayıcıyı çalıştırır (repo’nun kök dizininden çalıştırılmalıdır).\n\n[Belgeler hakkında bilgi edinin](../server/storage/documents/DOCUMENTS.md)\n\n## Telemetri ve Gizlilik\n\nMintplex Labs Inc. tarafından geliştirilen AnythingLLM, anonim kullanım bilgilerini toplayan bir telemetri özelliği içermektedir.\n\n<details>\n<summary><kbd>AnythingLLM için Telemetri ve Gizlilik hakkında daha fazla bilgi</kbd></summary>\n\n### Neden?\n\nBu bilgileri, AnythingLLM’in nasıl kullanıldığını anlamak, yeni özellikler ve hata düzeltmelerine öncelik vermek ve uygulamanın performansını ve kararlılığını iyileştirmek için kullanıyoruz.\n\n### Telemetriden Çıkış Yapma (Opt-Out)\n\nSunucu veya Docker `.env` ayarlarında `DISABLE_TELEMETRY` değerini \"true\" olarak ayarlayarak telemetriyi devre dışı bırakabilirsiniz. Ayrıca, uygulama içinde **Kenar Çubuğu > Gizlilik** bölümüne giderek de bu özelliği kapatabilirsiniz.\n\n### Hangi Verileri Açıkça Takip Ediyoruz?\n\nYalnızca ürün ve yol haritası kararlarını almamıza yardımcı olacak kullanım detaylarını takip ediyoruz:\n\n- Kurulum türü (Docker veya Masaüstü)\n- Bir belgenin eklenme veya kaldırılma olayı. **Belgenin içeriği hakkında hiçbir bilgi toplanmaz**, yalnızca olayın gerçekleştiği kaydedilir. Bu, kullanım sıklığını anlamamıza yardımcı olur.\n- Kullanılan vektör veritabanı türü. Hangi sağlayıcının daha çok tercih edildiğini belirlemek için bu bilgiyi topluyoruz.\n- Kullanılan LLM türü. En popüler modelleri belirleyerek bu sağlayıcılara öncelik verebilmemizi sağlar.\n- Sohbet başlatılması. Bu en sık gerçekleşen \"olay\" olup, projenin günlük etkinliği hakkında genel bir fikir edinmemize yardımcı olur. **Yalnızca olay kaydedilir, sohbetin içeriği veya doğası hakkında hiçbir bilgi toplanmaz.**\n\nBu verileri doğrulamak için kod içinde **`Telemetry.sendTelemetry` çağrılarını** inceleyebilirsiniz. Ayrıca, bu olaylar günlük kaydına yazıldığı için hangi verilerin gönderildiğini görebilirsiniz (eğer etkinleştirilmişse). **IP adresi veya diğer tanımlayıcı bilgiler toplanmaz.** Telemetri sağlayıcısı, açık kaynaklı bir telemetri toplama hizmeti olan [PostHog](https://posthog.com/)‘dur.\n\n[Kaynak kodda tüm telemetri olaylarını görüntüle](https://github.com/search?q=repo%3AMintplex-Labs%2Fanything-llm%20.sendTelemetry(&type=code)\n\n</details>\n\n## 👋 Katkıda Bulunma\n\n- Bir **issue** oluşturun.\n- `<issue numarası>-<kısa ad>` formatında bir **PR (Pull Request)** oluşturun.\n- Çekirdek ekipten **LGTM (Looks Good To Me)** onayı alın.\n\n## 🌟 Katkıda Bulunanlar\n\n[![anythingllm contributors](https://contrib.rocks/image?repo=mintplex-labs/anything-llm)](https://github.com/mintplex-labs/anything-llm/graphs/contributors)\n\n[![Star History Chart](https://api.star-history.com/svg?repos=mintplex-labs/anything-llm&type=Timeline)](https://star-history.com/#mintplex-labs/anything-llm&Date)\n\n## 🔗 Diğer Ürünler\n\n- **[VectorAdmin][vector-admin]:** Vektör veritabanlarını yönetmek için hepsi bir arada GUI ve araç paketi.\n- **[OpenAI Assistant Swarm][assistant-swarm]:** Tüm OpenAI asistanlarınızı tek bir ajan tarafından yönetilen bir yapay zeka ordusuna dönüştürün.\n\n<div align=\"right\">\n\n[![][back-to-top]](#readme-top)\n\n</div>\n\n---\n\nTelif Hakkı © 2026 [Mintplex Labs][profile-link]. <br />\nBu proje [MIT](../LICENSE) lisansı ile lisanslanmıştır.\n\n<!-- LINK GROUP -->\n\n[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-222628?style=flat-square\n[profile-link]: https://github.com/mintplex-labs\n[vector-admin]: https://github.com/mintplex-labs/vector-admin\n[assistant-swarm]: https://github.com/Mintplex-Labs/openai-assistant-swarm\n[docker-btn]: ./images/deployBtns/docker.png\n[docker-deploy]: ./docker/HOW_TO_USE_DOCKER.md\n[aws-btn]: ./images/deployBtns/aws.png\n[aws-deploy]: ./cloud-deployments/aws/cloudformation/DEPLOY.md\n[gcp-btn]: https://deploy.cloud.run/button.svg\n[gcp-deploy]: ./cloud-deployments/gcp/deployment/DEPLOY.md\n[do-btn]: https://www.deploytodo.com/do-btn-blue.svg\n[do-deploy]: ./cloud-deployments/digitalocean/terraform/DEPLOY.md\n[render-btn]: https://render.com/images/deploy-to-render-button.svg\n[render-deploy]: https://render.com/deploy?repo=https://github.com/Mintplex-Labs/anything-llm&branch=render\n[render-btn]: https://render.com/images/deploy-to-render-button.svg\n[render-deploy]: https://render.com/deploy?repo=https://github.com/Mintplex-Labs/anything-llm&branch=render\n[railway-btn]: https://railway.app/button.svg\n[railway-deploy]: https://railway.app/template/HNSCS1?referralCode=WFgJkn\n[repocloud-btn]: https://d16t0pc4846x52.cloudfront.net/deploylobe.svg\n[repocloud-deploy]: https://repocloud.io/details/?app_id=276\n[elestio-btn]: https://elest.io/images/logos/deploy-to-elestio-btn.png\n[elestio-deploy]: https://elest.io/open-source/anythingllm\n"
  },
  {
    "path": "locales/README.zh-CN.md",
    "content": "<a name=\"readme-top\"></a>\n\n<p align=\"center\">\n  <a href=\"https://anythingllm.com\"><img src=\"https://github.com/Mintplex-Labs/anything-llm/blob/master/images/wordmark.png?raw=true\" alt=\"AnythingLLM logo\"></a>\n</p>\n\n<div align='center'>\n<a href=\"https://trendshift.io/repositories/2415\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/2415\" alt=\"Mintplex-Labs%2Fanything-llm | Trendshift\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n</div>\n\n<p align=\"center\">\n    <b>AnythingLLM：</b> 您一直在寻找的全方位AI应用程序。<br />\n    与您的文档聊天，使用AI代理，高度可配置，多用户，无需繁琐的设置。\n</p>\n\n<p align=\"center\">\n\t<a href=\"https://discord.gg/6UyHPeGZAC\" target=\"_blank\">\n      <img src=\"https://img.shields.io/badge/chat-mintplex_labs-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAH1UExURQAAAP////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////r6+ubn5+7u7/3+/v39/enq6urq6/v7+97f39rb26eoqT1BQ0pOT4+Rkuzs7cnKykZKS0NHSHl8fdzd3ejo6UxPUUBDRdzc3RwgIh8jJSAkJm5xcvHx8aanqB4iJFBTVezt7V5hYlJVVuLj43p9fiImKCMnKZKUlaaoqSElJ21wcfT09O3u7uvr6zE0Nr6/wCUpK5qcnf7+/nh7fEdKTHx+f0tPUOTl5aipqiouMGtubz5CRDQ4OsTGxufn515hY7a3uH1/gXBydIOFhlVYWvX29qaoqCQoKs7Pz/Pz87/AwUtOUNfY2dHR0mhrbOvr7E5RUy8zNXR2d/f39+Xl5UZJSx0hIzQ3Odra2/z8/GlsbaGjpERHSezs7L/BwScrLTQ4Odna2zM3Obm7u3x/gKSmp9jZ2T1AQu/v71pdXkVISr2+vygsLiInKTg7PaOlpisvMcXGxzk8PldaXPLy8u7u7rm6u7S1tsDBwvj4+MPExbe4ueXm5s/Q0Kyf7ewAAAAodFJOUwAABClsrNjx/QM2l9/7lhmI6jTB/kA1GgKJN+nea6vy/MLZQYeVKK3rVA5tAAAAAWJLR0QB/wIt3gAAAAd0SU1FB+cKBAAmMZBHjXIAAAISSURBVDjLY2CAAkYmZhZWNnYODnY2VhZmJkYGVMDIycXNw6sBBbw8fFycyEoYGfkFBDVQgKAAPyMjQl5IWEQDDYgIC8FUMDKKsmlgAWyiEBWMjGJY5YEqxMAqGMWFNXAAYXGgAkYJSQ2cQFKCkYFRShq3AmkpRgYJbghbU0tbB0Tr6ukbgGhDI10gySfBwCwDUWBsYmpmDqQtLK2sbTQ0bO3sHYA8GWYGWWj4WTs6Obu4ami4OTm7exhqeHp5+4DCVJZBDmqdr7ufn3+ArkZgkJ+fU3CIRmgYWFiOARYGvo5OQUHhEUAFTkF+kVHRsLBgkIeyYmLjwoOc4hMSk5JTnINS06DC8gwcEEZ6RqZGlpOfc3ZObl5+gZ+TR2ERWFyBQQFMF5eklmqUpQb5+ReU61ZUOvkFVVXXQBSAraitq29o1GiKcfLzc29u0mjxBzq0tQ0kww5xZHtHUGeXhkZhdxBYgZ4d0LI6c4gjwd7siQQraOp1AivQ6CuAKZCDBBRQQQNQgUb/BGf3cqCCiZOcnCe3QQIKHNRTpk6bDgpZjRkzg3pBQTBrdtCcuZCgluAD0vPmL1gIdvSixUuWgqNs2YJ+DUhkEYxuggkGmOQUcckrioPTJCOXEnZ5JS5YslbGnuyVERlDDFvGEUPOWvwqaH6RVkHKeuDMK6SKnHlVhTgx8jeTmqy6Eij7K6nLqiGyPwChsa1MUrnq1wAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0xMC0wNFQwMDozODo0OSswMDowMB9V0a8AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMTAtMDRUMDA6Mzg6NDkrMDA6MDBuCGkTAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDIzLTEwLTA0VDAwOjM4OjQ5KzAwOjAwOR1IzAAAAABJRU5ErkJggg==\" alt=\"Discord\">\n  </a> |\n  <a href=\"https://github.com/Mintplex-Labs/anything-llm/blob/master/LICENSE\" target=\"_blank\">\n      <img src=\"https://img.shields.io/static/v1?label=license&message=MIT&color=white\" alt=\"许可证\">\n  </a> |\n  <a href=\"https://docs.anythingllm.com\" target=\"_blank\">\n    文档\n  </a> |\n  <a href=\"https://my.mintplexlabs.com/aio-checkout?product=anythingllm\" target=\"_blank\">\n    托管实例\n  </a>\n</p>\n\n<p align=\"center\">\n  <a href='../README.md'>English</a> · <b>简体中文</b> · <a href='./README.ja-JP.md'>日本語</a>\n</p>\n\n<p align=\"center\">\n👉 适用于桌面（Mac、Windows和Linux）的AnythingLLM！<a href=\"https://anythingllm.com/download\" target=\"_blank\">立即下载</a>\n</p>\n\n这是一个全栈应用程序，可以将任何文档、资源（如网址链接、音频、视频）或内容片段转换为上下文，以便任何大语言模型（LLM）在聊天期间作为参考使用。此应用程序允许您选择使用哪个LLM或向量数据库，同时支持多用户管理并设置不同权限。\n\n![Chatting](https://github.com/Mintplex-Labs/anything-llm/releases/download/v1.11.2/AnythingLLM720p.gif)\n\n<details>\n<summary><kbd>观看演示视频！</kbd></summary>\n\n[![观看视频](/images/youtube.png)](https://youtu.be/f95rGD9trL0)\n\n</details>\n\n### 产品概览\n\nAnythingLLM是您一直在寻找的全方位AI应用程序。AnythingLLM包含了使用您喜爱的本地或云端LLM提供商构建私有ChatGPT所需的一切，毫无妥协。AnythingLLM高度可配置，但开箱即用，内置代理、多用户支持、向量数据库、文档摄取管道等功能。\n\nAnythingLLM还支持多用户，您可以控制每个用户的访问权限和体验，同时不会影响实例的安全性、隐私性或您的知识产权。\n\n## AnythingLLM的一些酷炫特性\n\n- 🆕 [**完全兼容 MCP**](https://docs.anythingllm.com/mcp-compatibility/overview)\n- 🆕 [**无代码AI代理构建器**](https://docs.anythingllm.com/agent-flows/overview)\n- 🖼️ **多用户实例支持和权限管理（支持封闭源和开源LLM！）**\n- [**自定义人工智能代理**](https://docs.anythingllm.com/agent/custom/introduction)\n- 👤 多用户实例支持和权限管理 _仅限Docker版本_\n- 🦾 工作区内的智能体（浏览网页、运行代码等）\n- 💬 [为您的网站定制的可嵌入聊天窗口](https://github.com/Mintplex-Labs/anythingllm-embed/blob/main/README.md)\n- 📖 支持多种文档类型（PDF、TXT、DOCX等）\n- 带有拖放功能和清晰引用的简洁聊天界面。\n- 100%云部署就绪。\n- 兼容所有主流的[闭源和开源大语言模型提供商](#支持的llm嵌入模型转录模型和向量数据库)。\n- 内置节省成本和时间的机制，用于处理超大文档，优于任何其他聊天界面。\n- 全套的开发人员API，用于自定义集成！\n- 而且还有更多精彩功能……安装后亲自体验吧！\n\n### 支持的LLM、嵌入模型、转录模型和向量数据库\n\n**支持的LLM：**\n\n- [任何与llama.cpp兼容的开源模型](/server/storage/models/README.md#text-generation-llm-selection)\n- [OpenAI](https://openai.com)\n- [OpenAI (通用)](https://openai.com)\n- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)\n- [AWS Bedrock](https://aws.amazon.com/bedrock/)\n- [Anthropic](https://www.anthropic.com/)\n- [NVIDIA NIM (聊天模型)](https://build.nvidia.com/explore/discover)\n- [Google Gemini Pro](https://ai.google.dev/)\n- [Hugging Face (聊天模型)](https://huggingface.co/)\n- [Ollama (聊天模型)](https://ollama.ai/)\n- [LM Studio (所有模型)](https://lmstudio.ai)\n- [LocalAI (所有模型)](https://localai.io/)\n- [Together AI (聊天模型)](https://www.together.ai/)\n- [Fireworks AI (聊天模型)](https://fireworks.ai/)\n- [Perplexity (聊天模型)](https://www.perplexity.ai/)\n- [OpenRouter (聊天模型)](https://openrouter.ai/)\n- [DeepSeek (聊天模型)](https://deepseek.com/)\n- [Mistral](https://mistral.ai/)\n- [Groq](https://groq.com/)\n- [Cohere](https://cohere.com/)\n- [KoboldCPP](https://github.com/LostRuins/koboldcpp)\n- [LiteLLM](https://github.com/BerriAI/litellm)\n- [Text Generation Web UI](https://github.com/oobabooga/text-generation-webui)\n- [Apipie](https://apipie.ai/)\n- [xAI](https://x.ai/)\n- [Z.AI (聊天模型)](https://z.ai/model-api)\n- [Novita AI (聊天模型)](https://novita.ai/model-api/product/llm-api?utm_source=github_anything-llm&utm_medium=github_readme&utm_campaign=link)\n- [PPIO (聊天模型)](https://ppinfra.com?utm_source=github_anything-llm)\n- [CometAPI (聊天模型)](https://api.cometapi.com/)\n- [Docker Model Runner](https://docs.docker.com/ai/model-runner/)\n- [PrivateModeAI (chat models)](https://privatemode.ai/)\n- [SambaNova Cloud (chat models)](https://cloud.sambanova.ai/)\n- [Lemonade by AMD](https://lemonade-server.ai)\n\n**支持的嵌入模型：**\n\n- [AnythingLLM原生嵌入器](/server/storage/models/README.md)（默认）\n- [OpenAI](https://openai.com)\n- [Azure OpenAI](https://azure.microsoft.com/en-us/products/ai-services/openai-service)\n- [LocalAI (全部)](https://localai.io/)\n- [Ollama (全部)](https://ollama.ai/)\n- [LM Studio (全部)](https://lmstudio.ai)\n- [Cohere](https://cohere.com/)\n\n**支持的转录模型：**\n\n- [AnythingLLM内置](https://github.com/Mintplex-Labs/anything-llm/tree/master/server/storage/models#audiovideo-transcription) （默认）\n- [OpenAI](https://openai.com/)\n\n**TTS (文本转语音) 支持：**\n\n- 浏览器内置（默认）\n- [PiperTTSLocal - 在浏览器中运行](https://github.com/rhasspy/piper)\n- [OpenAI TTS](https://platform.openai.com/docs/guides/text-to-speech/voice-options)\n- [ElevenLabs](https://elevenlabs.io/)\n- 任何与 OpenAI 兼容的 TTS 服务\n\n**STT (语音转文本) 支持：**\n\n- 浏览器内置（默认）\n\n**支持的向量数据库：**\n\n- [LanceDB](https://github.com/lancedb/lancedb) （默认）\n- [PGVector](https://github.com/pgvector/pgvector)\n- [Astra DB](https://www.datastax.com/products/datastax-astra)\n- [Pinecone](https://pinecone.io)\n- [Chroma](https://trychroma.com)\n- [Weaviate](https://weaviate.io)\n- [QDrant](https://qdrant.tech)\n- [Milvus](https://milvus.io)\n- [Zilliz](https://zilliz.com)\n\n### 技术概览\n\n这个单库由六个主要部分组成：\n\n- `frontend`: 一个 viteJS + React 前端，您可以运行它来轻松创建和管理LLM可以使用的所有内容。\n- `server`: 一个 NodeJS express 服务器，用于处理所有交互并进行所有向量数据库管理和 LLM 交互。\n- `collector`: NodeJS express 服务器，用于从UI处理和解析文档。\n- `docker`: Docker 指令和构建过程 + 从源代码构建的信息。\n- `embed`: 用于生成和创建[网页嵌入组件](https://github.com/Mintplex-Labs/anythingllm-embed)的子模块.\n- `browser-extension`: 用于[Chrome 浏览器扩展](https://github.com/Mintplex-Labs/anythingllm-extension)的子模块.\n\n## 🛳 自托管\n\nMintplex Labs和社区维护了许多部署方法、脚本和模板，您可以使用它们在本地运行AnythingLLM。请参阅下面的表格，了解如何在您喜欢的环境上部署，或自动部署。\n| Docker | AWS | GCP | Digital Ocean | Render.com |\n|----------------------------------------|----|-----|---------------|------------|\n| [![在 Docker 上部署][docker-btn]][docker-deploy] | [![在 AWS 上部署][aws-btn]][aws-deploy] | [![在 GCP 上部署][gcp-btn]][gcp-deploy] | [![在DigitalOcean上部署][do-btn]][do-deploy] | [![在 Render.com 上部署][render-btn]][render-deploy] |\n\n| Railway                                             | RepoCloud                                                 | Elestio                                             |\n| --------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------- |\n| [![在 Railway 上部署][railway-btn]][railway-deploy] | [![在 RepoCloud 上部署][repocloud-btn]][repocloud-deploy] | [![在 Elestio 上部署][elestio-btn]][elestio-deploy] |\n\n[其他方案：不使用Docker配置AnythingLLM实例 →](../BARE_METAL.md)\n\n## 如何设置开发环境\n\n- `yarn setup` 填充每个应用程序部分所需的 `.env` 文件（从仓库的根目录）。\n  - 在开始下一步之前，先填写这些信息`server/.env.development`，不然代码无法正常执行。\n- `yarn dev:server` 在本地启动服务器（从仓库的根目录）。\n- `yarn dev:frontend` 在本地启动前端（从仓库的根目录）。\n- `yarn dev:collector` 然后运行文档收集器（从仓库的根目录）。\n\n[了解文档](../server/storage/documents/DOCUMENTS.md)\n\n## 远程信息收集与隐私保护\n\n由 Mintplex Labs Inc 开发的 AnythingLLM 包含一个收集匿名使用信息的 Telemetry 功能。\n\n<details>\n<summary><kbd>有关 AnythingLLM 的远程信息收集与隐私保护更多信息</kbd></summary>\n\n### 为什么收集信息？\n\n我们使用这些信息来帮助我们理解 AnythingLLM 的使用情况，帮助我们确定新功能和错误修复的优先级，并帮助我们提高 AnythingLLM 的性能和稳定性。\n\n### 怎样关闭\n\n在服务器或 Docker 的 .env 设置中将 `DISABLE_TELEMETRY` 设置为 \"true\"，即可选择不参与遥测数据收集。你也可以在应用内通过以下路径操作：侧边栏 > `Privacy` （隐私） > 关闭遥测功能。\n\n### 你们跟踪收集哪些信息？\n\n我们只会跟踪有助于我们做出产品和路线图决策的使用细节，具体包括：\n\n- 您的安装方式（Docker或桌面版）\n- 文档被添加或移除的时间。但不包括文档内的具体内容。我们只关注添加或移除文档这个行为。这些信息能让我们了解到文档功能的使用情况。\n- 使用中的向量数据库类型。让我们知道哪个向量数据库最受欢迎，并在后续更新中优先考虑相应的数据库。\n- 使用中的LLM类型。让我们知道谁才是最受欢迎的LLM模型，并在后续更新中优先考虑相应模型。\n- 信息被`发送`出去。这是最常规的“事件/行为/event”，并让我们了解到所有安装了这个项目的每日活动情况。同样，只收集`发送`这个行为的信息，我们不会收集关于聊天本身的性质或内容的任何信息。\n\n您可以通过查找所有调用`Telemetry.sendTelemetry`的位置来验证这些声明。此外，如果启用，这些事件也会被写入输出日志，因此您也可以看到发送了哪些具体数据。**IP或其他识别信息不会被收集**。Telemetry远程信息收集的方案来自[PostHog](https://posthog.com/) - 一个开源的远程信息收集服务。\n\n我们非常重视隐私，且不用烦人的弹窗问卷来获取反馈，希望你能理解为什么我们想要知道该工具的使用情况，这样我们才能打造真正值得使用的产品。所有匿名数据 _绝不会_ 与任何第三方共享。\n\n[在源代码中查看所有信息收集活动](https://github.com/search?q=repo%3AMintplex-Labs%2Fanything-llm%20.sendTelemetry(&type=code)\n\n</details>\n\n## 👋 如何贡献\n\n- 创建 issue\n- 创建 PR，分支名称格式为 `<issue number>-<short name>`\n- 合并\n\n## 💖 赞助商\n\n### 高级赞助商\n\n<!-- premium-sponsors (reserved for $100/mth sponsors who request to be called out here and/or are non-private sponsors) -->\n<a href=\"https://www.dcsdigital.co.uk\" target=\"_blank\">\n  <img src=\"https://a8cforagenciesportfolio.wordpress.com/wp-content/uploads/2024/08/logo-image-232621379.png\" height=\"100px\" alt=\"User avatar: DCS DIGITAL\" />\n</a>\n<!-- premium-sponsors -->\n\n### 所有赞助商\n\n<!-- all-sponsors --><a href=\"https://github.com/jaschadub\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;jaschadub.png\" width=\"60px\" alt=\"User avatar: Jascha\" /></a><a href=\"https://github.com/KickingAss2024\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;KickingAss2024.png\" width=\"60px\" alt=\"User avatar: KickAss\" /></a><a href=\"https://github.com/ShadowArcanist\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;ShadowArcanist.png\" width=\"60px\" alt=\"User avatar: ShadowArcanist\" /></a><a href=\"https://github.com/AtlasVIA\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;AtlasVIA.png\" width=\"60px\" alt=\"User avatar: Atlas\" /></a><a href=\"https://github.com/cope\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;cope.png\" width=\"60px\" alt=\"User avatar: Predrag Stojadinović\" /></a><a href=\"https://github.com/DiegoSpinola\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;DiegoSpinola.png\" width=\"60px\" alt=\"User avatar: Diego Spinola\" /></a><a href=\"https://github.com/PortlandKyGuy\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;PortlandKyGuy.png\" width=\"60px\" alt=\"User avatar: Kyle\" /></a><a href=\"https://github.com/peperunas\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;peperunas.png\" width=\"60px\" alt=\"User avatar: Giulio De Pasquale\" /></a><a href=\"https://github.com/jasoncdavis0\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;jasoncdavis0.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/macstadium\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;macstadium.png\" width=\"60px\" alt=\"User avatar: MacStadium\" /></a><a href=\"https://github.com/armlynobinguar\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;armlynobinguar.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/MikeHago\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;MikeHago.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/maaisde\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;maaisde.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/mhollier117\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;mhollier117.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/pleabargain\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;pleabargain.png\" width=\"60px\" alt=\"User avatar: Dennis\" /></a><a href=\"https://github.com/broichan\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;broichan.png\" width=\"60px\" alt=\"User avatar: Michael Hamilton, Ph.D.\" /></a><a href=\"https://github.com/azim-charaniya\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;azim-charaniya.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/gabriellemon\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;gabriellemon.png\" width=\"60px\" alt=\"User avatar: TernaryLabs\" /></a><a href=\"https://github.com/CelaDaniel\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;CelaDaniel.png\" width=\"60px\" alt=\"User avatar: Daniel Cela\" /></a><a href=\"https://github.com/altrsadmin\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;altrsadmin.png\" width=\"60px\" alt=\"User avatar: Alesso\" /></a><a href=\"https://github.com/bitjungle\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;bitjungle.png\" width=\"60px\" alt=\"User avatar: Rune Mathisen\" /></a><a href=\"https://github.com/pcrossleyAC\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;pcrossleyAC.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/saroj-pattnaik\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;saroj-pattnaik.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/techmedic5\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;techmedic5.png\" width=\"60px\" alt=\"User avatar: Alan\" /></a><a href=\"https://github.com/ddocta\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;ddocta.png\" width=\"60px\" alt=\"User avatar: Damien Peters\" /></a><a href=\"https://github.com/dcsdigital\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;dcsdigital.png\" width=\"60px\" alt=\"User avatar: DCS Digital\" /></a><a href=\"https://github.com/pm7y\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;pm7y.png\" width=\"60px\" alt=\"User avatar: Paul Mcilreavy\" /></a><a href=\"https://github.com/tilwolf\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;tilwolf.png\" width=\"60px\" alt=\"User avatar: Til Wolf\" /></a><a href=\"https://github.com/ozzyoss77\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;ozzyoss77.png\" width=\"60px\" alt=\"User avatar: Leopoldo Crhistian Riverin Gomez\" /></a><a href=\"https://github.com/AlphaEcho11\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;AlphaEcho11.png\" width=\"60px\" alt=\"User avatar: AJEsau\" /></a><a href=\"https://github.com/svanomm\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;svanomm.png\" width=\"60px\" alt=\"User avatar: Steven VanOmmeren\" /></a><a href=\"https://github.com/socketbox\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;socketbox.png\" width=\"60px\" alt=\"User avatar: Casey Boettcher\" /></a><a href=\"https://github.com/zebbern\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;zebbern.png\" width=\"60px\" alt=\"User avatar: \" /></a><a href=\"https://github.com/avineetbespin\"><img src=\"https:&#x2F;&#x2F;github.com&#x2F;avineetbespin.png\" width=\"60px\" alt=\"User avatar: Avineet\" /></a><!-- all-sponsors -->\n\n## 🌟 贡献者们\n\n[![anythingllm 的贡献者们](https://contrib.rocks/image?repo=mintplex-labs/anything-llm)](https://github.com/mintplex-labs/anything-llm/graphs/contributors)\n\n[![Star 历史图](https://api.star-history.com/svg?repos=mintplex-labs/anything-llm&type=Timeline)](https://star-history.com/#mintplex-labs/anything-llm&Date)\n\n## 🔗 更多产品\n\n- **[VectorAdmin][vector-admin]**：一个用于管理向量数据库的全方位图形用户界面和工具套件。\n- **[OpenAI Assistant Swarm][assistant-swarm]**：一个智能体就可以管理您所有的OpenAI助手。\n\n<div align=\"right\">\n\n[![][back-to-top]](#readme-top)\n\n</div>\n\n---\n\n版权所有 © 2026 [Mintplex Labs][profile-link]。<br />\n本项目采用[MIT](https://github.com/Mintplex-Labs/anything-llm/blob/master/LICENSE)许可证。\n\n<!-- LINK GROUP -->\n\n[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-222628?style=flat-square\n[profile-link]: https://github.com/mintplex-labs\n[vector-admin]: https://github.com/mintplex-labs/vector-admin\n[assistant-swarm]: https://github.com/Mintplex-Labs/openai-assistant-swarm\n[docker-btn]: ../images/deployBtns/docker.png\n[docker-deploy]: ../docker/HOW_TO_USE_DOCKER.md\n[aws-btn]: ../images/deployBtns/aws.png\n[aws-deploy]: ../cloud-deployments/aws/cloudformation/DEPLOY.md\n[gcp-btn]: https://deploy.cloud.run/button.svg\n[gcp-deploy]: ../cloud-deployments/gcp/deployment/DEPLOY.md\n[do-btn]: https://www.deploytodo.com/do-btn-blue.svg\n[do-deploy]: ../cloud-deployments/digitalocean/terraform/DEPLOY.md\n[render-btn]: https://render.com/images/deploy-to-render-button.svg\n[render-deploy]: https://render.com/deploy?repo=https://github.com/Mintplex-Labs/anything-llm&branch=render\n[render-btn]: https://render.com/images/deploy-to-render-button.svg\n[render-deploy]: https://render.com/deploy?repo=https://github.com/Mintplex-Labs/anything-llm&branch=render\n[railway-btn]: https://railway.app/button.svg\n[railway-deploy]: https://railway.app/template/HNSCS1?referralCode=WFgJkn\n[repocloud-btn]: https://d16t0pc4846x52.cloudfront.net/deploylobe.svg\n[repocloud-deploy]: https://repocloud.io/details/?app_id=276\n[elestio-btn]: https://elest.io/images/logos/deploy-to-elestio-btn.png\n[elestio-deploy]: https://elest.io/open-source/anythingllm\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"anything-llm\",\n  \"version\": \"1.11.1\",\n  \"description\": \"The best solution for turning private documents into a chat bot using off-the-shelf tools and commercially viable AI technologies.\",\n  \"main\": \"index.js\",\n  \"type\": \"module\",\n  \"author\": \"Timothy Carambat (Mintplex Labs)\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/mintplex-labs/anything-llm.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/mintplex-labs/anything-llm/issues\"\n  },\n  \"homepage\": \"https://github.com/mintplex-labs/anything-llm#readme\",\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"lint\": \"cd server && yarn lint && cd ../frontend && yarn lint && cd ../collector && yarn lint\",\n    \"lint:ci\": \"cd server && yarn lint:check && cd ../frontend && yarn lint:check && cd ../collector && yarn lint:check\",\n    \"setup\": \"cd server && yarn && cd ../collector && yarn && cd ../frontend && yarn && cd .. && yarn setup:envs && yarn prisma:setup && echo \\\"Please run yarn dev:server, yarn dev:collector, and yarn dev:frontend in separate terminal tabs.\\\"\",\n    \"setup:envs\": \"cp -n ./frontend/.env.example ./frontend/.env; cp -n ./server/.env.example ./server/.env.development; cp -n ./collector/.env.example ./collector/.env; cp -n ./docker/.env.example ./docker/.env; echo \\\"All ENV files copied!\\n\\\"\",\n    \"dev:server\": \"cd server && yarn dev\",\n    \"dev:collector\": \"cd collector && yarn dev\",\n    \"dev:frontend\": \"cd frontend && yarn dev\",\n    \"dev:all\": \"npx concurrently \\\"yarn dev:server\\\" \\\"yarn dev:frontend\\\" \\\"yarn dev:collector\\\"\",\n    \"prisma:generate\": \"cd server && npx prisma generate\",\n    \"prisma:migrate\": \"cd server && npx prisma migrate dev --name init\",\n    \"prisma:seed\": \"cd server && npx prisma db seed\",\n    \"prisma:setup\": \"yarn prisma:generate && yarn prisma:migrate && yarn prisma:seed\",\n    \"prisma:reset\": \"truncate -s 0 server/storage/anythingllm.db && yarn prisma:migrate\",\n    \"prod:server\": \"cd server && yarn start\",\n    \"prod:frontend\": \"cd frontend && yarn build\",\n    \"generate:cloudformation\": \"node cloud-deployments/aws/cloudformation/generate.mjs\",\n    \"generate::gcp_deployment\": \"node cloud-deployments/gcp/deployment/generate.mjs\",\n    \"translations:verify\": \"cd frontend/src/locales && node verifyTranslations.mjs\",\n    \"translations:normalize\": \"cd frontend/src/locales && node normalizeEn.mjs && cd ../../.. && cd frontend && yarn lint && cd .. && yarn translations:verify\",\n    \"translations:prune\": \"cd frontend/src/locales && node findUnusedTranslations.mjs --delete && cd ../../../ && yarn translations:normalize\",\n    \"translations:create\": \"cd extras/translator && node index.mjs --all\"\n  },\n  \"private\": false,\n  \"devDependencies\": {\n    \"concurrently\": \"^9.1.2\",\n    \"jest\": \"^29.7.0\"\n  }\n}\n"
  },
  {
    "path": "pull_request_template.md",
    "content": "\n### Pull Request Type\n\n<!-- For change type, change [ ] to [x]. -->\n\n- [ ] ✨ feat (New feature)\n- [ ] 🐛 fix (Bug fix)\n- [ ] ♻️ refactor (Code refactoring without changing behavior)\n- [ ] 💄 style (UI style changes)\n- [ ] 🔨 chore (Build, CI, maintenance)\n- [ ] 📝 docs (Documentation updates)\n\n### Relevant Issues\n\n<!-- Use \"resolves #xxx\" to auto resolve on merge. Otherwise, please use \"connect #xxx\" -->\n\nresolves #\n\n### Description\n\n<!-- Describe the changes in this PR that are impactful to the repo. What problem does it solve? -->\n\n\n### Visuals (if applicable)\n\n<!-- Add screenshots or screen recordings to demonstrate the changes, especially for UI updates. -->\n\n\n### Additional Information\n\n<!-- Add any other context about the Pull Request here that was not captured above. -->\n\n\n### Developer Validations\n\n<!-- All of the applicable items should be checked. -->\n\n- [ ] I ran `yarn lint` from the root of the repo & committed changes\n- [ ] Relevant documentation has been updated (if applicable)\n- [ ] I have tested my code functionality\n- [ ] Docker build succeeds locally\n"
  },
  {
    "path": "server/.env.example",
    "content": "SERVER_PORT=3001\nJWT_SECRET=\"my-random-string-for-seeding\" # Please generate random string at least 12 chars long.\n# JWT_EXPIRY=\"30d\" # (optional) https://docs.anythingllm.com/configuration#custom-ttl-for-sessions\nSIG_KEY='passphrase' # Please generate random string at least 32 chars long.\nSIG_SALT='salt' # Please generate random string at least 32 chars long.\n\n###########################################\n######## LLM API SElECTION ################\n###########################################\n# LLM_PROVIDER='openai'\n# OPEN_AI_KEY=\n# OPEN_MODEL_PREF='gpt-4o'\n\n# LLM_PROVIDER='gemini'\n# GEMINI_API_KEY=\n# GEMINI_LLM_MODEL_PREF='gemini-2.0-flash-lite'\n\n# LLM_PROVIDER='azure'\n# AZURE_OPENAI_ENDPOINT=\n# AZURE_OPENAI_KEY=\n# AZURE_OPENAI_MODEL_PREF='my-gpt35-deployment' # This is the \"deployment\" on Azure you want to use. Not the base model.\n# EMBEDDING_MODEL_PREF='embedder-model' # This is the \"deployment\" on Azure you want to use for embeddings. Not the base model. Valid base model is text-embedding-ada-002\n\n# LLM_PROVIDER='anthropic'\n# ANTHROPIC_API_KEY=sk-ant-xxxx\n# ANTHROPIC_MODEL_PREF='claude-2'\n# ANTHROPIC_CACHE_CONTROL=\"5m\" # Enable prompt caching (5m=5min cache, 1h=1hour cache). Reduces costs and improves speed by caching system prompts.\n\n# LLM_PROVIDER='lmstudio'\n# LMSTUDIO_BASE_PATH='http://your-server:1234/v1'\n# LMSTUDIO_MODEL_PREF='Loaded from Chat UI' # this is a bug in LMStudio 0.2.17\n# LMSTUDIO_MODEL_TOKEN_LIMIT=4096\n# LMSTUDIO_AUTH_TOKEN='your-lmstudio-auth-token-here'\n\n# LLM_PROVIDER='localai'\n# LOCAL_AI_BASE_PATH='http://localhost:8080/v1'\n# LOCAL_AI_MODEL_PREF='luna-ai-llama2'\n# LOCAL_AI_MODEL_TOKEN_LIMIT=4096\n# LOCAL_AI_API_KEY=\"sk-123abc\"\n\n# LLM_PROVIDER='ollama'\n# OLLAMA_BASE_PATH='http://host.docker.internal:11434'\n# OLLAMA_MODEL_PREF='llama2'\n# OLLAMA_MODEL_TOKEN_LIMIT=4096\n# OLLAMA_AUTH_TOKEN='your-ollama-auth-token-here (optional, only for ollama running behind auth - Bearer token)'\n# OLLAMA_RESPONSE_TIMEOUT=7200000 (optional, max timeout in milliseconds for ollama response to conclude. Default is 5min before aborting)\n\n# LLM_PROVIDER='togetherai'\n# TOGETHER_AI_API_KEY='my-together-ai-key'\n# TOGETHER_AI_MODEL_PREF='mistralai/Mixtral-8x7B-Instruct-v0.1'\n\n# LLM_PROVIDER='fireworksai'\n# FIREWORKS_AI_LLM_API_KEY='my-fireworks-ai-key'\n# FIREWORKS_AI_LLM_MODEL_PREF='accounts/fireworks/models/llama-v3p1-8b-instruct'\n\n# LLM_PROVIDER='perplexity'\n# PERPLEXITY_API_KEY='my-perplexity-key'\n# PERPLEXITY_MODEL_PREF='codellama-34b-instruct'\n\n# LLM_PROVIDER='deepseek'\n# DEEPSEEK_API_KEY=YOUR_API_KEY\n# DEEPSEEK_MODEL_PREF='deepseek-chat'\n\n# LLM_PROVIDER='openrouter'\n# OPENROUTER_API_KEY='my-openrouter-key'\n# OPENROUTER_MODEL_PREF='openrouter/auto'\n\n# LLM_PROVIDER='mistral'\n# MISTRAL_API_KEY='example-mistral-ai-api-key'\n# MISTRAL_MODEL_PREF='mistral-tiny'\n\n# LLM_PROVIDER='huggingface'\n# HUGGING_FACE_LLM_ENDPOINT=https://uuid-here.us-east-1.aws.endpoints.huggingface.cloud\n# HUGGING_FACE_LLM_API_KEY=hf_xxxxxx\n# HUGGING_FACE_LLM_TOKEN_LIMIT=8000\n\n# LLM_PROVIDER='groq'\n# GROQ_API_KEY=gsk_abcxyz\n# GROQ_MODEL_PREF=llama3-8b-8192\n\n# LLM_PROVIDER='koboldcpp'\n# KOBOLD_CPP_BASE_PATH='http://127.0.0.1:5000/v1'\n# KOBOLD_CPP_MODEL_PREF='koboldcpp/codellama-7b-instruct.Q4_K_S'\n# KOBOLD_CPP_MODEL_TOKEN_LIMIT=4096\n# KOBOLD_CPP_MAX_TOKENS=2048\n\n# LLM_PROVIDER='textgenwebui'\n# TEXT_GEN_WEB_UI_BASE_PATH='http://127.0.0.1:5000/v1'\n# TEXT_GEN_WEB_UI_TOKEN_LIMIT=4096\n# TEXT_GEN_WEB_UI_API_KEY='sk-123abc'\n\n# LLM_PROVIDER='generic-openai'\n# GENERIC_OPEN_AI_BASE_PATH='http://proxy.url.openai.com/v1'\n# GENERIC_OPEN_AI_MODEL_PREF='gpt-3.5-turbo'\n# GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT=4096\n# GENERIC_OPEN_AI_API_KEY=sk-123abc\n# GENERIC_OPEN_AI_CUSTOM_HEADERS=\"X-Custom-Auth:my-secret-key,X-Custom-Header:my-value\" (useful if using a proxy that requires authentication or other headers)\n\n# LLM_PROVIDER='litellm'\n# LITE_LLM_MODEL_PREF='gpt-3.5-turbo'\n# LITE_LLM_MODEL_TOKEN_LIMIT=4096\n# LITE_LLM_BASE_PATH='http://127.0.0.1:4000'\n# LITE_LLM_API_KEY='sk-123abc'\n\n# LLM_PROVIDER='novita'\n# NOVITA_LLM_API_KEY='your-novita-api-key-here' check on https://novita.ai/settings#key-management\n# NOVITA_LLM_MODEL_PREF='deepseek/deepseek-r1'\n\n# LLM_PROVIDER='cohere'\n# COHERE_API_KEY=\n# COHERE_MODEL_PREF='command-r'\n\n# LLM_PROVIDER='cometapi'\n# COMETAPI_LLM_API_KEY='your-cometapi-key-here' # Get one at https://api.cometapi.com/console/token\n# COMETAPI_LLM_MODEL_PREF='gpt-5-mini'\n# COMETAPI_LLM_TIMEOUT_MS=500 # Optional; stream idle timeout in ms (min 500ms)\n\n\n# LLM_PROVIDER='bedrock'\n# AWS_BEDROCK_LLM_ACCESS_KEY_ID=\n# AWS_BEDROCK_LLM_ACCESS_KEY=\n# AWS_BEDROCK_LLM_REGION=us-west-2\n# AWS_BEDROCK_LLM_MODEL_PREFERENCE=meta.llama3-1-8b-instruct-v1:0\n# AWS_BEDROCK_LLM_MODEL_TOKEN_LIMIT=8191\n# AWS_BEDROCK_LLM_CONNECTION_METHOD=iam\n# AWS_BEDROCK_LLM_MAX_OUTPUT_TOKENS=4096\n# AWS_BEDROCK_LLM_SESSION_TOKEN= # Only required if CONNECTION_METHOD is 'sessionToken'\n# or even use Short and Long Term API keys\n# AWS_BEDROCK_LLM_CONNECTION_METHOD=\"apiKey\"\n# AWS_BEDROCK_LLM_API_KEY=\n\n# LLM_PROVIDER='apipie'\n# APIPIE_LLM_API_KEY='sk-123abc'\n# APIPIE_LLM_MODEL_PREF='openrouter/llama-3.1-8b-instruct'\n\n# LLM_PROVIDER='xai'\n# XAI_LLM_API_KEY='xai-your-api-key-here'\n# XAI_LLM_MODEL_PREF='grok-beta'\n\n# LLM_PROVIDER='zai'\n# ZAI_API_KEY=\"your-zai-api-key-here\"\n# ZAI_MODEL_PREF=\"glm-4.5\"\n\n# LLM_PROVIDER='nvidia-nim'\n# NVIDIA_NIM_LLM_BASE_PATH='http://127.0.0.1:8000'\n# NVIDIA_NIM_LLM_MODEL_PREF='meta/llama-3.2-3b-instruct'\n\n# LLM_PROVIDER='ppio'\n# PPIO_API_KEY='your-ppio-api-key-here'\n# PPIO_MODEL_PREF='deepseek/deepseek-v3/community'\n\n# LLM_PROVIDER='moonshotai'\n# MOONSHOT_AI_API_KEY='your-moonshot-api-key-here'\n# MOONSHOT_AI_MODEL_PREF='moonshot-v1-32k'\n\n# LLM_PROVIDER='foundry'\n# FOUNDRY_BASE_PATH='http://127.0.0.1:55776'\n# FOUNDRY_MODEL_PREF='phi-3.5-mini'\n# FOUNDRY_MODEL_TOKEN_LIMIT=4096\n\n# LLM_PROVIDER='giteeai'\n# GITEE_AI_API_KEY=\n# GITEE_AI_MODEL_PREF=\n# GITEE_AI_MODEL_TOKEN_LIMIT=\n\n# LLM_PROVIDER='docker-model-runner'\n# DOCKER_MODEL_RUNNER_BASE_PATH='http://127.0.0.1:12434'\n# DOCKER_MODEL_RUNNER_LLM_MODEL_PREF='phi-3.5-mini'\n# DOCKER_MODEL_RUNNER_LLM_MODEL_TOKEN_LIMIT=4096\n\n# LLM_PROVIDER='privatemode'\n# PRIVATEMODE_LLM_BASE_PATH='http://127.0.0.1:8080'\n# PRIVATEMODE_LLM_MODEL_PREF='gemma-3-27b'\n\n# LLM_PROVIDER='sambanova'\n# SAMBANOVA_LLM_API_KEY='xxx-xxx-xxx'\n# SAMBANOVA_LLM_MODEL_PREF='gpt-oss-120b'\n\n###########################################\n######## Embedding API SElECTION ##########\n###########################################\n# This will be the assumed default embedding seleciton and model\n# EMBEDDING_ENGINE='native'\n# EMBEDDING_MODEL_PREF='Xenova/all-MiniLM-L6-v2'\n\n# Only used if you are using an LLM that does not natively support embedding (openai or Azure)\n# EMBEDDING_ENGINE='openai'\n# OPEN_AI_KEY=sk-xxxx\n# EMBEDDING_MODEL_PREF='text-embedding-ada-002'\n\n# EMBEDDING_ENGINE='azure'\n# AZURE_OPENAI_ENDPOINT=\n# AZURE_OPENAI_KEY=\n# EMBEDDING_MODEL_PREF='my-embedder-model' # This is the \"deployment\" on Azure you want to use for embeddings. Not the base model. Valid base model is text-embedding-ada-002\n\n# EMBEDDING_ENGINE='localai'\n# EMBEDDING_BASE_PATH='http://localhost:8080/v1'\n# EMBEDDING_MODEL_PREF='text-embedding-ada-002'\n# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=1000 # The max chunk size in chars a string to embed can be\n\n# EMBEDDING_ENGINE='ollama'\n# EMBEDDING_BASE_PATH='http://127.0.0.1:11434'\n# EMBEDDING_MODEL_PREF='nomic-embed-text:latest'\n# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192\n\n# EMBEDDING_ENGINE='lmstudio'\n# EMBEDDING_BASE_PATH='https://localhost:1234/v1'\n# EMBEDDING_MODEL_PREF='nomic-ai/nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.Q4_0.gguf'\n# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192\n\n# EMBEDDING_ENGINE='cohere'\n# COHERE_API_KEY=\n# EMBEDDING_MODEL_PREF='embed-english-v3.0'\n\n# EMBEDDING_ENGINE='voyageai'\n# VOYAGEAI_API_KEY=\n# EMBEDDING_MODEL_PREF='voyage-large-2-instruct'\n\n# EMBEDDING_ENGINE='litellm'\n# EMBEDDING_MODEL_PREF='text-embedding-ada-002'\n# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192\n# LITE_LLM_BASE_PATH='http://127.0.0.1:4000'\n# LITE_LLM_API_KEY='sk-123abc'\n\n# EMBEDDING_ENGINE='generic-openai'\n# EMBEDDING_MODEL_PREF='text-embedding-ada-002'\n# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192\n# EMBEDDING_BASE_PATH='http://127.0.0.1:4000'\n# GENERIC_OPEN_AI_EMBEDDING_API_KEY='sk-123abc'\n# GENERIC_OPEN_AI_EMBEDDING_MAX_CONCURRENT_CHUNKS=500\n# GENERIC_OPEN_AI_EMBEDDING_API_DELAY_MS=1000\n\n# EMBEDDING_ENGINE='gemini'\n# GEMINI_EMBEDDING_API_KEY=\n# EMBEDDING_MODEL_PREF='text-embedding-004'\n\n# EMBEDDING_ENGINE='openrouter'\n# EMBEDDING_MODEL_PREF='baai/bge-m3'\n# OPENROUTER_API_KEY=''\n\n# EMBEDDING_ENGINE='lemonade'\n# EMBEDDING_BASE_PATH='http://127.0.0.1:8000'\n# EMBEDDING_MODEL_PREF='Qwen3-embedder'\n# EMBEDDING_MODEL_MAX_CHUNK_LENGTH=8192\n\n###########################################\n######## Vector Database Selection ########\n###########################################\n# Enable all below if you are using vector database: Chroma.\n# VECTOR_DB=\"chroma\"\n# CHROMA_ENDPOINT='http://localhost:8000'\n# CHROMA_API_HEADER=\"X-Api-Key\"\n# CHROMA_API_KEY=\"sk-123abc\"\n\n# Enable all below if you are using vector database: Chroma Cloud.\n# VECTOR_DB=\"chromacloud\"\n# CHROMACLOUD_API_KEY=\"ck-your-api-key\"\n# CHROMACLOUD_TENANT=\n# CHROMACLOUD_DATABASE=\n\n# Enable all below if you are using vector database: Pinecone.\n# VECTOR_DB=\"pinecone\"\n# PINECONE_API_KEY=\n# PINECONE_INDEX=\n\n# Enable all below if you are using vector database: Astra DB.\n# VECTOR_DB=\"astra\"\n# ASTRA_DB_APPLICATION_TOKEN=\n# ASTRA_DB_ENDPOINT=\n\n# Enable all below if you are using vector database: LanceDB.\nVECTOR_DB=\"lancedb\"\n\n# Enable all below if you are using vector database: PG Vector.\n# VECTOR_DB=\"pgvector\"\n# PGVECTOR_CONNECTION_STRING=\"postgresql://dbuser:dbuserpass@localhost:5432/yourdb\"\n# PGVECTOR_TABLE_NAME=\"anythingllm_vectors\" # optional, but can be defined\n\n# Enable all below if you are using vector database: Weaviate.\n# VECTOR_DB=\"weaviate\"\n# WEAVIATE_ENDPOINT=\"http://localhost:8080\"\n# WEAVIATE_API_KEY=\n\n# Enable all below if you are using vector database: Qdrant.\n# VECTOR_DB=\"qdrant\"\n# QDRANT_ENDPOINT=\"http://localhost:6333\"\n# QDRANT_API_KEY=\n\n# Enable all below if you are using vector database: Milvus.\n# VECTOR_DB=\"milvus\"\n# MILVUS_ADDRESS=\"http://localhost:19530\"\n# MILVUS_USERNAME=\n# MILVUS_PASSWORD=\n\n# Enable all below if you are using vector database: Zilliz Cloud.\n# VECTOR_DB=\"zilliz\"\n# ZILLIZ_ENDPOINT=\"https://sample.api.gcp-us-west1.zillizcloud.com\"\n# ZILLIZ_API_TOKEN=api-token-here\n\n###########################################\n######## Audio Model Selection ############\n###########################################\n# (default) use built-in whisper-small model.\nWHISPER_PROVIDER=\"local\"\n\n# use openai hosted whisper model.\n# WHISPER_PROVIDER=\"openai\"\n# OPEN_AI_KEY=sk-xxxxxxxx\n\n###########################################\n######## TTS/STT Model Selection ##########\n###########################################\nTTS_PROVIDER=\"native\"\n\n# TTS_PROVIDER=\"openai\"\n# TTS_OPEN_AI_KEY=sk-example\n# TTS_OPEN_AI_VOICE_MODEL=nova\n\n# TTS_PROVIDER=\"elevenlabs\"\n# TTS_ELEVEN_LABS_KEY=\n# TTS_ELEVEN_LABS_VOICE_MODEL=21m00Tcm4TlvDq8ikWAM # Rachel\n\n# TTS_PROVIDER=\"generic-openai\"\n# TTS_OPEN_AI_COMPATIBLE_KEY=sk-example\n# TTS_OPEN_AI_COMPATIBLE_MODEL=tts-1\n# TTS_OPEN_AI_COMPATIBLE_VOICE_MODEL=nova\n# TTS_OPEN_AI_COMPATIBLE_ENDPOINT=\"https://api.openai.com/v1\"\n\n# CLOUD DEPLOYMENT VARIRABLES ONLY\n# AUTH_TOKEN=\"hunter2\" # This is the password to your application if remote hosting.\n# STORAGE_DIR= # absolute filesystem path with no trailing slash\n\n###########################################\n######## PASSWORD COMPLEXITY ##############\n###########################################\n# Enforce a password schema for your organization users.\n# Documentation on how to use https://github.com/kamronbatman/joi-password-complexity\n#PASSWORDMINCHAR=8\n#PASSWORDMAXCHAR=250\n#PASSWORDLOWERCASE=1\n#PASSWORDUPPERCASE=1\n#PASSWORDNUMERIC=1\n#PASSWORDSYMBOL=1\n#PASSWORDREQUIREMENTS=4\n\n###########################################\n######## ENABLE HTTPS SERVER ##############\n###########################################\n# By enabling this and providing the path/filename for the key and cert,\n# the server will use HTTPS instead of HTTP.\n#ENABLE_HTTPS=\"true\"\n#HTTPS_CERT_PATH=\"sslcert/cert.pem\"\n#HTTPS_KEY_PATH=\"sslcert/key.pem\"\n\n###########################################\n######## AGENT SERVICE KEYS ###############\n###########################################\n\n#------ SEARCH ENGINES -------\n#=============================\n#------ Google Search -------- https://programmablesearchengine.google.com/controlpanel/create\n# AGENT_GSE_KEY=\n# AGENT_GSE_CTX=\n\n#------ SerpApi ----------- https://serpapi.com/\n# AGENT_SERPAPI_API_KEY=\n# AGENT_SERPAPI_ENGINE=google\n\n#------ SearchApi.io ----------- https://www.searchapi.io/\n# AGENT_SEARCHAPI_API_KEY=\n# AGENT_SEARCHAPI_ENGINE=google\n\n#------ Serper.dev ----------- https://serper.dev/\n# AGENT_SERPER_DEV_KEY=\n\n#------ Bing Search ----------- https://portal.azure.com/\n# AGENT_BING_SEARCH_API_KEY=\n\n#------ Serply.io ----------- https://serply.io/\n# AGENT_SERPLY_API_KEY=\n\n#------ SearXNG ----------- https://github.com/searxng/searxng\n# AGENT_SEARXNG_API_URL=\n\n#------ Tavily ----------- https://www.tavily.com/\n# AGENT_TAVILY_API_KEY=\n\n#------ Exa Search ----------- https://www.exa.ai/\n# AGENT_EXA_API_KEY=\n\n#------ Perplexity Search ----------- [https://console.perplexity.ai](https://console.perplexity.ai)\n# AGENT_PERPLEXITY_API_KEY=\n\n###########################################\n######## Other Configurations ############\n###########################################\n\n# Disable viewing chat history from the UI and frontend APIs.\n# See https://docs.anythingllm.com/configuration#disable-view-chat-history for more information.\n# DISABLE_VIEW_CHAT_HISTORY=1\n\n# Enable simple SSO passthrough to pre-authenticate users from a third party service.\n# See https://docs.anythingllm.com/configuration#simple-sso-passthrough for more information.\n# SIMPLE_SSO_ENABLED=1\n# SIMPLE_SSO_NO_LOGIN=1\n# SIMPLE_SSO_NO_LOGIN_REDIRECT=https://your-custom-login-url.com (optional)\n\n# Allow scraping of any IP address in collector - must be string \"true\" to be enabled\n# See https://docs.anythingllm.com/configuration#local-ip-address-scraping for more information.\n# COLLECTOR_ALLOW_ANY_IP=\"true\"\n\n# Specify the target languages for when using OCR to parse images and PDFs.\n# This is a comma separated list of language codes as a string. Unsupported languages will be ignored.\n# Default is English. See https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html for a list of valid language codes.\n# TARGET_OCR_LANG=eng,deu,ita,spa,fra,por,rus,nld,tur,hun,pol,ita,spa,fra,por,rus,nld,tur,hun,pol\n\n# Runtime flags for built-in pupeeteer Chromium instance\n# This is only required on Linux machines running AnythingLLM via Docker\n# and do not want to use the --cap-add=SYS_ADMIN docker argument\n# ANYTHINGLLM_CHROMIUM_ARGS=\"--no-sandbox,--disable-setuid-sandbox\"\n\n# This enables HTTP request/response logging in development. Set value to a truthy string to enable, leave empty value or comment out to disable.\n# ENABLE_HTTP_LOGGER=\"\"\n# This enables timestamps for the HTTP Logger. Set value to a truthy string to enable, leave empty value or comment out to disable.\n# ENABLE_HTTP_LOGGER_TIMESTAMPS=\"\"\n\n# Disable Swagger API documentation endpoint.\n# Set to \"true\" to disable the /api/docs endpoint (recommended for production deployments).\n# DISABLE_SWAGGER_DOCS=\"true\"\n\n# Disable MCP cooldown timer for agent calls\n# this can lead to infinite recursive calls of the same function\n# for some model/provider combinations\n# MCP_NO_COOLDOWN=\"true\n\n# Allow native tool calling for specific providers.\n# This can VASTLY improve performance and speed of agent calls.\n# Check code for supported providers who can be enabled here via this flag\n# PROVIDER_SUPPORTS_NATIVE_TOOL_CALLING=\"generic-openai,bedrock,localai,groq,litellm,openrouter\"\n\n# (optional) Maximum number of tools an agent can chain for a single response.\n# This prevents some lower-end models from infinite recursive tool calls.\n# AGENT_MAX_TOOL_CALLS=10\n\n# Enable agent tool reranking to reduce token usage by selecting only the most relevant tools\n# for each query. Uses the native embedding reranker to score tools against the user's prompt.\n# Set to \"true\" to enable. This can reduce token costs by 80% when you have\n# many tools/MCP servers enabled.\n# AGENT_SKILL_RERANKER_ENABLED=\"true\"\n# AGENT_SKILL_RERANKER_TOP_N=15 # (optional) Number of top tools to keep after reranking (default: 15)"
  },
  {
    "path": "server/.flowconfig",
    "content": "# How to config: https://flow.org/en/docs/config/\n[version]\n\n[options]\nall=false\nemoji=false\ninclude_warnings=false\nlazy_mode=false\n\n[include]\n\n[ignore]\n.*/node_modules/resolve/test/.*\n\n[untyped]\n# <PROJECT_ROOT>/src/models/.*\n\n[declarations]\n\n[libs]\n\n[lints]\nall=warn\n\n[strict]\nnonstrict-import\nunclear-type\nunsafe-getters-setters\nuntyped-import\nuntyped-type-import\n"
  },
  {
    "path": "server/.gitignore",
    "content": ".env.production\n.env.development\n.env.test\nserver/.env\nstorage/assets/*\n!storage/assets/anything-llm.png\nstorage/documents/*\nstorage/comkey/*\nstorage/tmp/*\nstorage/vector-cache/*.json\nstorage/exports\nstorage/imports\nstorage/plugins/agent-skills/*\nstorage/plugins/agent-flows/*\nstorage/plugins/office-extensions/*\nstorage/plugins/anythingllm_mcp_servers.json\n!storage/documents/DOCUMENTS.md\nstorage/push-notifications/*\nstorage/direct-uploads\nlogs/server.log\n*.db\n*.db-journal\nstorage/lancedb\npublic/\n\n# For legacy copies of repo\ndocuments\nvector-cache\nyarn-error.log\n\n# Local SSL Certs for HTTPS\nsslcert\n"
  },
  {
    "path": "server/.nvmrc",
    "content": "v18.18.0"
  },
  {
    "path": "server/__tests__/models/systemPromptVariables.test.js",
    "content": "const { SystemPromptVariables } = require(\"../../models/systemPromptVariables\");\nconst prisma = require(\"../../utils/prisma\");\n\nconst mockUser = {\n  id: 1,\n  username: \"john.doe\",\n  bio: \"I am a test user\",\n};\n\nconst mockWorkspace = {\n  id: 1,\n  name: \"Test Workspace\",\n  slug: 'test-workspace',\n};\n\nconst mockSystemPromptVariables = [\n  {\n    id: 1,\n    key: \"mystaticvariable\",\n    value: \"AnythingLLM testing runtime\",\n    description: \"A test variable\",\n    type: \"static\",\n    userId: null,\n  },\n];\n\ndescribe(\"SystemPromptVariables.expandSystemPromptVariables\", () => {\n  beforeEach(() => {\n    jest.clearAllMocks();\n    // Mock just the Prisma actions since that is what is used by default values\n    prisma.system_prompt_variables.findMany = jest.fn().mockResolvedValue(mockSystemPromptVariables);\n    prisma.workspaces.findUnique = jest.fn().mockResolvedValue(mockWorkspace);\n    prisma.users.findUnique = jest.fn().mockResolvedValue(mockUser);\n  });\n\n  it(\"should expand user-defined system prompt variables\", async () => {\n    const variables = await SystemPromptVariables.expandSystemPromptVariables(\"Hello {mystaticvariable}\");\n    expect(variables).toBe(`Hello ${mockSystemPromptVariables[0].value}`);\n  });\n\n  it(\"should expand workspace-defined system prompt variables\", async () => {\n    const variables = await SystemPromptVariables.expandSystemPromptVariables(\"Hello {workspace.name}\", null, mockWorkspace.id);\n    expect(variables).toBe(`Hello ${mockWorkspace.name}`);\n  });\n\n  it(\"should expand user-defined system prompt variables\", async () => {\n    const variables = await SystemPromptVariables.expandSystemPromptVariables(\"Hello {user.name}\", mockUser.id);\n    expect(variables).toBe(`Hello ${mockUser.username}`);\n  });\n\n  it(\"should work with any combination of variables\", async () => {\n    const variables = await SystemPromptVariables.expandSystemPromptVariables(\"Hello {mystaticvariable} {workspace.name} {user.name}\", mockUser.id, mockWorkspace.id);\n    expect(variables).toBe(`Hello ${mockSystemPromptVariables[0].value} ${mockWorkspace.name} ${mockUser.username}`);\n  });\n\n  it('should fail gracefully with invalid variables that are undefined for any reason', async () => {\n    // Undefined sub-fields on valid classes are push to a placeholder [Class prop]. This is expected behavior.\n    const variables = await SystemPromptVariables.expandSystemPromptVariables(\"Hello {invalid.variable} {user.password} the current user is {user.name} on workspace id #{workspace.id}\", null, null);\n    expect(variables).toBe(\"Hello {invalid.variable} [User password] the current user is [User name] on workspace id #[Workspace ID]\");\n  });\n});"
  },
  {
    "path": "server/__tests__/models/user.test.js",
    "content": "const { User } = require(\"../../models/user\");\n\ndescribe(\"username validation restrictions\", () => {\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  const failureMessages = [\n    \"Username cannot be longer than 32 characters\",\n    \"Username must be at least 2 characters\",\n    \"Username must start with a lowercase letter and only contain lowercase letters, numbers, underscores, hyphens, and periods\",\n  ];\n\n  it(\"should throw an error if the username is longer than 32 characters\", () => {\n    expect(() => User.validations.username(\"a\".repeat(33))).toThrow(failureMessages[0]);\n  });\n\n  it(\"should throw an error if the username is less than 2 characters\", () => {\n    expect(() => User.validations.username(\"a\")).toThrow(failureMessages[1]);\n  });\n\n  it(\"should throw an error if the username does not start with a lowercase letter\", () => {\n    expect(() => User.validations.username(\"Aa1\")).toThrow(failureMessages[2]);\n  });\n\n  it(\"should throw an error if the username contains invalid characters\", () => {\n    expect(() => User.validations.username(\"ad-123_456.789*\")).toThrow(failureMessages[2]);\n    expect(() => User.validations.username(\"ad-123_456#456\")).toThrow(failureMessages[2]);\n    expect(() => User.validations.username(\"ad-123_456!456\")).toThrow(failureMessages[2]);\n  });\n\n  it(\"should return the username if it is valid or an email address\", () => {\n    expect(User.validations.username(\"a123_456.789@\")).toBe(\"a123_456.789@\");\n    expect(User.validations.username(\"a123_456.789@example.com\")).toBe(\"a123_456.789@example.com\");\n  });\n\n  it(\"should throw an error if the username is not a string\", () => {\n    expect(() => User.validations.username(123)).toThrow(failureMessages[2]);\n    expect(() => User.validations.username(null)).not.toThrow();\n    expect(() => User.validations.username(undefined)).toThrow(failureMessages[1]);\n    expect(() => User.validations.username({})).toThrow(failureMessages[3]);\n    expect(() => User.validations.username([])).toThrow(failureMessages[3]);\n    expect(() => User.validations.username(true)).not.toThrow();\n    expect(() => User.validations.username(false)).not.toThrow();\n  });\n});"
  },
  {
    "path": "server/__tests__/utils/SQLConnectors/connectionParser.test.js",
    "content": "/* eslint-env jest */\nconst { ConnectionStringParser } = require(\"../../../utils/agents/aibitat/plugins/sql-agent/SQLConnectors/utils\");\n\ndescribe(\"ConnectionStringParser\", () => {\n  describe(\"Basic Parsing\", () => {\n    test(\"should parse a basic connection string without options\", () => {\n      const parser = new ConnectionStringParser({ scheme: \"mssql\" });\n      const result = parser.parse(\"mssql://user:pass@localhost:1433/mydb\");\n\n      expect(result).toEqual({\n        scheme: \"mssql\",\n        username: \"user\",\n        password: \"pass\",\n        hosts: [{ host: \"localhost\", port: 1433 }],\n        endpoint: \"mydb\",\n        options: undefined\n      });\n    });\n\n    test(\"should parse a connection string with options\", () => {\n      const parser = new ConnectionStringParser({ scheme: \"mssql\" });\n      const result = parser.parse(\"mssql://user:pass@localhost:1433/mydb?encrypt=true&trustServerCertificate=true\");\n\n      expect(result).toEqual({\n        scheme: \"mssql\",\n        username: \"user\",\n        password: \"pass\",\n        hosts: [{ host: \"localhost\", port: 1433 }],\n        endpoint: \"mydb\",\n        options: {\n          encrypt: \"true\",\n          trustServerCertificate: \"true\"\n        }\n      });\n    });\n\n    test(\"should handle empty passwords\", () => {\n      const parser = new ConnectionStringParser({ scheme: \"mssql\" });\n      const result = parser.parse(\"mssql://user@localhost:1433/mydb\");\n\n      expect(result).toEqual({\n        scheme: \"mssql\",\n        username: \"user\",\n        password: undefined,\n        hosts: [{ host: \"localhost\", port: 1433 }],\n        endpoint: \"mydb\",\n        options: undefined\n      });\n    });\n  });\n\n  describe(\"Error Handling\", () => {\n    test(\"should throw error for invalid scheme\", () => {\n      const parser = new ConnectionStringParser({ scheme: \"mssql\" });\n      expect(() => parser.parse(\"mysql://user:pass@localhost:3306/mydb\"))\n        .toThrow(\"URI must start with 'mssql://'\");\n    });\n\n    test(\"should throw error for missing scheme\", () => {\n      const parser = new ConnectionStringParser({ scheme: \"mssql\" });\n      expect(() => parser.parse(\"user:pass@localhost:1433/mydb\"))\n        .toThrow(\"No scheme found in URI\");\n    });\n  });\n\n  describe(\"Special Characters\", () => {\n    test(\"should handle special characters in username and password\", () => {\n      const parser = new ConnectionStringParser({ scheme: \"mssql\" });\n      const result = parser.parse(\"mssql://user%40domain:p%40ssw%3Ard@localhost:1433/mydb\");\n\n      expect(result).toEqual({\n        scheme: \"mssql\",\n        username: \"user@domain\",\n        password: \"p@ssw:rd\",\n        hosts: [{ host: \"localhost\", port: 1433 }],\n        endpoint: \"mydb\",\n        options: undefined\n      });\n    });\n\n    test(\"should handle special characters in database name\", () => {\n      const parser = new ConnectionStringParser({ scheme: \"mssql\" });\n      const result = parser.parse(\"mssql://user:pass@localhost:1433/my%20db\");\n\n      expect(result).toEqual({\n        scheme: \"mssql\",\n        username: \"user\",\n        password: \"pass\",\n        hosts: [{ host: \"localhost\", port: 1433 }],\n        endpoint: \"my db\",\n        options: undefined\n      });\n    });\n  });\n\n  describe(\"Multiple Hosts\", () => {\n    test(\"should parse multiple hosts\", () => {\n      const parser = new ConnectionStringParser({ scheme: \"mssql\" });\n      const result = parser.parse(\"mssql://user:pass@host1:1433,host2:1434/mydb\");\n\n      expect(result).toEqual({\n        scheme: \"mssql\",\n        username: \"user\",\n        password: \"pass\",\n        hosts: [\n          { host: \"host1\", port: 1433 },\n          { host: \"host2\", port: 1434 }\n        ],\n        endpoint: \"mydb\",\n        options: undefined\n      });\n    });\n\n    test(\"should handle hosts without ports\", () => {\n      const parser = new ConnectionStringParser({ scheme: \"mssql\" });\n      const result = parser.parse(\"mssql://user:pass@host1,host2/mydb\");\n\n      expect(result).toEqual({\n        scheme: \"mssql\",\n        username: \"user\",\n        password: \"pass\",\n        hosts: [\n          { host: \"host1\" },\n          { host: \"host2\" }\n        ],\n        endpoint: \"mydb\",\n        options: undefined\n      });\n    });\n  });\n\n  describe(\"Provider-Specific Tests\", () => {\n    test(\"should parse MySQL connection string\", () => {\n      const parser = new ConnectionStringParser({ scheme: \"mysql\" });\n      const result = parser.parse(\"mysql://user:pass@localhost:3306/mydb?ssl=true\");\n\n      expect(result).toEqual({\n        scheme: \"mysql\",\n        username: \"user\",\n        password: \"pass\",\n        hosts: [{ host: \"localhost\", port: 3306 }],\n        endpoint: \"mydb\",\n        options: { ssl: \"true\" }\n      });\n    });\n\n    test(\"should parse PostgreSQL connection string\", () => {\n      const parser = new ConnectionStringParser({ scheme: \"postgresql\" });\n      const result = parser.parse(\"postgresql://user:pass@localhost:5432/mydb?sslmode=require\");\n\n      expect(result).toEqual({\n        scheme: \"postgresql\",\n        username: \"user\",\n        password: \"pass\",\n        hosts: [{ host: \"localhost\", port: 5432 }],\n        endpoint: \"mydb\",\n        options: { sslmode: \"require\" }\n      });\n    });\n\n    test(\"should parse MSSQL connection string with encryption options\", () => {\n      const parser = new ConnectionStringParser({ scheme: \"mssql\" });\n      const result = parser.parse(\"mssql://user:pass@localhost:1433/mydb?encrypt=true&trustServerCertificate=true\");\n\n      expect(result).toEqual({\n        scheme: \"mssql\",\n        username: \"user\",\n        password: \"pass\",\n        hosts: [{ host: \"localhost\", port: 1433 }],\n        endpoint: \"mydb\",\n        options: {\n          encrypt: \"true\",\n          trustServerCertificate: \"true\"\n        }\n      });\n    });\n  });\n});"
  },
  {
    "path": "server/__tests__/utils/TextSplitter/index.test.js",
    "content": "const { TextSplitter } = require(\"../../../utils/TextSplitter\");\nconst _ = require(\"lodash\");\n\ndescribe(\"TextSplitter\", () => {\n  test(\"should split long text into n sized chunks\", async () => {\n    const text = \"This is a test text to be split into chunks\".repeat(2);\n    const textSplitter = new TextSplitter({\n      chunkSize: 20,\n      chunkOverlap: 0,\n    });\n    const chunks = await textSplitter.splitText(text);\n    expect(chunks.length).toEqual(5);\n  });\n\n  test(\"applies chunk overlap of 20 characters on invalid chunkOverlap\", async () => {\n    const text = \"This is a test text to be split into chunks\".repeat(2);\n    const textSplitter = new TextSplitter({\n      chunkSize: 30,\n    });\n    const chunks = await textSplitter.splitText(text);\n    expect(chunks.length).toEqual(6);\n  });\n\n  test(\"does not allow chunkOverlap to be greater than chunkSize\", async () => {\n    expect(() => {\n      new TextSplitter({\n        chunkSize: 20,\n        chunkOverlap: 21,\n      });\n    }).toThrow();\n  });\n\n  test(\"applies specific metadata to stringifyHeader to each chunk\", async () => {\n    const metadata = {\n      id: \"123e4567-e89b-12d3-a456-426614174000\",\n      url: \"https://example.com\",\n      title: \"Example\",\n      docAuthor: \"John Doe\",\n      published: \"2021-01-01\",\n      chunkSource: \"link://https://example.com\",\n      description: \"This is a test text to be split into chunks\",\n    };\n    const chunkHeaderMeta = TextSplitter.buildHeaderMeta(metadata);\n    expect(chunkHeaderMeta).toEqual({\n      sourceDocument: metadata.title,\n      source: metadata.url,\n      published: metadata.published,\n    });\n  });\n\n  test(\"applies a valid chunkPrefix to each chunk\", async () => {\n    const text = \"This is a test text to be split into chunks\".repeat(2);\n    let textSplitter = new TextSplitter({\n      chunkSize: 20,\n      chunkOverlap: 0,\n      chunkPrefix: \"testing: \",\n    });\n    let chunks = await textSplitter.splitText(text);\n    expect(chunks.length).toEqual(5);\n    expect(chunks.every(chunk => chunk.startsWith(\"testing: \"))).toBe(true);\n\n    textSplitter = new TextSplitter({\n      chunkSize: 20,\n      chunkOverlap: 0,\n      chunkPrefix: \"testing2: \",\n    });\n    chunks = await textSplitter.splitText(text);\n    expect(chunks.length).toEqual(5);\n    expect(chunks.every(chunk => chunk.startsWith(\"testing2: \"))).toBe(true);\n\n    textSplitter = new TextSplitter({\n      chunkSize: 20,\n      chunkOverlap: 0,\n      chunkPrefix: undefined,\n    });\n    chunks = await textSplitter.splitText(text);\n    expect(chunks.length).toEqual(5);\n    expect(chunks.every(chunk => !chunk.startsWith(\": \"))).toBe(true);\n\n    textSplitter = new TextSplitter({\n      chunkSize: 20,\n      chunkOverlap: 0,\n      chunkPrefix: \"\",\n    });\n    chunks = await textSplitter.splitText(text);\n    expect(chunks.length).toEqual(5);\n    expect(chunks.every(chunk => !chunk.startsWith(\": \"))).toBe(true);\n\n    // Applied chunkPrefix with chunkHeaderMeta\n    textSplitter = new TextSplitter({\n      chunkSize: 20,\n      chunkOverlap: 0,\n      chunkHeaderMeta: TextSplitter.buildHeaderMeta({\n        title: \"Example\",\n        url: \"https://example.com\",\n        published: \"2021-01-01\",\n      }),\n      chunkPrefix: \"testing3: \",\n    });\n    chunks = await textSplitter.splitText(text);\n    expect(chunks.length).toEqual(5);\n    expect(chunks.every(chunk => chunk.startsWith(\"testing3: <document_metadata>\"))).toBe(true);\n  });\n});\n"
  },
  {
    "path": "server/__tests__/utils/agentFlows/executor.test.js",
    "content": "const { FlowExecutor } = require(\"../../../utils/agentFlows/executor\");\n\ndescribe(\"FlowExecutor: getValueFromPath\", () => {\n  const executor = new FlowExecutor();\n\n  it(\"can handle invalid objects\", () => {\n    expect(executor.getValueFromPath(null, \"a.b.c\")).toBe(\"\");\n    expect(executor.getValueFromPath(undefined, \"a.b.c\")).toBe(\"\");\n    expect(executor.getValueFromPath(1, \"a.b.c\")).toBe(\"\");\n    expect(executor.getValueFromPath(\"string\", \"a.b.c\")).toBe(\"\");\n    expect(executor.getValueFromPath(true, \"a.b.c\")).toBe(\"\");\n  });\n\n  it(\"can handle invalid paths\", () => {\n    const obj = { a: { b: { c: \"answer\" } } };\n    expect(executor.getValueFromPath(obj, -1)).toBe(\"\");\n    expect(executor.getValueFromPath(obj, undefined)).toBe(\"\");\n    expect(executor.getValueFromPath(obj, [1, 2, 3])).toBe(\"\");\n    expect(executor.getValueFromPath(obj, () => { })).toBe(\"\");\n  });\n\n  it(\"should be able to resolve a value from a dot path at various levels\", () => {\n    let obj = {\n      a: {\n        prop: \"top-prop\",\n        b: {\n          c: \"answer\",\n          num: 100,\n          arr: [1, 2, 3],\n          subarr: [\n            { id: 1, name: \"answer2\" },\n            { id: 2, name: \"answer3\" },\n            { id: 3, name: \"answer4\" },\n          ]\n        }\n      }\n    };\n    expect(executor.getValueFromPath(obj, \"a.prop\")).toBe(\"top-prop\");\n    expect(executor.getValueFromPath(obj, \"a.b.c\")).toBe(\"answer\");\n    expect(executor.getValueFromPath(obj, \"a.b.num\")).toBe(100);\n    expect(executor.getValueFromPath(obj, \"a.b.arr[0]\")).toBe(1);\n    expect(executor.getValueFromPath(obj, \"a.b.arr[1]\")).toBe(2);\n    expect(executor.getValueFromPath(obj, \"a.b.arr[2]\")).toBe(3);\n    expect(executor.getValueFromPath(obj, \"a.b.subarr[0].id\")).toBe(1);\n    expect(executor.getValueFromPath(obj, \"a.b.subarr[0].name\")).toBe(\"answer2\");\n    expect(executor.getValueFromPath(obj, \"a.b.subarr[1].id\")).toBe(2);\n    expect(executor.getValueFromPath(obj, \"a.b.subarr[2].name\")).toBe(\"answer4\");\n    expect(executor.getValueFromPath(obj, \"a.b.subarr[2].id\")).toBe(3);\n  });\n\n  it(\"should return empty string if the path is invalid\", () => {\n    const result = executor.getValueFromPath({}, \"a.b.c\");\n    expect(result).toBe(\"\");\n  });\n\n  it(\"should return empty string if the object is invalid\", () => {\n    const result = executor.getValueFromPath(null, \"a.b.c\");\n    expect(result).toBe(\"\");\n  });\n\n  it(\"can return a stringified item if the path target is not an object or array\", () => {\n    const obj = { a: { b: { c: \"answer\", numbers: [1, 2, 3] } } };\n    expect(executor.getValueFromPath(obj, \"a.b\")).toEqual(JSON.stringify(obj.a.b));\n    expect(executor.getValueFromPath(obj, \"a.b.numbers\")).toEqual(JSON.stringify(obj.a.b.numbers));\n    expect(executor.getValueFromPath(obj, \"a.b.c\")).toBe(\"answer\");\n  });\n\n  it(\"can return a stringified object if the path target is an array\", () => {\n    const obj = { a: { b: [1, 2, 3] } };\n    expect(executor.getValueFromPath(obj, \"a.b\")).toEqual(JSON.stringify(obj.a.b));\n    expect(executor.getValueFromPath(obj, \"a.b[0]\")).toBe(1);\n    expect(executor.getValueFromPath(obj, \"a.b[1]\")).toBe(2);\n    expect(executor.getValueFromPath(obj, \"a.b[2]\")).toBe(3);\n  });\n\n  it(\"can find a value by string key traversal\", () => {\n    const obj = {\n      a: {\n        items: [\n          {\n            'my-long-key': [\n              { id: 1, name: \"answer1\" },\n              { id: 2, name: \"answer2\" },\n              { id: 3, name: \"answer3\" },\n            ]\n          },\n        ],\n      }\n    };\n    expect(executor.getValueFromPath(obj, \"a.items[0]['my-long-key'][1].id\")).toBe(2);\n    expect(executor.getValueFromPath(obj, \"a.items[0]['my-long-key'][1].name\")).toBe(\"answer2\");\n  });\n});"
  },
  {
    "path": "server/__tests__/utils/agents/aibitat/providers/helpers/untooled.test.js",
    "content": "const UnTooled = require(\"../../../../../../utils/agents/aibitat/providers/helpers/untooled\");\n\ndescribe(\"UnTooled: validFuncCall\", () => {\n  const untooled = new UnTooled();\n  const validFunc = {\n    \"name\": \"brave-search-brave_web_search\",\n    \"description\": \"Example function\",\n    \"parameters\": {\n      \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"query\": {\n          \"type\": \"string\",\n          \"description\": \"Search query (max 400 chars, 50 words)\"\n        },\n        \"count\": {\n          \"type\": \"number\",\n          \"description\": \"Number of results (1-20, default 10)\",\n          \"default\": 10\n        },\n        \"offset\": {\n          \"type\": \"number\",\n          \"description\": \"Pagination offset (max 9, default 0)\",\n          \"default\": 0\n        }\n      },\n      \"required\": [\n        \"query\"\n      ]\n    }\n  };\n\n  it(\"Be truthy if the function call is valid and has all required arguments\", () => {\n    const result = untooled.validFuncCall(\n      {\n        name: validFunc.name,\n        arguments: { query: \"test\" },\n      }, [validFunc]);\n    expect(result.valid).toBe(true);\n    expect(result.reason).toBe(null);\n  });\n\n  it(\"Be falsey if the function call has no name or arguments\", () => {\n    const result = untooled.validFuncCall(\n      { arguments: {} }, [validFunc]);\n    expect(result.valid).toBe(false);\n    expect(result.reason).toBe(\"Missing name or arguments in function call.\");\n\n    const result2 = untooled.validFuncCall(\n      { name: validFunc.name }, [validFunc]);\n    expect(result2.valid).toBe(false);\n    expect(result2.reason).toBe(\"Missing name or arguments in function call.\");\n  });\n\n  it(\"Be falsey if the function call references an unknown function definition\", () => {\n    const result = untooled.validFuncCall(\n      {\n        name: \"unknown-function\",\n        arguments: {},\n      }, [validFunc]);\n    expect(result.valid).toBe(false);\n    expect(result.reason).toBe(\"Function name does not exist.\");\n  });\n\n  it(\"Be falsey if the function call is valid but missing any required arguments\", () => {\n    const result = untooled.validFuncCall(\n      {\n        name: validFunc.name,\n        arguments: {},\n      }, [validFunc]);\n    expect(result.valid).toBe(false);\n    expect(result.reason).toBe(\"Missing required argument: query\");\n  });\n\n  it(\"Be falsey if the function call is valid but has an unknown argument defined (required or not)\", () => {\n    const result = untooled.validFuncCall(\n      {\n        name: validFunc.name,\n        arguments: {\n          query: \"test\",\n          unknown: \"unknown\",\n        },\n      }, [validFunc]);\n    expect(result.valid).toBe(false);\n    expect(result.reason).toBe(\"Unknown argument: unknown provided but not in schema.\");\n  });\n});"
  },
  {
    "path": "server/__tests__/utils/agents/defaults.test.js",
    "content": "// Set required env vars before requiring modules\nprocess.env.STORAGE_DIR = __dirname;\nprocess.env.NODE_ENV = \"test\";\n\nconst { SystemPromptVariables } = require(\"../../../models/systemPromptVariables\");\nconst Provider = require(\"../../../utils/agents/aibitat/providers/ai-provider\");\n\njest.mock(\"../../../models/systemPromptVariables\");\njest.mock(\"../../../models/systemSettings\");\njest.mock(\"../../../utils/agents/imported\", () => ({\n  activeImportedPlugins: jest.fn().mockReturnValue([]),\n}));\njest.mock(\"../../../utils/agentFlows\", () => ({\n  AgentFlows: {\n    activeFlowPlugins: jest.fn().mockReturnValue([]),\n  },\n}));\njest.mock(\"../../../utils/MCP\", () => {\n  return jest.fn().mockImplementation(() => ({\n    activeMCPServers: jest.fn().mockResolvedValue([]),\n  }));\n});\n\nconst { WORKSPACE_AGENT } = require(\"../../../utils/agents/defaults\");\n\ndescribe(\"WORKSPACE_AGENT.getDefinition\", () => {\n  beforeEach(() => {\n    jest.clearAllMocks();\n    // Mock SystemSettings to return empty arrays for agent skills\n    const { SystemSettings } = require(\"../../../models/systemSettings\");\n    SystemSettings.getValueOrFallback = jest.fn().mockResolvedValue(\"[]\");\n  });\n\n  it(\"should use provider default system prompt when workspace has no openAiPrompt\", async () => {\n    const workspace = {\n      id: 1,\n      name: \"Test Workspace\",\n      openAiPrompt: null,\n    };\n    const user = { id: 1 };\n    const provider = \"openai\";\n    const expectedPrompt = await Provider.systemPrompt({ provider, workspace, user });\n    const definition = await WORKSPACE_AGENT.getDefinition(\n      provider,\n      workspace,\n      user\n    );\n    expect(definition.role).toBe(expectedPrompt);\n    expect(SystemPromptVariables.expandSystemPromptVariables).not.toHaveBeenCalled();\n  });\n\n  it(\"should use workspace system prompt with variable expansion when openAiPrompt exists\", async () => {\n    const workspace = {\n      id: 1,\n      name: \"Test Workspace\",\n      openAiPrompt: \"You are a helpful assistant for {workspace.name}. The current user is {user.name}.\",\n    };\n    const user = { id: 1 };\n    const provider = \"openai\";\n\n    const expandedPrompt = \"You are a helpful assistant for Test Workspace. The current user is John Doe.\";\n    SystemPromptVariables.expandSystemPromptVariables.mockResolvedValue(expandedPrompt);\n\n    const definition = await WORKSPACE_AGENT.getDefinition(\n      provider,\n      workspace,\n      user\n    );\n\n    expect(SystemPromptVariables.expandSystemPromptVariables).toHaveBeenCalledWith(\n      workspace.openAiPrompt,\n      user.id,\n      workspace.id\n    );\n    expect(definition.role).toBe(expandedPrompt);\n  });\n\n  it(\"should handle workspace system prompt without user context\", async () => {\n    const workspace = {\n      id: 1,\n      name: \"Test Workspace\",\n      openAiPrompt: \"You are a helpful assistant. Today is {date}.\",\n    };\n    const user = null;\n    const provider = \"lmstudio\";\n    const expandedPrompt = \"You are a helpful assistant. Today is January 1, 2024.\";\n    SystemPromptVariables.expandSystemPromptVariables.mockResolvedValue(expandedPrompt);\n\n    const definition = await WORKSPACE_AGENT.getDefinition(\n      provider,\n      workspace,\n      user\n    );\n\n    expect(SystemPromptVariables.expandSystemPromptVariables).toHaveBeenCalledWith(\n      workspace.openAiPrompt,\n      null,\n      workspace.id\n    );\n    expect(definition.role).toBe(expandedPrompt);\n  });\n\n  it(\"should return functions array in definition\", async () => {\n    const workspace = { id: 1, openAiPrompt: null };\n    const provider = \"openai\";\n\n    const definition = await WORKSPACE_AGENT.getDefinition(\n      provider,\n      workspace,\n      null\n    );\n\n    expect(definition).toHaveProperty(\"functions\");\n    expect(Array.isArray(definition.functions)).toBe(true);\n  });\n\n  it(\"should use LMStudio specific prompt when workspace has no openAiPrompt\", async () => {\n    const workspace = { id: 1, openAiPrompt: null };\n    const user = null;\n    const provider = \"lmstudio\";\n    const definition = await WORKSPACE_AGENT.getDefinition(\n      provider,\n      workspace,\n      null\n    );\n\n    expect(definition.role).toBe(await Provider.systemPrompt({ provider, workspace, user }));\n    expect(definition.role).toContain(\"helpful ai assistant\");\n  });\n});\n\n"
  },
  {
    "path": "server/__tests__/utils/chats/openaiCompatible.test.js",
    "content": "/* eslint-env jest, node */\nconst { OpenAICompatibleChat } = require('../../../utils/chats/openaiCompatible');\nconst { WorkspaceChats } = require('../../../models/workspaceChats');\nconst { getVectorDbClass, getLLMProvider } = require('../../../utils/helpers');\nconst { extractTextContent, extractAttachments } = require('../../../endpoints/api/openai/helpers');\n\n// Mock dependencies\njest.mock('../../../models/workspaceChats');\njest.mock('../../../utils/helpers');\njest.mock('../../../utils/DocumentManager', () => ({\n  DocumentManager: class {\n    constructor() {\n      this.pinnedDocs = jest.fn().mockResolvedValue([]);\n    }\n  }\n}));\n\ndescribe('OpenAICompatibleChat', () => {\n  let mockWorkspace;\n  let mockVectorDb;\n  let mockLLMConnector;\n  let mockResponse;\n\n  beforeEach(() => {\n    // Reset all mocks\n    jest.clearAllMocks();\n\n    // Setup mock workspace\n    mockWorkspace = {\n      id: 1,\n      slug: 'test-workspace',\n      chatMode: 'chat',\n      chatProvider: 'openai',\n      chatModel: 'gpt-4',\n    };\n\n    // Setup mock VectorDb\n    mockVectorDb = {\n      hasNamespace: jest.fn().mockResolvedValue(true),\n      namespaceCount: jest.fn().mockResolvedValue(1),\n      performSimilaritySearch: jest.fn().mockResolvedValue({\n        contextTexts: [],\n        sources: [],\n        message: null,\n      }),\n    };\n    getVectorDbClass.mockReturnValue(mockVectorDb);\n\n    // Setup mock LLM connector\n    mockLLMConnector = {\n      promptWindowLimit: jest.fn().mockReturnValue(4000),\n      compressMessages: jest.fn().mockResolvedValue([]),\n      getChatCompletion: jest.fn().mockResolvedValue({\n        textResponse: 'Mock response',\n        metrics: {},\n      }),\n      streamingEnabled: jest.fn().mockReturnValue(true),\n      streamGetChatCompletion: jest.fn().mockResolvedValue({\n        metrics: {},\n      }),\n      handleStream: jest.fn().mockResolvedValue('Mock streamed response'),\n      defaultTemp: 0.7,\n    };\n    getLLMProvider.mockReturnValue(mockLLMConnector);\n\n    // Setup WorkspaceChats mock\n    WorkspaceChats.new.mockResolvedValue({ chat: { id: 'mock-chat-id' } });\n\n    // Setup mock response object for streaming\n    mockResponse = {\n      write: jest.fn(),\n    };\n  });\n\n  describe('chatSync', () => {\n    test('should handle OpenAI vision multimodal messages', async () => {\n      const multiModalPrompt = [\n        {\n          type: 'text',\n          text: 'What do you see in this image?'\n        },\n        {\n          type: 'image_url',\n          image_url: {\n            url: 'data:image/png;base64,abc123',\n            detail: 'low'\n          }\n        }\n      ];\n\n      const prompt = extractTextContent(multiModalPrompt);\n      const attachments = extractAttachments(multiModalPrompt);\n      const result = await OpenAICompatibleChat.chatSync({\n        workspace: mockWorkspace,\n        prompt,\n        attachments,\n        systemPrompt: 'You are a helpful assistant',\n        history: [\n          { role: 'user', content: 'Previous message' },\n          { role: 'assistant', content: 'Previous response' }\n        ],\n        temperature: 0.7\n      });\n\n      // Verify chat was saved with correct format\n      expect(WorkspaceChats.new).toHaveBeenCalledWith(\n        expect.objectContaining({\n          workspaceId: mockWorkspace.id,\n          prompt: multiModalPrompt[0].text,\n          response: expect.objectContaining({\n            text: 'Mock response',\n            attachments: [{\n              name: 'uploaded_image_0',\n              mime: 'image/png',\n              contentString: multiModalPrompt[1].image_url.url\n            }]\n          })\n        })\n      );\n\n      // Verify response format\n      expect(result).toEqual(\n        expect.objectContaining({\n          object: 'chat.completion',\n          choices: expect.arrayContaining([\n            expect.objectContaining({\n              message: expect.objectContaining({\n                role: 'assistant',\n                content: 'Mock response',\n              }),\n            }),\n          ]),\n        })\n      );\n    });\n\n    test('should handle regular text messages in OpenAI format', async () => {\n      const promptString = 'Hello world';\n      const result = await OpenAICompatibleChat.chatSync({\n        workspace: mockWorkspace,\n        prompt: promptString,\n        systemPrompt: 'You are a helpful assistant',\n        history: [\n          { role: 'user', content: 'Previous message' },\n          { role: 'assistant', content: 'Previous response' }\n        ],\n        temperature: 0.7\n      });\n\n      // Verify chat was saved without attachments\n      expect(WorkspaceChats.new).toHaveBeenCalledWith(\n        expect.objectContaining({\n          workspaceId: mockWorkspace.id,\n          prompt: promptString,\n          response: expect.objectContaining({\n            text: 'Mock response',\n            attachments: []\n          })\n        })\n      );\n\n      expect(result).toBeTruthy();\n    });\n  });\n\n  describe('streamChat', () => {\n    test('should handle OpenAI vision multimodal messages in streaming mode', async () => {\n      const multiModalPrompt = [\n        {\n          type: 'text',\n          text: 'What do you see in this image?'\n        },\n        {\n          type: 'image_url',\n          image_url: {\n            url: 'data:image/png;base64,abc123',\n            detail: 'low'\n          }\n        }\n      ];\n\n      const prompt = extractTextContent(multiModalPrompt);\n      const attachments = extractAttachments(multiModalPrompt);\n      await OpenAICompatibleChat.streamChat({\n        workspace: mockWorkspace,\n        response: mockResponse,\n        prompt,\n        attachments,\n        systemPrompt: 'You are a helpful assistant',\n        history: [\n          { role: 'user', content: 'Previous message' },\n          { role: 'assistant', content: 'Previous response' }\n        ],\n        temperature: 0.7\n      });\n\n      // Verify streaming was handled\n      expect(mockLLMConnector.streamGetChatCompletion).toHaveBeenCalled();\n      expect(mockLLMConnector.handleStream).toHaveBeenCalled();\n\n      // Verify chat was saved with attachments\n      expect(WorkspaceChats.new).toHaveBeenCalledWith(\n        expect.objectContaining({\n          workspaceId: mockWorkspace.id,\n          prompt: multiModalPrompt[0].text,\n          response: expect.objectContaining({\n            text: 'Mock streamed response',\n            attachments: [{\n              name: 'uploaded_image_0',\n              mime: 'image/png',\n              contentString: multiModalPrompt[1].image_url.url\n            }]\n          })\n        })\n      );\n    });\n\n    test('should handle regular text messages in streaming mode', async () => {\n      const promptString = 'Hello world';\n      await OpenAICompatibleChat.streamChat({\n        workspace: mockWorkspace,\n        response: mockResponse,\n        prompt: promptString,\n        systemPrompt: 'You are a helpful assistant',\n        history: [\n          { role: 'user', content: 'Previous message' },\n          { role: 'assistant', content: 'Previous response' }\n        ],\n        temperature: 0.7\n      });\n\n      // Verify streaming was handled\n      expect(mockLLMConnector.streamGetChatCompletion).toHaveBeenCalled();\n      expect(mockLLMConnector.handleStream).toHaveBeenCalled();\n\n      // Verify chat was saved without attachments\n      expect(WorkspaceChats.new).toHaveBeenCalledWith(\n        expect.objectContaining({\n          workspaceId: mockWorkspace.id,\n          prompt: promptString,\n          response: expect.objectContaining({\n            text: 'Mock streamed response',\n            attachments: []\n          })\n        })\n      );\n    });\n  });\n});"
  },
  {
    "path": "server/__tests__/utils/chats/openaiHelpers.test.js",
    "content": "/* eslint-env jest, node */\nconst { extractTextContent, extractAttachments } = require('../../../endpoints/api/openai/helpers');\n\ndescribe('OpenAI Helper Functions', () => {\n  describe('extractTextContent', () => {\n    test('should return string content as-is when not an array', () => {\n      const content = 'Hello world';\n      expect(extractTextContent(content)).toBe('Hello world');\n    });\n\n    test('should extract text from multi-modal content array', () => {\n      const content = [\n        {\n          type: 'text',\n          text: 'What do you see in this image?'\n        },\n        {\n          type: 'image_url',\n          image_url: {\n            url: 'data:image/png;base64,abc123',\n            detail: 'low'\n          }\n        },\n        {\n          type: 'text',\n          text: 'And what about this part?'\n        }\n      ];\n      expect(extractTextContent(content)).toBe('What do you see in this image?\\nAnd what about this part?');\n    });\n\n    test('should handle empty array', () => {\n      expect(extractTextContent([])).toBe('');\n    });\n\n    test('should handle array with no text content', () => {\n      const content = [\n        {\n          type: 'image_url',\n          image_url: {\n            url: 'data:image/png;base64,abc123',\n            detail: 'low'\n          }\n        }\n      ];\n      expect(extractTextContent(content)).toBe('');\n    });\n  });\n\n  describe('extractAttachments', () => {\n    test('should return empty array for string content', () => {\n      const content = 'Hello world';\n      expect(extractAttachments(content)).toEqual([]);\n    });\n\n    test('should extract image attachments with correct mime types', () => {\n      const content = [\n        {\n          type: 'image_url',\n          image_url: {\n            url: 'data:image/png;base64,abc123',\n            detail: 'low'\n          }\n        },\n        {\n          type: 'text',\n          text: 'Between images'\n        },\n        {\n          type: 'image_url',\n          image_url: {\n            url: 'data:image/jpeg;base64,def456',\n            detail: 'high'\n          }\n        }\n      ];\n      expect(extractAttachments(content)).toEqual([\n        {\n          name: 'uploaded_image_0',\n          mime: 'image/png',\n          contentString: 'data:image/png;base64,abc123'\n        },\n        {\n          name: 'uploaded_image_1',\n          mime: 'image/jpeg',\n          contentString: 'data:image/jpeg;base64,def456'\n        }\n      ]);\n    });\n\n    test('should handle invalid data URLs with PNG fallback', () => {\n      const content = [\n        {\n          type: 'image_url',\n          image_url: {\n            url: 'invalid-data-url',\n            detail: 'low'\n          }\n        }\n      ];\n      expect(extractAttachments(content)).toEqual([\n        {\n          name: 'uploaded_image_0',\n          mime: 'image/png',\n          contentString: 'invalid-data-url'\n        }\n      ]);\n    });\n\n    test('should handle empty array', () => {\n      expect(extractAttachments([])).toEqual([]);\n    });\n\n    test('should handle array with no image content', () => {\n      const content = [\n        {\n          type: 'text',\n          text: 'Just some text'\n        },\n        {\n          type: 'text',\n          text: 'More text'\n        }\n      ];\n      expect(extractAttachments(content)).toEqual([]);\n    });\n  });\n});"
  },
  {
    "path": "server/__tests__/utils/helpers/azureOpenAiModelPref.test.js",
    "content": "/* eslint-env jest */\n\n/**\n * Tests for the AzureOpenAI model key migration from OPEN_MODEL_PREF\n * to AZURE_OPENAI_MODEL_PREF, ensuring backwards compatibility for\n * existing users who have OPEN_MODEL_PREF set.\n *\n * Related issue: https://github.com/Mintplex-Labs/anything-llm/issues/3839\n */\n\ndescribe(\"AzureOpenAI model key backwards compatibility\", () => {\n  const ORIGINAL_ENV = process.env;\n\n  beforeEach(() => {\n    jest.resetModules();\n    process.env = { ...ORIGINAL_ENV };\n    delete process.env.AZURE_OPENAI_MODEL_PREF;\n    delete process.env.OPEN_MODEL_PREF;\n  });\n\n  afterAll(() => {\n    process.env = ORIGINAL_ENV;\n  });\n\n  describe(\"getBaseLLMProviderModel - helpers/index.js\", () => {\n    test(\"returns AZURE_OPENAI_MODEL_PREF when set\", () => {\n      process.env.AZURE_OPENAI_MODEL_PREF = \"my-azure-deployment\";\n      process.env.OPEN_MODEL_PREF = \"gpt-4o\";\n      const { getBaseLLMProviderModel } = require(\"../../../utils/helpers/index\");\n      expect(getBaseLLMProviderModel({ provider: \"azure\" })).toBe(\"my-azure-deployment\");\n    });\n\n    test(\"falls back to OPEN_MODEL_PREF when AZURE_OPENAI_MODEL_PREF is not set (backwards compat)\", () => {\n      process.env.OPEN_MODEL_PREF = \"my-old-azure-deployment\";\n      const { getBaseLLMProviderModel } = require(\"../../../utils/helpers/index\");\n      expect(getBaseLLMProviderModel({ provider: \"azure\" })).toBe(\"my-old-azure-deployment\");\n    });\n\n    test(\"openai provider still uses OPEN_MODEL_PREF exclusively\", () => {\n      process.env.OPEN_MODEL_PREF = \"gpt-4o\";\n      process.env.AZURE_OPENAI_MODEL_PREF = \"my-azure-deployment\";\n      const { getBaseLLMProviderModel } = require(\"../../../utils/helpers/index\");\n      expect(getBaseLLMProviderModel({ provider: \"openai\" })).toBe(\"gpt-4o\");\n    });\n\n    test(\"azure and openai return different values when both keys are set\", () => {\n      process.env.OPEN_MODEL_PREF = \"gpt-4o\";\n      process.env.AZURE_OPENAI_MODEL_PREF = \"my-azure-deployment\";\n      const { getBaseLLMProviderModel } = require(\"../../../utils/helpers/index\");\n      expect(getBaseLLMProviderModel({ provider: \"azure\" })).not.toBe(\n        getBaseLLMProviderModel({ provider: \"openai\" })\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "server/__tests__/utils/helpers/convertTo.test.js",
    "content": "/* eslint-env jest */\nconst { prepareChatsForExport } = require(\"../../../utils/helpers/chat/convertTo\");\n\n// Mock the database models\njest.mock(\"../../../models/workspaceChats\");\njest.mock(\"../../../models/embedChats\");\n\nconst { WorkspaceChats } = require(\"../../../models/workspaceChats\");\nconst { EmbedChats } = require(\"../../../models/embedChats\");\n\nconst mockChat = (withImages = false) => {\n  return {\n    id: 1,\n    prompt: \"Test prompt\",\n    response: JSON.stringify({\n      text: \"Test response\",\n      attachments: withImages ? [\n        { mime: \"image/png\", name: \"image.png\", contentString: \"data:image/png;base64,iVBORw0KGg....=\" },\n        { mime: \"image/jpeg\", name: \"image2.jpeg\", contentString: \"data:image/jpeg;base64,iVBORw0KGg....=\" }\n      ] : [],\n      sources: [],\n      metrics: {},\n    }),\n    createdAt: new Date(),\n    workspace: { name: \"Test Workspace\", openAiPrompt: \"Test OpenAI Prompt\" },\n    user: { username: \"testuser\" },\n    feedbackScore: 1,\n  }\n};\n\ndescribe(\"prepareChatsForExport\", () => {\n  beforeEach(() => {\n    jest.clearAllMocks();\n    WorkspaceChats.whereWithData = jest.fn().mockResolvedValue([]);\n    EmbedChats.whereWithEmbedAndWorkspace = jest.fn().mockResolvedValue([]);\n  });\n\n  test(\"should throw error for invalid chat type\", async () => {\n    await expect(prepareChatsForExport(\"json\", \"invalid\"))\n      .rejects\n      .toThrow(\"Invalid chat type: invalid\");\n  });\n\n  test(\"should throw error for invalid export type\", async () => {\n    await expect(prepareChatsForExport(\"invalid\", \"workspace\"))\n      .rejects\n      .toThrow(\"Invalid export type: invalid\");\n  });\n\n  // CSV and JSON are the same format, so we can test them together\n  test(\"should return prepared data in csv and json format for workspace chat type\", async () => {\n    const chatExample = mockChat();\n    WorkspaceChats.whereWithData.mockResolvedValue([chatExample]);\n    const result = await prepareChatsForExport(\"json\", \"workspace\");\n\n    const responseJson = JSON.parse(chatExample.response);\n    expect(result).toBeDefined();\n    expect(result).toEqual([{\n      id: chatExample.id,\n      prompt: chatExample.prompt,\n      response: responseJson.text,\n      sent_at: chatExample.createdAt,\n      rating: chatExample.feedbackScore ? \"GOOD\" : \"BAD\",\n      username: chatExample.user.username,\n      workspace: chatExample.workspace.name,\n      attachments: [],\n    }]);\n  });\n\n  test(\"Should handle attachments for workspace chat type when json format is selected\", async () => {\n    const chatExample = mockChat(true);\n    WorkspaceChats.whereWithData.mockResolvedValue([chatExample]);\n    const result = await prepareChatsForExport(\"json\", \"workspace\");\n\n    const responseJson = JSON.parse(chatExample.response);\n    expect(result).toBeDefined();\n    expect(result).toEqual([{\n      id: chatExample.id,\n      prompt: chatExample.prompt,\n      response: responseJson.text,\n      sent_at: chatExample.createdAt,\n      rating: chatExample.feedbackScore ? \"GOOD\" : \"BAD\",\n      username: chatExample.user.username,\n      workspace: chatExample.workspace.name,\n      attachments: [\n        {\n          type: \"image\",\n          image: responseJson.attachments[0].contentString,\n        },\n        {\n          type: \"image\",\n          image: responseJson.attachments[1].contentString,\n        },\n      ]\n    }]);\n  });\n\n  test(\"Should ignore attachments for workspace chat type when csv format is selected\", async () => {\n    const chatExample = mockChat(true);\n    WorkspaceChats.whereWithData.mockResolvedValue([chatExample]);\n    const result = await prepareChatsForExport(\"csv\", \"workspace\");\n\n    const responseJson = JSON.parse(chatExample.response);\n    expect(result).toBeDefined();\n    expect(result.attachments).not.toBeDefined();\n    expect(result).toEqual([{\n      id: chatExample.id,\n      prompt: chatExample.prompt,\n      response: responseJson.text,\n      sent_at: chatExample.createdAt,\n      rating: chatExample.feedbackScore ? \"GOOD\" : \"BAD\",\n      username: chatExample.user.username,\n      workspace: chatExample.workspace.name,\n    }]);\n  });\n\n  test(\"should return prepared data in jsonAlpaca format for workspace chat type\", async () => {\n    const chatExample = mockChat();\n    const imageChatExample = mockChat(true);\n    WorkspaceChats.whereWithData.mockResolvedValue([chatExample, imageChatExample]);\n    const result = await prepareChatsForExport(\"jsonAlpaca\", \"workspace\");\n\n    const responseJson1 = JSON.parse(chatExample.response);\n    const responseJson2 = JSON.parse(imageChatExample.response);\n    expect(result).toBeDefined();\n\n    // Alpaca format does not support attachments - so they are not included\n    expect(result[0].attachments).not.toBeDefined();\n    expect(result[1].attachments).not.toBeDefined();\n    expect(result).toEqual([{\n      instruction: chatExample.workspace.openAiPrompt,\n      input: chatExample.prompt,\n      output: responseJson1.text,\n    },\n    {\n      instruction: chatExample.workspace.openAiPrompt,\n      input: imageChatExample.prompt,\n      output: responseJson2.text,\n    }]);\n  });\n\n  test(\"should return prepared data in jsonl format for workspace chat type\", async () => {\n    const chatExample = mockChat();\n    const responseJson = JSON.parse(chatExample.response);\n    WorkspaceChats.whereWithData.mockResolvedValue([chatExample]);\n    const result = await prepareChatsForExport(\"jsonl\", \"workspace\");\n    expect(result).toBeDefined();\n    expect(result).toEqual(\n      {\n        [chatExample.workspace.id]: {\n          messages: [\n            {\n              role: \"system\",\n              content: [{\n                type: \"text\",\n                text: chatExample.workspace.openAiPrompt,\n              }],\n            },\n            {\n              role: \"user\",\n              content: [{\n                type: \"text\",\n                text: chatExample.prompt,\n              }],\n            },\n            {\n              role: \"assistant\",\n              content: [{\n                type: \"text\",\n                text: responseJson.text,\n              }],\n            },\n          ],\n        },\n      },\n    );\n  });\n\n  test(\"should return prepared data in jsonl format for workspace chat type with attachments\", async () => {\n    const chatExample = mockChat();\n    const imageChatExample = mockChat(true);\n    const responseJson = JSON.parse(chatExample.response);\n    const imageResponseJson = JSON.parse(imageChatExample.response);\n\n    WorkspaceChats.whereWithData.mockResolvedValue([chatExample, imageChatExample]);\n    const result = await prepareChatsForExport(\"jsonl\", \"workspace\");\n    expect(result).toBeDefined();\n    expect(result).toEqual(\n      {\n        [chatExample.workspace.id]: {\n          messages: [\n            {\n              role: \"system\",\n              content: [{\n                type: \"text\",\n                text: chatExample.workspace.openAiPrompt,\n              }],\n            },\n            {\n              role: \"user\",\n              content: [{\n                type: \"text\",\n                text: chatExample.prompt,\n              }],\n            },\n            {\n              role: \"assistant\",\n              content: [{\n                type: \"text\",\n                text: responseJson.text,\n              }],\n            },\n            {\n              role: \"user\",\n              content: [{\n                type: \"text\",\n                text: imageChatExample.prompt,\n              }, {\n                type: \"image\",\n                image: imageResponseJson.attachments[0].contentString,\n              }, {\n                type: \"image\",\n                image: imageResponseJson.attachments[1].contentString,\n              }],\n            },\n            {\n              role: \"assistant\",\n              content: [{\n                type: \"text\",\n                text: imageResponseJson.text,\n              }],\n            },\n          ],\n        },\n      },\n    );\n  });\n});"
  },
  {
    "path": "server/__tests__/utils/safeJSONStringify/safeJSONStringify.test.js",
    "content": "/* eslint-env jest */\nconst { safeJSONStringify } = require(\"../../../utils/helpers/chat/responses\");\n\ndescribe(\"safeJSONStringify\", () => {\n  test(\"handles regular objects without BigInt\", () => {\n    const obj = { a: 1, b: \"test\", c: true, d: null };\n    expect(safeJSONStringify(obj)).toBe(JSON.stringify(obj));\n  });\n\n  test(\"converts BigInt to string\", () => {\n    const bigInt = BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1);\n    expect(safeJSONStringify(bigInt)).toBe(`\"${bigInt.toString()}\"`);\n  });\n\n  test(\"handles nested BigInt values\", () => {\n    const obj = {\n      metrics: {\n        tokens: BigInt(123),\n        nested: { moreBigInt: BigInt(456) }\n      },\n      normal: \"value\"\n    };\n    expect(safeJSONStringify(obj)).toBe(\n      '{\"metrics\":{\"tokens\":\"123\",\"nested\":{\"moreBigInt\":\"456\"}},\"normal\":\"value\"}'\n    );\n  });\n\n  test(\"handles arrays with BigInt\", () => {\n    const arr = [BigInt(1), 2, BigInt(3)];\n    expect(safeJSONStringify(arr)).toBe('[\"1\",2,\"3\"]');\n  });\n\n  test(\"handles mixed complex objects\", () => {\n    const obj = {\n      id: 1,\n      bigNums: [BigInt(123), BigInt(456)],\n      nested: {\n        more: { huge: BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1) }\n      },\n      normal: { str: \"test\", num: 42, bool: true, nil: null, sub_arr: [\"alpha\", \"beta\", \"gamma\", 1, 2, BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1), { map: { a: BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1) } }] }\n    };\n    const result = JSON.parse(safeJSONStringify(obj)); // Should parse back without errors\n    expect(typeof result.bigNums[0]).toBe(\"string\");\n    expect(result.bigNums[0]).toEqual(\"123\");\n    expect(typeof result.nested.more.huge).toBe(\"string\");\n    expect(result.normal).toEqual({ str: \"test\", num: 42, bool: true, nil: null, sub_arr: [\"alpha\", \"beta\", \"gamma\", 1, 2, (BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1)).toString(), { map: { a: (BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1)).toString() } }] });\n    expect(result.normal.sub_arr[6].map.a).toEqual((BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1)).toString());\n  });\n\n  test(\"handles invariants\", () => {\n    expect(safeJSONStringify({})).toBe(\"{}\");\n    expect(safeJSONStringify(null)).toBe(\"null\");\n    expect(safeJSONStringify(undefined)).toBe(undefined);\n    expect(safeJSONStringify(true)).toBe(\"true\");\n    expect(safeJSONStringify(false)).toBe(\"false\");\n    expect(safeJSONStringify(0)).toBe(\"0\");\n    expect(safeJSONStringify(1)).toBe(\"1\");\n    expect(safeJSONStringify(-1)).toBe(\"-1\");\n  });\n});"
  },
  {
    "path": "server/__tests__/utils/vectorDbProviders/pgvector/index.test.js",
    "content": "const { PGVector: PGVectorClass } = require(\"../../../../utils/vectorDbProviders/pgvector\");\n\nconst PGVector = new PGVectorClass();\n\ndescribe(\"PGVector.sanitizeForJsonb\", () => {\n  it(\"returns null/undefined as-is\", () => {\n    expect(PGVector.sanitizeForJsonb(null)).toBeNull();\n    expect(PGVector.sanitizeForJsonb(undefined)).toBeUndefined();\n  });\n\n  it(\"keeps safe whitespace (tab, LF, CR) and removes disallowed C0 controls\", () => {\n    const input = \"a\\u0000\\u0001\\u0002\\tline\\ncarriage\\rreturn\\u001Fend\";\n    const result = PGVector.sanitizeForJsonb(input);\n    // Expect all < 0x20 except 9,10,13 removed; keep letters and allowed whitespace\n    expect(result).toBe(\"a\\tline\\ncarriage\\rreturnend\");\n  });\n\n  it(\"removes only disallowed control chars; keeps normal printable chars\", () => {\n    const input = \"Hello\\u0000, World! \\u0007\\u0008\\u000B\\u000C\\u001F\";\n    const result = PGVector.sanitizeForJsonb(input);\n    expect(result).toBe(\"Hello, World! \");\n  });\n\n  it(\"deeply sanitizes objects\", () => {\n    const input = {\n      plain: \"ok\",\n      bad: \"has\\u0000nul\",\n      nested: {\n        arr: [\"fine\", \"bad\\u0001\", { deep: \"\\u0002oops\" }],\n      },\n    };\n    const result = PGVector.sanitizeForJsonb(input);\n    expect(result).toEqual({\n      plain: \"ok\",\n      bad: \"hasnul\",\n      nested: { arr: [\"fine\", \"bad\", { deep: \"oops\" }] },\n    });\n  });\n\n  it(\"deeply sanitizes arrays\", () => {\n    const input = [\"\\u0000\", 1, true, { s: \"bad\\u0003\" }, [\"ok\", \"\\u0004bad\"]];\n    const result = PGVector.sanitizeForJsonb(input);\n    expect(result).toEqual([\"\", 1, true, { s: \"bad\" }, [\"ok\", \"bad\"]]);\n  });\n\n  it(\"converts Date to ISO string\", () => {\n    const d = new Date(\"2020-01-02T03:04:05.000Z\");\n    expect(PGVector.sanitizeForJsonb(d)).toBe(d.toISOString());\n  });\n\n  it(\"returns primitives unchanged (number, boolean, bigint)\", () => {\n    expect(PGVector.sanitizeForJsonb(42)).toBe(42);\n    expect(PGVector.sanitizeForJsonb(3.14)).toBe(3.14);\n    expect(PGVector.sanitizeForJsonb(true)).toBe(true);\n    expect(PGVector.sanitizeForJsonb(false)).toBe(false);\n    expect(PGVector.sanitizeForJsonb(BigInt(1))).toBe(BigInt(1));\n  });\n\n  it(\"returns symbol unchanged\", () => {\n    const sym = Symbol(\"x\");\n    expect(PGVector.sanitizeForJsonb(sym)).toBe(sym);\n  });\n\n  it(\"does not mutate original objects/arrays\", () => {\n    const obj = { a: \"bad\\u0000\", nested: { b: \"ok\" } };\n    const arr = [\"\\u0001\", { c: \"bad\\u0002\" }];\n    const objCopy = JSON.parse(JSON.stringify(obj));\n    const arrCopy = JSON.parse(JSON.stringify(arr));\n    const resultObj = PGVector.sanitizeForJsonb(obj);\n    const resultArr = PGVector.sanitizeForJsonb(arr);\n    // Original inputs remain unchanged\n    expect(obj).toEqual(objCopy);\n    expect(arr).toEqual(arrCopy);\n    // Results are sanitized copies\n    expect(resultObj).toEqual({ a: \"bad\", nested: { b: \"ok\" } });\n    expect(resultArr).toEqual([\"\", { c: \"bad\" }]);\n  });\n});\n"
  },
  {
    "path": "server/endpoints/admin.js",
    "content": "const { ApiKey } = require(\"../models/apiKeys\");\nconst { BrowserExtensionApiKey } = require(\"../models/browserExtensionApiKey\");\nconst { Document } = require(\"../models/documents\");\nconst { EventLogs } = require(\"../models/eventLogs\");\nconst { Invite } = require(\"../models/invite\");\nconst { SystemSettings } = require(\"../models/systemSettings\");\nconst { User } = require(\"../models/user\");\nconst { DocumentVectors } = require(\"../models/vectors\");\nconst { Workspace } = require(\"../models/workspace\");\nconst { WorkspaceChats } = require(\"../models/workspaceChats\");\nconst {\n  getVectorDbClass,\n  getEmbeddingEngineSelection,\n} = require(\"../utils/helpers\");\nconst {\n  validRoleSelection,\n  canModifyAdmin,\n  validCanModify,\n} = require(\"../utils/helpers/admin\");\nconst { reqBody, userFromSession, safeJsonParse } = require(\"../utils/http\");\nconst {\n  strictMultiUserRoleValid,\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../utils/middleware/multiUserProtected\");\nconst { validatedRequest } = require(\"../utils/middleware/validatedRequest\");\nconst ImportedPlugin = require(\"../utils/agents/imported\");\nconst {\n  simpleSSOLoginDisabledMiddleware,\n} = require(\"../utils/middleware/simpleSSOEnabled\");\n\nfunction adminEndpoints(app) {\n  if (!app) return;\n\n  app.get(\n    \"/admin/users\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (_request, response) => {\n      try {\n        const users = await User.where();\n        response.status(200).json({ users });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/admin/users/new\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const currUser = await userFromSession(request, response);\n        const newUserParams = reqBody(request);\n        const roleValidation = validRoleSelection(currUser, newUserParams);\n\n        if (!roleValidation.valid) {\n          response\n            .status(200)\n            .json({ user: null, error: roleValidation.error });\n          return;\n        }\n\n        const { user: newUser, error } = await User.create(newUserParams);\n        if (!!newUser) {\n          await EventLogs.logEvent(\n            \"user_created\",\n            {\n              userName: newUser.username,\n              createdBy: currUser.username,\n            },\n            currUser.id\n          );\n        }\n\n        response.status(200).json({ user: newUser, error });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/admin/user/:id\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const currUser = await userFromSession(request, response);\n        const { id } = request.params;\n        const updates = reqBody(request);\n        const user = await User.get({ id: Number(id) });\n\n        const canModify = validCanModify(currUser, user);\n        if (!canModify.valid) {\n          response.status(200).json({ success: false, error: canModify.error });\n          return;\n        }\n\n        const roleValidation = validRoleSelection(currUser, updates);\n        if (!roleValidation.valid) {\n          response\n            .status(200)\n            .json({ success: false, error: roleValidation.error });\n          return;\n        }\n\n        const validAdminRoleModification = await canModifyAdmin(user, updates);\n        if (!validAdminRoleModification.valid) {\n          response\n            .status(200)\n            .json({ success: false, error: validAdminRoleModification.error });\n          return;\n        }\n\n        const { success, error } = await User.update(id, updates);\n        response.status(200).json({ success, error });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/admin/user/:id\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const currUser = await userFromSession(request, response);\n        const { id } = request.params;\n        const user = await User.get({ id: Number(id) });\n\n        const canModify = validCanModify(currUser, user);\n        if (!canModify.valid) {\n          response.status(200).json({ success: false, error: canModify.error });\n          return;\n        }\n\n        await BrowserExtensionApiKey.deleteAllForUser(Number(id));\n        await User.delete({ id: Number(id) });\n        await EventLogs.logEvent(\n          \"user_deleted\",\n          {\n            userName: user.username,\n            deletedBy: currUser.username,\n          },\n          currUser.id\n        );\n        response.status(200).json({ success: true, error: null });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/admin/invites\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (_request, response) => {\n      try {\n        const invites = await Invite.whereWithUsers();\n        response.status(200).json({ invites });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/admin/invite/new\",\n    [\n      validatedRequest,\n      strictMultiUserRoleValid([ROLES.admin, ROLES.manager]),\n      simpleSSOLoginDisabledMiddleware,\n    ],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const body = reqBody(request);\n        const { invite, error } = await Invite.create({\n          createdByUserId: user.id,\n          workspaceIds: body?.workspaceIds || [],\n        });\n\n        await EventLogs.logEvent(\n          \"invite_created\",\n          {\n            inviteCode: invite.code,\n            createdBy: response.locals?.user?.username,\n          },\n          response.locals?.user?.id\n        );\n        response.status(200).json({ invite, error });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/admin/invite/:id\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { id } = request.params;\n        const { success, error } = await Invite.deactivate(id);\n        await EventLogs.logEvent(\n          \"invite_deleted\",\n          { deletedBy: response.locals?.user?.username },\n          response.locals?.user?.id\n        );\n        response.status(200).json({ success, error });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/admin/workspaces\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (_request, response) => {\n      try {\n        const workspaces = await Workspace.whereWithUsers();\n        response.status(200).json({ workspaces });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/admin/workspaces/:workspaceId/users\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { workspaceId } = request.params;\n        const users = await Workspace.workspaceUsers(workspaceId);\n        response.status(200).json({ users });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/admin/workspaces/new\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const { name } = reqBody(request);\n        const { workspace, message: error } = await Workspace.new(\n          name,\n          user.id\n        );\n        response.status(200).json({ workspace, error });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/admin/workspaces/:workspaceId/update-users\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { workspaceId } = request.params;\n        const { userIds } = reqBody(request);\n        const { success, error } = await Workspace.updateUsers(\n          workspaceId,\n          userIds\n        );\n        response.status(200).json({ success, error });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/admin/workspaces/:id\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { id } = request.params;\n        const VectorDb = getVectorDbClass();\n        const workspace = await Workspace.get({ id: Number(id) });\n        if (!workspace) {\n          response.sendStatus(404).end();\n          return;\n        }\n\n        await WorkspaceChats.delete({ workspaceId: Number(workspace.id) });\n        await DocumentVectors.deleteForWorkspace(Number(workspace.id));\n        await Document.delete({ workspaceId: Number(workspace.id) });\n        await Workspace.delete({ id: Number(workspace.id) });\n        try {\n          await VectorDb[\"delete-namespace\"]({ namespace: workspace.slug });\n        } catch (e) {\n          console.error(e.message);\n        }\n\n        response.status(200).json({ success: true, error: null });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  // System preferences but only by array of labels\n  app.get(\n    \"/admin/system-preferences-for\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const requestedSettings = {};\n        const labels = request.query.labels?.split(\",\") || [];\n        const needEmbedder = [\n          \"text_splitter_chunk_size\",\n          \"max_embed_chunk_size\",\n        ];\n        const noRecord = [\n          \"max_embed_chunk_size\",\n          \"agent_sql_connections\",\n          \"imported_agent_skills\",\n          \"feature_flags\",\n          \"meta_page_title\",\n          \"meta_page_favicon\",\n        ];\n\n        // Managers can only read a limited set of settings.\n        // These match the ManagerRoute pages in the frontend.\n        const managerAllowedFields = [\n          \"custom_app_name\",\n          \"footer_data\",\n          \"support_email\",\n          \"meta_page_title\",\n          \"meta_page_favicon\",\n        ];\n\n        for (const label of labels) {\n          // Skip any settings that are not explicitly defined as public\n          if (!SystemSettings.publicFields.includes(label)) continue;\n\n          // Managers can only read manager-allowed fields\n          if (\n            user?.role === ROLES.manager &&\n            !managerAllowedFields.includes(label)\n          )\n            continue;\n\n          // Only get the embedder if the setting actually needs it\n          let embedder = needEmbedder.includes(label)\n            ? getEmbeddingEngineSelection()\n            : null;\n          // Only get the record from db if the setting actually needs it\n          let setting = noRecord.includes(label)\n            ? null\n            : await SystemSettings.get({ label });\n\n          switch (label) {\n            case \"footer_data\":\n              requestedSettings[label] = setting?.value ?? JSON.stringify([]);\n              break;\n            case \"support_email\":\n              requestedSettings[label] = setting?.value || null;\n              break;\n            case \"text_splitter_chunk_size\":\n              requestedSettings[label] =\n                setting?.value || embedder?.embeddingMaxChunkLength || null;\n              break;\n            case \"text_splitter_chunk_overlap\":\n              requestedSettings[label] = setting?.value || null;\n              break;\n            case \"max_embed_chunk_size\":\n              requestedSettings[label] =\n                embedder?.embeddingMaxChunkLength || 1000;\n              break;\n            case \"agent_search_provider\":\n              requestedSettings[label] = setting?.value || null;\n              break;\n            case \"agent_sql_connections\":\n              requestedSettings[label] =\n                await SystemSettings.agent_sql_connections();\n              break;\n            case \"default_agent_skills\":\n              requestedSettings[label] = safeJsonParse(setting?.value, []);\n              break;\n            case \"disabled_agent_skills\":\n              requestedSettings[label] = safeJsonParse(setting?.value, []);\n              break;\n            case \"imported_agent_skills\":\n              requestedSettings[label] = ImportedPlugin.listImportedPlugins();\n              break;\n            case \"custom_app_name\":\n              requestedSettings[label] = setting?.value || null;\n              break;\n            case \"feature_flags\":\n              requestedSettings[label] =\n                (await SystemSettings.getFeatureFlags()) || {};\n              break;\n            case \"meta_page_title\":\n              requestedSettings[label] =\n                await SystemSettings.getValueOrFallback({ label }, null);\n              break;\n            case \"meta_page_favicon\":\n              requestedSettings[label] =\n                await SystemSettings.getValueOrFallback({ label }, null);\n              break;\n            default:\n              break;\n          }\n        }\n\n        response.status(200).json({ settings: requestedSettings });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/admin/system-preferences\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        let updates = reqBody(request);\n\n        // Managers can only update a limited set of settings.\n        // These match the ManagerRoute pages in the frontend.\n        // Admin users can update all supportedFields without restriction.\n        if (user?.role === ROLES.manager) {\n          const managerAllowedFields = [\n            \"custom_app_name\",\n            \"footer_data\",\n            \"support_email\",\n            \"meta_page_title\",\n            \"meta_page_favicon\",\n          ];\n          const filteredUpdates = {};\n          for (const key of Object.keys(updates)) {\n            if (managerAllowedFields.includes(key)) {\n              filteredUpdates[key] = updates[key];\n            }\n          }\n          updates = filteredUpdates;\n        }\n\n        await SystemSettings.updateSettings(updates);\n        response.status(200).json({ success: true, error: null });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/admin/api-keys\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],\n    async (_request, response) => {\n      try {\n        const apiKeys = await ApiKey.whereWithUser({});\n        return response.status(200).json({\n          apiKeys,\n          error: null,\n        });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({\n          apiKey: null,\n          error: \"Could not find an API Keys.\",\n        });\n      }\n    }\n  );\n\n  app.post(\n    \"/admin/generate-api-key\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const { apiKey, error } = await ApiKey.create(user.id);\n        await EventLogs.logEvent(\n          \"api_key_created\",\n          { createdBy: user?.username },\n          user?.id\n        );\n        return response.status(200).json({\n          apiKey,\n          error,\n        });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/admin/delete-api-key/:id\",\n    [validatedRequest, strictMultiUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { id } = request.params;\n        if (!id || isNaN(Number(id))) return response.sendStatus(400).end();\n        await ApiKey.delete({ id: Number(id) });\n\n        await EventLogs.logEvent(\n          \"api_key_deleted\",\n          { deletedBy: response.locals?.user?.username },\n          response?.locals?.user?.id\n        );\n        return response.status(200).end();\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { adminEndpoints };\n"
  },
  {
    "path": "server/endpoints/agentFlows.js",
    "content": "const { AgentFlows } = require(\"../utils/agentFlows\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../utils/middleware/multiUserProtected\");\nconst { validatedRequest } = require(\"../utils/middleware/validatedRequest\");\nconst { Telemetry } = require(\"../models/telemetry\");\n\nfunction agentFlowEndpoints(app) {\n  if (!app) return;\n\n  // Save a flow configuration\n  app.post(\n    \"/agent-flows/save\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { name, config, uuid } = request.body;\n\n        if (!name || !config) {\n          return response.status(400).json({\n            success: false,\n            error: \"Name and config are required\",\n          });\n        }\n\n        const flow = AgentFlows.saveFlow(name, config, uuid);\n        if (!flow || !flow.success)\n          return response\n            .status(200)\n            .json({ flow: null, error: flow.error || \"Failed to save flow\" });\n\n        if (!uuid) {\n          await Telemetry.sendTelemetry(\"agent_flow_created\", {\n            blockCount: config.blocks?.length || 0,\n          });\n        }\n\n        return response.status(200).json({\n          success: true,\n          flow,\n        });\n      } catch (error) {\n        console.error(\"Error saving flow:\", error);\n        return response.status(500).json({\n          success: false,\n          error: error.message,\n        });\n      }\n    }\n  );\n\n  // List all available flows\n  app.get(\n    \"/agent-flows/list\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (_request, response) => {\n      try {\n        const flows = AgentFlows.listFlows();\n        return response.status(200).json({\n          success: true,\n          flows,\n        });\n      } catch (error) {\n        console.error(\"Error listing flows:\", error);\n        return response.status(500).json({\n          success: false,\n          error: error.message,\n        });\n      }\n    }\n  );\n\n  // Get a specific flow by UUID\n  app.get(\n    \"/agent-flows/:uuid\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { uuid } = request.params;\n        const flow = AgentFlows.loadFlow(uuid);\n        if (!flow) {\n          return response.status(404).json({\n            success: false,\n            error: \"Flow not found\",\n          });\n        }\n\n        return response.status(200).json({\n          success: true,\n          flow,\n        });\n      } catch (error) {\n        console.error(\"Error getting flow:\", error);\n        return response.status(500).json({\n          success: false,\n          error: error.message,\n        });\n      }\n    }\n  );\n\n  // Run a specific flow\n  // app.post(\n  //   \"/agent-flows/:uuid/run\",\n  //   [validatedRequest, flexUserRoleValid([ROLES.admin])],\n  //   async (request, response) => {\n  //     try {\n  //       const { uuid } = request.params;\n  //       const { variables = {} } = request.body;\n\n  //       // TODO: Implement flow execution\n  //       console.log(\"Running flow with UUID:\", uuid);\n\n  //       await Telemetry.sendTelemetry(\"agent_flow_executed\", {\n  //         variableCount: Object.keys(variables).length,\n  //       });\n\n  //       return response.status(200).json({\n  //         success: true,\n  //         results: {\n  //           success: true,\n  //           results: \"test\",\n  //           variables: variables,\n  //         },\n  //       });\n  //     } catch (error) {\n  //       console.error(\"Error running flow:\", error);\n  //       return response.status(500).json({\n  //         success: false,\n  //         error: error.message,\n  //       });\n  //     }\n  //   }\n  // );\n\n  // Delete a specific flow\n  app.delete(\n    \"/agent-flows/:uuid\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { uuid } = request.params;\n        const { success } = AgentFlows.deleteFlow(uuid);\n\n        if (!success) {\n          return response.status(500).json({\n            success: false,\n            error: \"Failed to delete flow\",\n          });\n        }\n\n        return response.status(200).json({\n          success,\n        });\n      } catch (error) {\n        console.error(\"Error deleting flow:\", error);\n        return response.status(500).json({\n          success: false,\n          error: error.message,\n        });\n      }\n    }\n  );\n\n  // Toggle flow active status\n  app.post(\n    \"/agent-flows/:uuid/toggle\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { uuid } = request.params;\n        const { active } = request.body;\n\n        const flow = AgentFlows.loadFlow(uuid);\n        if (!flow) {\n          return response\n            .status(404)\n            .json({ success: false, error: \"Flow not found\" });\n        }\n\n        flow.config.active = active;\n        const { success } = AgentFlows.saveFlow(flow.name, flow.config, uuid);\n\n        if (!success) {\n          return response\n            .status(500)\n            .json({ success: false, error: \"Failed to update flow\" });\n        }\n\n        return response.json({ success: true, flow });\n      } catch (error) {\n        console.error(\"Error toggling flow:\", error);\n        response.status(500).json({ success: false, error: error.message });\n      }\n    }\n  );\n}\n\nmodule.exports = { agentFlowEndpoints };\n"
  },
  {
    "path": "server/endpoints/agentWebsocket.js",
    "content": "const { Telemetry } = require(\"../models/telemetry\");\nconst {\n  WorkspaceAgentInvocation,\n} = require(\"../models/workspaceAgentInvocation\");\nconst { AgentHandler } = require(\"../utils/agents\");\nconst {\n  WEBSOCKET_BAIL_COMMANDS,\n} = require(\"../utils/agents/aibitat/plugins/websocket\");\nconst { safeJsonParse } = require(\"../utils/http\");\n\n// Setup listener for incoming messages to relay to socket so it can be handled by agent plugin.\nfunction relayToSocket(message) {\n  if (this.handleFeedback) return this?.handleFeedback?.(message);\n  this.checkBailCommand(message);\n}\n\nfunction agentWebsocket(app) {\n  if (!app) return;\n\n  app.ws(\"/agent-invocation/:uuid\", async function (socket, request) {\n    try {\n      const agentHandler = await new AgentHandler({\n        uuid: String(request.params.uuid),\n      }).init();\n\n      if (!agentHandler.invocation) {\n        socket.close();\n        return;\n      }\n\n      socket.on(\"message\", relayToSocket);\n      socket.on(\"close\", () => {\n        agentHandler.closeAlert();\n        WorkspaceAgentInvocation.close(String(request.params.uuid));\n        return;\n      });\n\n      socket.checkBailCommand = (data) => {\n        const content = safeJsonParse(data)?.feedback;\n        if (WEBSOCKET_BAIL_COMMANDS.includes(content)) {\n          agentHandler.log(\n            `User invoked bail command while processing. Closing session now.`\n          );\n          agentHandler.aibitat.abort();\n          socket.close();\n          return;\n        }\n      };\n\n      await Telemetry.sendTelemetry(\"agent_chat_started\");\n      await agentHandler.createAIbitat({ socket });\n      await agentHandler.startAgentCluster();\n    } catch (e) {\n      console.error(e.message, e);\n      socket?.send(JSON.stringify({ type: \"wssFailure\", content: e.message }));\n      socket?.close();\n    }\n  });\n}\n\nmodule.exports = { agentWebsocket };\n"
  },
  {
    "path": "server/endpoints/api/admin/index.js",
    "content": "const { EventLogs } = require(\"../../../models/eventLogs\");\nconst { Invite } = require(\"../../../models/invite\");\nconst { SystemSettings } = require(\"../../../models/systemSettings\");\nconst { User } = require(\"../../../models/user\");\nconst { Workspace } = require(\"../../../models/workspace\");\nconst { WorkspaceChats } = require(\"../../../models/workspaceChats\");\nconst { WorkspaceUser } = require(\"../../../models/workspaceUsers\");\nconst { canModifyAdmin } = require(\"../../../utils/helpers/admin\");\nconst { multiUserMode, reqBody } = require(\"../../../utils/http\");\nconst { validApiKey } = require(\"../../../utils/middleware/validApiKey\");\n\nfunction apiAdminEndpoints(app) {\n  if (!app) return;\n\n  app.get(\"/v1/admin/is-multi-user-mode\", [validApiKey], (_, response) => {\n    /*\n    #swagger.tags = ['Admin']\n    #swagger.description = 'Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.'\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n             \"isMultiUser\": true\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n    const isMultiUser = multiUserMode(response);\n    response.status(200).json({ isMultiUser });\n  });\n\n  app.get(\"/v1/admin/users\", [validApiKey], async (request, response) => {\n    /*\n    #swagger.tags = ['Admin']\n    #swagger.description = 'Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.'\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n             \"users\": [\n                {\n                  username: \"sample-sam\",\n                  role: 'default',\n                }\n             ]\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n     #swagger.responses[401] = {\n      description: \"Instance is not in Multi-User mode. Method denied\",\n    }\n    */\n    try {\n      if (!multiUserMode(response)) {\n        response.sendStatus(401).end();\n        return;\n      }\n\n      const users = await User.where();\n      response.status(200).json({ users });\n    } catch (e) {\n      console.error(e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.post(\"/v1/admin/users/new\", [validApiKey], async (request, response) => {\n    /*\n    #swagger.tags = ['Admin']\n    #swagger.description = 'Create a new user with username and password. Methods are disabled until multi user mode is enabled via the UI.'\n    #swagger.requestBody = {\n        description: 'Key pair object that will define the new user to add to the system.',\n        required: true,\n        content: {\n          \"application/json\": {\n            example: {\n              username: \"sample-sam\",\n              password: 'hunter2',\n              role: 'default | admin'\n            }\n          }\n        }\n      }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              user: {\n                id: 1,\n                username: 'sample-sam',\n                role: 'default',\n              },\n              error: null,\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n     #swagger.responses[401] = {\n      description: \"Instance is not in Multi-User mode. Method denied\",\n    }\n    */\n    try {\n      if (!multiUserMode(response)) {\n        response.sendStatus(401).end();\n        return;\n      }\n\n      const newUserParams = reqBody(request);\n      const { user: newUser, error } = await User.create(newUserParams);\n      response.status(newUser ? 200 : 400).json({ user: newUser, error });\n    } catch (e) {\n      console.error(e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.post(\"/v1/admin/users/:id\", [validApiKey], async (request, response) => {\n    /*\n    #swagger.tags = ['Admin']\n    #swagger.parameters['id'] = {\n      in: 'path',\n      description: 'id of the user in the database.',\n      required: true,\n      type: 'string'\n    }\n    #swagger.description = 'Update existing user settings. Methods are disabled until multi user mode is enabled via the UI.'\n    #swagger.requestBody = {\n        description: 'Key pair object that will update the found user. All fields are optional and will not update unless specified.',\n        required: true,\n        content: {\n          \"application/json\": {\n            example: {\n              username: \"sample-sam\",\n              password: 'hunter2',\n              role: 'default | admin',\n              suspended: 0,\n            }\n          }\n        }\n      }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              success: true,\n              error: null,\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n     #swagger.responses[401] = {\n      description: \"Instance is not in Multi-User mode. Method denied\",\n    }\n    */\n    try {\n      if (!multiUserMode(response)) {\n        response.sendStatus(401).end();\n        return;\n      }\n\n      const { id } = request.params;\n      const updates = reqBody(request);\n      const user = await User.get({ id: Number(id) });\n      const validAdminRoleModification = await canModifyAdmin(user, updates);\n\n      if (!validAdminRoleModification.valid) {\n        response\n          .status(200)\n          .json({ success: false, error: validAdminRoleModification.error });\n        return;\n      }\n\n      const { success, error } = await User.update(id, updates);\n      response.status(200).json({ success, error });\n    } catch (e) {\n      console.error(e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.delete(\n    \"/v1/admin/users/:id\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Admin']\n    #swagger.description = 'Delete existing user by id. Methods are disabled until multi user mode is enabled via the UI.'\n    #swagger.parameters['id'] = {\n      in: 'path',\n      description: 'id of the user in the database.',\n      required: true,\n      type: 'string'\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              success: true,\n              error: null,\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n     #swagger.responses[401] = {\n      description: \"Instance is not in Multi-User mode. Method denied\",\n    }\n    */\n      try {\n        if (!multiUserMode(response)) {\n          response.sendStatus(401).end();\n          return;\n        }\n\n        const { id } = request.params;\n        const user = await User.get({ id: Number(id) });\n        await User.delete({ id: user.id });\n        await EventLogs.logEvent(\"api_user_deleted\", {\n          userName: user.username,\n        });\n        response.status(200).json({ success: true, error: null });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\"/v1/admin/invites\", [validApiKey], async (request, response) => {\n    /*\n    #swagger.tags = ['Admin']\n    #swagger.description = 'List all existing invitations to instance regardless of status. Methods are disabled until multi user mode is enabled via the UI.'\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n             \"invites\": [\n                {\n                  id: 1,\n                  status: \"pending\",\n                  code: 'abc-123',\n                  claimedBy: null\n                }\n             ]\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n     #swagger.responses[401] = {\n      description: \"Instance is not in Multi-User mode. Method denied\",\n    }\n    */\n    try {\n      if (!multiUserMode(response)) {\n        response.sendStatus(401).end();\n        return;\n      }\n\n      const invites = await Invite.whereWithUsers();\n      response.status(200).json({ invites });\n    } catch (e) {\n      console.error(e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.post(\"/v1/admin/invite/new\", [validApiKey], async (request, response) => {\n    /*\n    #swagger.tags = ['Admin']\n    #swagger.description = 'Create a new invite code for someone to use to register with instance. Methods are disabled until multi user mode is enabled via the UI.'\n    #swagger.requestBody = {\n        description: 'Request body for creation parameters of the invitation',\n        required: false,\n        content: {\n          \"application/json\": {\n            example: {\n              workspaceIds: [1,2,45],\n            }\n          }\n        }\n      }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              invite: {\n                id: 1,\n                status: \"pending\",\n                code: 'abc-123',\n              },\n              error: null,\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n     #swagger.responses[401] = {\n      description: \"Instance is not in Multi-User mode. Method denied\",\n    }\n    */\n    try {\n      if (!multiUserMode(response)) {\n        response.sendStatus(401).end();\n        return;\n      }\n\n      const body = reqBody(request);\n      const { invite, error } = await Invite.create({\n        workspaceIds: body?.workspaceIds ?? [],\n      });\n      response.status(200).json({ invite, error });\n    } catch (e) {\n      console.error(e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.delete(\n    \"/v1/admin/invite/:id\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Admin']\n    #swagger.description = 'Deactivates (soft-delete) invite by id. Methods are disabled until multi user mode is enabled via the UI.'\n    #swagger.parameters['id'] = {\n      in: 'path',\n      description: 'id of the invite in the database.',\n      required: true,\n      type: 'string'\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              success: true,\n              error: null,\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n     #swagger.responses[401] = {\n      description: \"Instance is not in Multi-User mode. Method denied\",\n    }\n    */\n      try {\n        if (!multiUserMode(response)) {\n          response.sendStatus(401).end();\n          return;\n        }\n\n        const { id } = request.params;\n        const { success, error } = await Invite.deactivate(id);\n        response.status(200).json({ success, error });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/v1/admin/workspaces/:workspaceId/users\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Admin']\n      #swagger.parameters['workspaceId'] = {\n        in: 'path',\n        description: 'id of the workspace.',\n        required: true,\n        type: 'string'\n      }\n      #swagger.description = 'Retrieve a list of users with permissions to access the specified workspace.'\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                users: [\n                  {\"userId\": 1, \"role\": \"admin\"},\n                  {\"userId\": 2, \"role\": \"member\"}\n                ]\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n       #swagger.responses[401] = {\n        description: \"Instance is not in Multi-User mode. Method denied\",\n      }\n      */\n\n      try {\n        if (!multiUserMode(response)) {\n          response.sendStatus(401).end();\n          return;\n        }\n\n        const workspaceId = request.params.workspaceId;\n        const users = await Workspace.workspaceUsers(workspaceId);\n\n        response.status(200).json({ users });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/admin/workspaces/:workspaceId/update-users\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Admin']\n    #swagger.deprecated = true\n    #swagger.parameters['workspaceId'] = {\n      in: 'path',\n      description: 'id of the workspace in the database.',\n      required: true,\n      type: 'string'\n    }\n    #swagger.description = 'Overwrite workspace permissions to only be accessible by the given user ids and admins. Methods are disabled until multi user mode is enabled via the UI.'\n    #swagger.requestBody = {\n        description: 'Entire array of user ids who can access the workspace. All fields are optional and will not update unless specified.',\n        required: true,\n        content: {\n          \"application/json\": {\n            example: {\n              userIds: [1,2,4,12],\n            }\n          }\n        }\n      }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              success: true,\n              error: null,\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n     #swagger.responses[401] = {\n      description: \"Instance is not in Multi-User mode. Method denied\",\n    }\n    */\n      try {\n        if (!multiUserMode(response)) {\n          response.sendStatus(401).end();\n          return;\n        }\n\n        const { workspaceId } = request.params;\n        const { userIds } = reqBody(request);\n        const { success, error } = await Workspace.updateUsers(\n          workspaceId,\n          userIds\n        );\n        response.status(200).json({ success, error });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/admin/workspaces/:workspaceSlug/manage-users\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Admin']\n    #swagger.parameters['workspaceSlug'] = {\n      in: 'path',\n      description: 'slug of the workspace in the database',\n      required: true,\n      type: 'string'\n    }\n    #swagger.description = 'Set workspace permissions to be accessible by the given user ids and admins. Methods are disabled until multi user mode is enabled via the UI.'\n    #swagger.requestBody = {\n        description: 'Array of user ids who will be given access to the target workspace. <code>reset</code> will remove all existing users from the workspace and only add the new users - default <code>false</code>.',\n        required: true,\n        content: {\n          \"application/json\": {\n            example: {\n              userIds: [1,2,4,12],\n              reset: false\n            }\n          }\n        }\n      }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              success: true,\n              error: null,\n              users: [\n                {\"userId\": 1, \"username\": \"main-admin\", \"role\": \"admin\"},\n                {\"userId\": 2, \"username\": \"sample-sam\", \"role\": \"default\"}\n              ]\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n     #swagger.responses[401] = {\n      description: \"Instance is not in Multi-User mode. Method denied\",\n    }\n    */\n      try {\n        if (!multiUserMode(response)) {\n          response.sendStatus(401).end();\n          return;\n        }\n\n        const { workspaceSlug } = request.params;\n        const { userIds: _uids, reset = false } = reqBody(request);\n        const userIds = (\n          await User.where({ id: { in: _uids.map(Number) } })\n        ).map((user) => user.id);\n        const workspace = await Workspace.get({ slug: String(workspaceSlug) });\n        const workspaceUsers = await Workspace.workspaceUsers(workspace.id);\n\n        if (!workspace) {\n          response.status(404).json({\n            success: false,\n            error: `Workspace ${workspaceSlug} not found`,\n            users: workspaceUsers,\n          });\n          return;\n        }\n\n        if (userIds.length === 0) {\n          response.status(404).json({\n            success: false,\n            error: `No valid user IDs provided.`,\n            users: workspaceUsers,\n          });\n          return;\n        }\n\n        // Reset all users in the workspace and add the new users as the only users in the workspace\n        if (reset) {\n          const { success, error } = await Workspace.updateUsers(\n            workspace.id,\n            userIds\n          );\n          return response.status(200).json({\n            success,\n            error,\n            users: await Workspace.workspaceUsers(workspace.id),\n          });\n        }\n\n        // Add new users to the workspace if they are not already in the workspace\n        const existingUserIds = workspaceUsers.map((user) => user.userId);\n        const usersToAdd = userIds.filter(\n          (userId) => !existingUserIds.includes(userId)\n        );\n        if (usersToAdd.length > 0)\n          await WorkspaceUser.createManyUsers(usersToAdd, workspace.id);\n        response.status(200).json({\n          success: true,\n          error: null,\n          users: await Workspace.workspaceUsers(workspace.id),\n        });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/admin/workspace-chats\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Admin']\n    #swagger.description = 'All chats in the system ordered by most recent. Methods are disabled until multi user mode is enabled via the UI.'\n    #swagger.requestBody = {\n        description: 'Page offset to show of workspace chats. All fields are optional and will not update unless specified.',\n        required: false,\n        content: {\n          \"application/json\": {\n            example: {\n              offset: 2,\n            }\n          }\n        }\n      }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              success: true,\n              error: null,\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n      try {\n        const pgSize = 20;\n        const { offset = 0 } = reqBody(request);\n        const chats = await WorkspaceChats.whereWithData(\n          {},\n          pgSize,\n          offset * pgSize,\n          { id: \"desc\" }\n        );\n\n        const hasPages = (await WorkspaceChats.count()) > (offset + 1) * pgSize;\n        response.status(200).json({ chats: chats, hasPages });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/admin/preferences\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Admin']\n    #swagger.description = 'Update multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.'\n    #swagger.requestBody = {\n      description: 'Object with setting key and new value to set. All keys are optional and will not update unless specified.',\n      required: true,\n      content: {\n        \"application/json\": {\n          example: {\n            support_email: \"support@example.com\",\n          }\n        }\n      }\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              success: true,\n              error: null,\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n     #swagger.responses[401] = {\n      description: \"Instance is not in Multi-User mode. Method denied\",\n    }\n    */\n      try {\n        if (!multiUserMode(response)) {\n          response.sendStatus(401).end();\n          return;\n        }\n\n        const updates = reqBody(request);\n        await SystemSettings.updateSettings(updates);\n        response.status(200).json({ success: true, error: null });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { apiAdminEndpoints };\n"
  },
  {
    "path": "server/endpoints/api/auth/index.js",
    "content": "const { validApiKey } = require(\"../../../utils/middleware/validApiKey\");\n\nfunction apiAuthEndpoints(app) {\n  if (!app) return;\n\n  app.get(\"/v1/auth\", [validApiKey], (_, response) => {\n    /* \n    #swagger.tags = ['Authentication']\n    #swagger.description = 'Verify the attached Authentication header contains a valid API token.'\n    #swagger.responses[200] = {\n      description: 'Valid auth token was found.',\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              authenticated: true,\n            }\n          }\n        }           \n      }\n    }  \n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n    response.status(200).json({ authenticated: true });\n  });\n}\n\nmodule.exports = { apiAuthEndpoints };\n"
  },
  {
    "path": "server/endpoints/api/document/index.js",
    "content": "const { Telemetry } = require(\"../../../models/telemetry\");\nconst { validApiKey } = require(\"../../../utils/middleware/validApiKey\");\nconst { handleAPIFileUpload } = require(\"../../../utils/files/multer\");\nconst {\n  viewLocalFiles,\n  findDocumentInDocuments,\n  getDocumentsByFolder,\n  normalizePath,\n  isWithin,\n} = require(\"../../../utils/files\");\nconst { reqBody, safeJsonParse } = require(\"../../../utils/http\");\nconst { EventLogs } = require(\"../../../models/eventLogs\");\nconst { CollectorApi } = require(\"../../../utils/collectorApi\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { Document } = require(\"../../../models/documents\");\nconst { purgeFolder } = require(\"../../../utils/files/purgeDocument\");\nconst documentsPath =\n  process.env.NODE_ENV === \"development\"\n    ? path.resolve(__dirname, \"../../../storage/documents\")\n    : path.resolve(process.env.STORAGE_DIR, `documents`);\n\n/**\n * Runs a simple validation check on the addToWorkspaces query parameter to ensure it is a string of comma-separated workspace slugs.\n * @param {*} request\n * @param {*} response\n * @param {*} next\n * @returns\n */\nfunction validateWorkspaceSlugQuery(request, response, next) {\n  const { addToWorkspaces = \"\" } = reqBody(request);\n  if (!addToWorkspaces) return next();\n  if (typeof addToWorkspaces !== \"string\") {\n    return response\n      .status(422)\n      .json({\n        success: false,\n        error: `addToWorkspaces must be a string of comma-separated workspace slugs. Got ${typeof addToWorkspaces}`,\n      })\n      .end();\n  }\n  next();\n}\n\nfunction apiDocumentEndpoints(app) {\n  if (!app) return;\n\n  app.post(\n    \"/v1/document/upload\",\n    [validApiKey, handleAPIFileUpload, validateWorkspaceSlugQuery],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Documents']\n    #swagger.description = 'Upload a new file to AnythingLLM to be parsed and prepared for embedding, with optional metadata.'\n    #swagger.requestBody = {\n      description: 'File to be uploaded.',\n      required: true,\n      content: {\n        \"multipart/form-data\": {\n          schema: {\n            type: 'object',\n            required: ['file'],\n            properties: {\n              file: {\n                type: 'string',\n                format: 'binary',\n                description: 'The file to upload'\n              },\n              addToWorkspaces: {\n                type: 'string',\n                description: 'comma-separated text-string of workspace slugs to embed the document into post-upload. eg: workspace1,workspace2',\n              },\n              metadata: {\n                type: 'object',\n                description: 'Key:Value pairs of metadata to attach to the document in JSON Object format. Only specific keys are allowed - see example.',\n                example: { 'title': 'Custom Title', 'docAuthor': 'Author Name', 'description': 'A brief description', 'docSource': 'Source of the document' }\n              }\n            },\n            required: ['file']\n          }\n        }\n      }\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              success: true,\n              error: null,\n              documents: [\n                {\n                  \"location\": \"custom-documents/anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json\",\n                  \"name\": \"anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json\",\n                  \"url\": \"file:///Users/tim/Documents/anything-llm/collector/hotdir/anythingllm.txt\",\n                  \"title\": \"anythingllm.txt\",\n                  \"docAuthor\": \"Unknown\",\n                  \"description\": \"Unknown\",\n                  \"docSource\": \"a text file uploaded by the user.\",\n                  \"chunkSource\": \"anythingllm.txt\",\n                  \"published\": \"1/16/2024, 3:07:00 PM\",\n                  \"wordCount\": 93,\n                  \"token_count_estimate\": 115,\n                }\n              ]\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n      try {\n        const Collector = new CollectorApi();\n        const { originalname } = request.file;\n        const { addToWorkspaces = \"\", metadata: _metadata = {} } =\n          reqBody(request);\n        const metadata =\n          typeof _metadata === \"string\"\n            ? safeJsonParse(_metadata, {})\n            : _metadata;\n        const processingOnline = await Collector.online();\n\n        if (!processingOnline) {\n          response\n            .status(500)\n            .json({\n              success: false,\n              error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,\n            })\n            .end();\n          return;\n        }\n\n        const { success, reason, documents } = await Collector.processDocument(\n          originalname,\n          metadata\n        );\n\n        if (!success) {\n          return response\n            .status(500)\n            .json({ success: false, error: reason, documents })\n            .end();\n        }\n\n        Collector.log(\n          `Document ${originalname} uploaded processed and successfully. It is now available in documents.`\n        );\n        await Telemetry.sendTelemetry(\"document_uploaded\");\n        await EventLogs.logEvent(\"api_document_uploaded\", {\n          documentName: originalname,\n        });\n\n        if (!!addToWorkspaces)\n          await Document.api.uploadToWorkspace(\n            addToWorkspaces,\n            documents?.[0].location\n          );\n        response.status(200).json({ success: true, error: null, documents });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/document/upload/:folderName\",\n    [validApiKey, handleAPIFileUpload, validateWorkspaceSlugQuery],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Documents']\n      #swagger.description = 'Upload a new file to a specific folder in AnythingLLM to be parsed and prepared for embedding. If the folder does not exist, it will be created.'\n      #swagger.parameters['folderName'] = {\n        in: 'path',\n        description: 'Target folder path (defaults to \\\"custom-documents\\\" if not provided)',\n        required: true,\n        type: 'string',\n        example: 'my-folder'\n      }\n      #swagger.requestBody = {\n        description: 'File to be uploaded, with optional metadata.',\n        required: true,\n        content: {\n          \"multipart/form-data\": {\n            schema: {\n              type: 'object',\n              required: ['file'],\n              properties: {\n                file: {\n                  type: 'string',\n                  format: 'binary',\n                  description: 'The file to upload'\n                },\n                addToWorkspaces: {\n                  type: 'string',\n                  description: 'comma-separated text-string of workspace slugs to embed the document into post-upload. eg: workspace1,workspace2',\n                },\n                metadata: {\n                  type: 'object',\n                  description: 'Key:Value pairs of metadata to attach to the document in JSON Object format. Only specific keys are allowed - see example.',\n                  example: { 'title': 'Custom Title', 'docAuthor': 'Author Name', 'description': 'A brief description', 'docSource': 'Source of the document' }\n                }\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                success: true,\n                error: null,\n                documents: [{\n                  \"location\": \"custom-documents/anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json\",\n                  \"name\": \"anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json\",\n                  \"url\": \"file:///Users/tim/Documents/anything-llm/collector/hotdir/anythingllm.txt\",\n                  \"title\": \"anythingllm.txt\",\n                  \"docAuthor\": \"Unknown\",\n                  \"description\": \"Unknown\",\n                  \"docSource\": \"a text file uploaded by the user.\",\n                  \"chunkSource\": \"anythingllm.txt\",\n                  \"published\": \"1/16/2024, 3:07:00 PM\",\n                  \"wordCount\": 93,\n                  \"token_count_estimate\": 115\n                }]\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      #swagger.responses[500] = {\n        description: \"Internal Server Error\",\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                success: false,\n                error: \"Document processing API is not online. Document will not be processed automatically.\"\n              }\n            }\n          }\n        }\n      }\n      */\n      try {\n        const { originalname } = request.file;\n        const { addToWorkspaces = \"\", metadata: _metadata = {} } =\n          reqBody(request);\n        const metadata =\n          typeof _metadata === \"string\"\n            ? safeJsonParse(_metadata, {})\n            : _metadata;\n\n        let folder = request.params?.folderName || \"custom-documents\";\n        folder = normalizePath(folder);\n        const targetFolderPath = path.join(documentsPath, folder);\n\n        if (\n          !isWithin(path.resolve(documentsPath), path.resolve(targetFolderPath))\n        )\n          throw new Error(\"Invalid folder name\");\n        if (!fs.existsSync(targetFolderPath))\n          fs.mkdirSync(targetFolderPath, { recursive: true });\n\n        const Collector = new CollectorApi();\n        const processingOnline = await Collector.online();\n        if (!processingOnline) {\n          return response\n            .status(500)\n            .json({\n              success: false,\n              error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,\n            })\n            .end();\n        }\n\n        // Process the uploaded document with metadata\n        const { success, reason, documents } = await Collector.processDocument(\n          originalname,\n          metadata\n        );\n        if (!success) {\n          return response\n            .status(500)\n            .json({ success: false, error: reason, documents })\n            .end();\n        }\n\n        // For each processed document, check if it is already in the desired folder.\n        // If not, move it using similar logic as in the move-files endpoint.\n        for (const doc of documents) {\n          const currentFolder = path.dirname(doc.location);\n          if (currentFolder !== folder) {\n            const sourcePath = path.join(\n              documentsPath,\n              normalizePath(doc.location)\n            );\n            const destinationPath = path.join(\n              targetFolderPath,\n              path.basename(doc.location)\n            );\n\n            if (\n              !isWithin(documentsPath, sourcePath) ||\n              !isWithin(documentsPath, destinationPath)\n            )\n              throw new Error(\"Invalid file location\");\n\n            fs.renameSync(sourcePath, destinationPath);\n            doc.location = path.join(folder, path.basename(doc.location));\n            doc.name = path.basename(doc.location);\n          }\n        }\n\n        Collector.log(\n          `Document ${originalname} uploaded, processed, and moved to folder ${folder} successfully.`\n        );\n\n        await Telemetry.sendTelemetry(\"document_uploaded\");\n        await EventLogs.logEvent(\"api_document_uploaded\", {\n          documentName: originalname,\n          folder,\n        });\n\n        if (!!addToWorkspaces)\n          await Document.api.uploadToWorkspace(\n            addToWorkspaces,\n            documents?.[0].location\n          );\n        response.status(200).json({ success: true, error: null, documents });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/document/upload-link\",\n    [validApiKey, validateWorkspaceSlugQuery],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Documents']\n    #swagger.description = 'Upload a valid URL for AnythingLLM to scrape and prepare for embedding. Optionally, specify a comma-separated list of workspace slugs to embed the document into post-upload.'\n    #swagger.requestBody = {\n      description: 'Link of web address to be scraped and optionally a comma-separated list of workspace slugs to embed the document into post-upload, and optional metadata.',\n      required: true,\n      content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                \"link\": \"https://anythingllm.com\",\n                \"addToWorkspaces\": \"workspace1,workspace2\",\n                \"scraperHeaders\": {\n                  \"Authorization\": \"Bearer token123\",\n                  \"My-Custom-Header\": \"value\"\n                },\n                \"metadata\": {\n                  \"title\": \"Custom Title\",\n                  \"docAuthor\": \"Author Name\",\n                  \"description\": \"A brief description\",\n                  \"docSource\": \"Source of the document\"\n                }\n              }\n            }\n          }\n        }\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              success: true,\n              error: null,\n              documents: [\n                {\n                  \"id\": \"c530dbe6-bff1-4b9e-b87f-710d539d20bc\",\n                  \"url\": \"file://useanything_com.html\",\n                  \"title\": \"useanything_com.html\",\n                  \"docAuthor\": \"no author found\",\n                  \"description\": \"No description found.\",\n                  \"docSource\": \"URL link uploaded by the user.\",\n                  \"chunkSource\": \"https:anythingllm.com.html\",\n                  \"published\": \"1/16/2024, 3:46:33 PM\",\n                  \"wordCount\": 252,\n                  \"pageContent\": \"AnythingLLM is the best....\",\n                  \"token_count_estimate\": 447,\n                  \"location\": \"custom-documents/url-useanything_com-c530dbe6-bff1-4b9e-b87f-710d539d20bc.json\"\n                }\n              ]\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n      try {\n        const Collector = new CollectorApi();\n        const {\n          link,\n          addToWorkspaces = \"\",\n          scraperHeaders = {},\n          metadata: _metadata = {},\n        } = reqBody(request);\n        const metadata =\n          typeof _metadata === \"string\"\n            ? safeJsonParse(_metadata, {})\n            : _metadata;\n        const processingOnline = await Collector.online();\n\n        if (!processingOnline) {\n          return response\n            .status(500)\n            .json({\n              success: false,\n              error: `Document processing API is not online. Link ${link} will not be processed automatically.`,\n            })\n            .end();\n        }\n\n        const { success, reason, documents } = await Collector.processLink(\n          link,\n          scraperHeaders,\n          metadata\n        );\n        if (!success) {\n          return response\n            .status(500)\n            .json({ success: false, error: reason, documents })\n            .end();\n        }\n\n        Collector.log(\n          `Link ${link} uploaded processed and successfully. It is now available in documents.`\n        );\n        await Telemetry.sendTelemetry(\"link_uploaded\");\n        await EventLogs.logEvent(\"api_link_uploaded\", {\n          link,\n        });\n\n        if (!!addToWorkspaces)\n          await Document.api.uploadToWorkspace(\n            addToWorkspaces,\n            documents?.[0].location\n          );\n        response.status(200).json({ success: true, error: null, documents });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/document/raw-text\",\n    [validApiKey, validateWorkspaceSlugQuery],\n    async (request, response) => {\n      /*\n     #swagger.tags = ['Documents']\n     #swagger.description = 'Upload a file by specifying its raw text content and metadata values without having to upload a file.'\n     #swagger.requestBody = {\n      description: 'Text content and metadata of the file to be saved to the system. Use metadata-schema endpoint to get the possible metadata keys',\n      required: true,\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              \"textContent\": \"This is the raw text that will be saved as a document in AnythingLLM.\",\n              \"addToWorkspaces\": \"workspace1,workspace2\",\n              \"metadata\": {\n                \"title\": \"This key is required. See in /server/endpoints/api/document/index.js:287\",\n                \"keyOne\": \"valueOne\",\n                \"keyTwo\": \"valueTwo\",\n                \"etc\": \"etc\"\n              }\n            }\n          }\n        }\n      }\n     }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              success: true,\n              error: null,\n              documents: [\n                {\n                  \"id\": \"c530dbe6-bff1-4b9e-b87f-710d539d20bc\",\n                  \"url\": \"file://my-document.txt\",\n                  \"title\": \"hello-world.txt\",\n                  \"docAuthor\": \"no author found\",\n                  \"description\": \"No description found.\",\n                  \"docSource\": \"My custom description set during upload\",\n                  \"chunkSource\": \"no chunk source specified\",\n                  \"published\": \"1/16/2024, 3:46:33 PM\",\n                  \"wordCount\": 252,\n                  \"pageContent\": \"AnythingLLM is the best....\",\n                  \"token_count_estimate\": 447,\n                  \"location\": \"custom-documents/raw-my-doc-text-c530dbe6-bff1-4b9e-b87f-710d539d20bc.json\"\n                }\n              ]\n            }\n          }\n        }\n      }\n     }\n     #swagger.responses[403] = {\n       schema: {\n         \"$ref\": \"#/definitions/InvalidAPIKey\"\n       }\n     }\n     */\n      try {\n        const Collector = new CollectorApi();\n        const requiredMetadata = [\"title\"];\n        const {\n          textContent,\n          metadata: _metadata = {},\n          addToWorkspaces = \"\",\n        } = reqBody(request);\n        const metadata =\n          typeof _metadata === \"string\"\n            ? safeJsonParse(_metadata, {})\n            : _metadata;\n        const processingOnline = await Collector.online();\n\n        if (!processingOnline) {\n          return response\n            .status(500)\n            .json({\n              success: false,\n              error: `Document processing API is not online. Request will not be processed.`,\n            })\n            .end();\n        }\n\n        if (\n          !requiredMetadata.every(\n            (reqKey) =>\n              Object.keys(metadata).includes(reqKey) && !!metadata[reqKey]\n          )\n        ) {\n          return response\n            .status(422)\n            .json({\n              success: false,\n              error: `You are missing required metadata key:value pairs in your request. Required metadata key:values are ${requiredMetadata\n                .map((v) => `'${v}'`)\n                .join(\", \")}`,\n            })\n            .end();\n        }\n\n        if (!textContent || textContent?.length === 0) {\n          return response\n            .status(422)\n            .json({\n              success: false,\n              error: `The 'textContent' key cannot have an empty value.`,\n            })\n            .end();\n        }\n\n        const { success, reason, documents } = await Collector.processRawText(\n          textContent,\n          metadata\n        );\n        if (!success) {\n          return response\n            .status(500)\n            .json({ success: false, error: reason, documents })\n            .end();\n        }\n\n        Collector.log(\n          `Document created successfully. It is now available in documents.`\n        );\n        await Telemetry.sendTelemetry(\"raw_document_uploaded\");\n        await EventLogs.logEvent(\"api_raw_document_uploaded\");\n\n        if (!!addToWorkspaces)\n          await Document.api.uploadToWorkspace(\n            addToWorkspaces,\n            documents?.[0].location\n          );\n        response.status(200).json({ success: true, error: null, documents });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\"/v1/documents\", [validApiKey], async (_, response) => {\n    /*\n    #swagger.tags = ['Documents']\n    #swagger.description = 'List of all locally-stored documents in instance'\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n             \"localFiles\": {\n              \"name\": \"documents\",\n              \"type\": \"folder\",\n              items: [\n                {\n                  \"name\": \"my-stored-document.json\",\n                  \"type\": \"file\",\n                  \"id\": \"bb07c334-4dab-4419-9462-9d00065a49a1\",\n                  \"url\": \"file://my-stored-document.txt\",\n                  \"title\": \"my-stored-document.txt\",\n                  \"cached\": false\n                },\n              ]\n             }\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n    try {\n      const localFiles = await viewLocalFiles();\n      response.status(200).json({ localFiles });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.get(\n    \"/v1/documents/folder/:folderName\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Documents']\n    #swagger.description = 'Get all documents stored in a specific folder.'\n    #swagger.parameters['folderName'] = {\n      in: 'path',\n      description: 'Name of the folder to retrieve documents from',\n      required: true,\n      type: 'string'\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              folder: \"custom-documents\",\n              documents: [\n                {\n                  name: \"document1.json\",\n                  type: \"file\",\n                  cached: false,\n                  pinnedWorkspaces: [],\n                  watched: false,\n                  more: \"data\",\n                },\n                {\n                  name: \"document2.json\",\n                  type: \"file\",\n                  cached: false,\n                  pinnedWorkspaces: [],\n                  watched: false,\n                  more: \"data\",\n                },\n              ]\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n      try {\n        const { folderName } = request.params;\n        const result = await getDocumentsByFolder(folderName);\n        response.status(result.code).json({\n          folder: result.folder,\n          documents: result.documents,\n          error: result.error,\n        });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/v1/document/accepted-file-types\",\n    [validApiKey],\n    async (_, response) => {\n      /*\n    #swagger.tags = ['Documents']\n    #swagger.description = 'Check available filetypes and MIMEs that can be uploaded.'\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              \"types\": {\n                \"application/mbox\": [\n                  \".mbox\"\n                ],\n                \"application/pdf\": [\n                  \".pdf\"\n                ],\n                \"application/vnd.oasis.opendocument.text\": [\n                  \".odt\"\n                ],\n                \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": [\n                  \".docx\"\n                ],\n                \"text/plain\": [\n                  \".txt\",\n                  \".md\"\n                ]\n              }\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n      try {\n        const types = await new CollectorApi().acceptedFileTypes();\n        if (!types) {\n          response.sendStatus(404).end();\n          return;\n        }\n\n        response.status(200).json({ types });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/v1/document/metadata-schema\",\n    [validApiKey],\n    async (_, response) => {\n      /*\n    #swagger.tags = ['Documents']\n    #swagger.description = 'Get the known available metadata schema for when doing a raw-text upload and the acceptable type of value for each key.'\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n             \"schema\": {\n                \"keyOne\": \"string | number | nullable\",\n                \"keyTwo\": \"string | number | nullable\",\n                \"specialKey\": \"number\",\n                \"title\": \"string\",\n              }\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n      try {\n        response.status(200).json({\n          schema: {\n            // If you are updating this be sure to update the collector METADATA_KEYS constant in /processRawText.\n            url: \"string | nullable\",\n            title: \"string\",\n            docAuthor: \"string | nullable\",\n            description: \"string | nullable\",\n            docSource: \"string | nullable\",\n            chunkSource: \"string | nullable\",\n            published: \"epoch timestamp in ms | nullable\",\n          },\n        });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  // Be careful and place as last route to prevent override of the other /document/ GET\n  // endpoints!\n  app.get(\"/v1/document/:docName\", [validApiKey], async (request, response) => {\n    /*\n    #swagger.tags = ['Documents']\n    #swagger.description = 'Get a single document by its unique AnythingLLM document name'\n    #swagger.parameters['docName'] = {\n        in: 'path',\n        description: 'Unique document name to find (name in /documents)',\n        required: true,\n        type: 'string'\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n             \"localFiles\": {\n              \"name\": \"documents\",\n              \"type\": \"folder\",\n              items: [\n                {\n                  \"name\": \"my-stored-document.txt-uuid1234.json\",\n                  \"type\": \"file\",\n                  \"id\": \"bb07c334-4dab-4419-9462-9d00065a49a1\",\n                  \"url\": \"file://my-stored-document.txt\",\n                  \"title\": \"my-stored-document.txt\",\n                  \"cached\": false\n                },\n              ]\n             }\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n    try {\n      const { docName } = request.params;\n      const document = await findDocumentInDocuments(docName);\n      if (!document) {\n        response.sendStatus(404).end();\n        return;\n      }\n      response.status(200).json({ document });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.post(\n    \"/v1/document/create-folder\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Documents']\n      #swagger.description = 'Create a new folder inside the documents storage directory.'\n      #swagger.requestBody = {\n        description: 'Name of the folder to create.',\n        required: true,\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'string',\n              example: {\n                \"name\": \"new-folder\"\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                success: true,\n                message: null\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      */\n      try {\n        const { name } = reqBody(request);\n        const storagePath = path.join(documentsPath, normalizePath(name));\n        if (!isWithin(path.resolve(documentsPath), path.resolve(storagePath)))\n          throw new Error(\"Invalid path name\");\n\n        if (fs.existsSync(storagePath)) {\n          response.status(500).json({\n            success: false,\n            message: \"Folder by that name already exists\",\n          });\n          return;\n        }\n\n        fs.mkdirSync(storagePath, { recursive: true });\n        response.status(200).json({ success: true, message: null });\n      } catch (e) {\n        console.error(e);\n        response.status(500).json({\n          success: false,\n          message: `Failed to create folder: ${e.message}`,\n        });\n      }\n    }\n  );\n\n  app.delete(\n    \"/v1/document/remove-folder\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Documents']\n      #swagger.description = 'Remove a folder and all its contents from the documents storage directory.'\n      #swagger.requestBody = {\n        description: 'Name of the folder to remove.',\n        required: true,\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              properties: {\n                name: {\n                  type: 'string',\n                  example: \"my-folder\"\n                }\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                success: true,\n                message: \"Folder removed successfully\"\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      */\n      try {\n        const { name } = reqBody(request);\n        await purgeFolder(name);\n        response\n          .status(200)\n          .json({ success: true, message: \"Folder removed successfully\" });\n      } catch (e) {\n        console.error(e);\n        response.status(500).json({\n          success: false,\n          message: `Failed to remove folder: ${e.message}`,\n        });\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/document/move-files\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Documents']\n      #swagger.description = 'Move files within the documents storage directory.'\n      #swagger.requestBody = {\n        description: 'Array of objects containing source and destination paths of files to move.',\n        required: true,\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                \"files\": [\n                  {\n                    \"from\": \"custom-documents/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json\",\n                    \"to\": \"folder/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json\"\n                  }\n                ]\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                success: true,\n                message: null\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      */\n      try {\n        const { files } = reqBody(request);\n        const docpaths = files.map(({ from }) => from);\n        const documents = await Document.where({ docpath: { in: docpaths } });\n        const embeddedFiles = documents.map((doc) => doc.docpath);\n        const moveableFiles = files.filter(\n          ({ from }) => !embeddedFiles.includes(from)\n        );\n        const movePromises = moveableFiles.map(({ from, to }) => {\n          const sourcePath = path.join(documentsPath, normalizePath(from));\n          const destinationPath = path.join(documentsPath, normalizePath(to));\n          return new Promise((resolve, reject) => {\n            if (\n              !isWithin(documentsPath, sourcePath) ||\n              !isWithin(documentsPath, destinationPath)\n            )\n              return reject(\"Invalid file location\");\n\n            fs.rename(sourcePath, destinationPath, (err) => {\n              if (err) {\n                console.error(`Error moving file ${from} to ${to}:`, err);\n                reject(err);\n              } else {\n                resolve();\n              }\n            });\n          });\n        });\n        Promise.all(movePromises)\n          .then(() => {\n            const unmovableCount = files.length - moveableFiles.length;\n            if (unmovableCount > 0) {\n              response.status(200).json({\n                success: true,\n                message: `${unmovableCount}/${files.length} files not moved. Unembed them from all workspaces.`,\n              });\n            } else {\n              response.status(200).json({\n                success: true,\n                message: null,\n              });\n            }\n          })\n          .catch((err) => {\n            console.error(\"Error moving files:\", err);\n            response\n              .status(500)\n              .json({ success: false, message: \"Failed to move some files.\" });\n          });\n      } catch (e) {\n        console.error(e);\n        response\n          .status(500)\n          .json({ success: false, message: \"Failed to move files.\" });\n      }\n    }\n  );\n}\n\nmodule.exports = { apiDocumentEndpoints };\n"
  },
  {
    "path": "server/endpoints/api/embed/index.js",
    "content": "const { EmbedConfig } = require(\"../../../models/embedConfig\");\nconst { EmbedChats } = require(\"../../../models/embedChats\");\nconst { validApiKey } = require(\"../../../utils/middleware/validApiKey\");\nconst { reqBody } = require(\"../../../utils/http\");\nconst { Workspace } = require(\"../../../models/workspace\");\n\nfunction apiEmbedEndpoints(app) {\n  if (!app) return;\n\n  app.get(\"/v1/embed\", [validApiKey], async (request, response) => {\n    /*\n      #swagger.tags = ['Embed']\n      #swagger.description = 'List all active embeds'\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                embeds: [\n                  {\n                    \"id\": 1,\n                    \"uuid\": \"embed-uuid-1\",\n                    \"enabled\": true,\n                    \"chat_mode\": \"query\",\n                    \"createdAt\": \"2023-04-01T12:00:00Z\",\n                    \"workspace\": {\n                      \"id\": 1,\n                      \"name\": \"Workspace 1\"\n                    },\n                    \"chat_count\": 10\n                  },\n                  {\n                    \"id\": 2,\n                    \"uuid\": \"embed-uuid-2\",\n                    \"enabled\": false,\n                    \"chat_mode\": \"chat\",\n                    \"createdAt\": \"2023-04-02T14:30:00Z\",\n                    \"workspace\": {\n                      \"id\": 1,\n                      \"name\": \"Workspace 1\"\n                    },\n                    \"chat_count\": 10\n                  }\n                ]\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n    */\n    try {\n      const embeds = await EmbedConfig.whereWithWorkspace();\n      const filteredEmbeds = embeds.map((embed) => ({\n        id: embed.id,\n        uuid: embed.uuid,\n        enabled: embed.enabled,\n        chat_mode: embed.chat_mode,\n        createdAt: embed.createdAt,\n        workspace: {\n          id: embed.workspace.id,\n          name: embed.workspace.name,\n        },\n        chat_count: embed._count.embed_chats,\n      }));\n      response.status(200).json({ embeds: filteredEmbeds });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.get(\n    \"/v1/embed/:embedUuid/chats\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Embed']\n      #swagger.description = 'Get all chats for a specific embed'\n      #swagger.parameters['embedUuid'] = {\n        in: 'path',\n        description: 'UUID of the embed',\n        required: true,\n        type: 'string'\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                chats: [\n                  {\n                    \"id\": 1,\n                    \"session_id\": \"session-uuid-1\",\n                    \"prompt\": \"Hello\",\n                    \"response\": \"Hi there!\",\n                    \"createdAt\": \"2023-04-01T12:00:00Z\"\n                  },\n                  {\n                    \"id\": 2,\n                    \"session_id\": \"session-uuid-2\",\n                    \"prompt\": \"How are you?\",\n                    \"response\": \"I'm doing well, thank you!\",\n                    \"createdAt\": \"2023-04-02T14:30:00Z\"\n                  }\n                ]\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      #swagger.responses[404] = {\n        description: \"Embed not found\",\n      }\n    */\n      try {\n        const { embedUuid } = request.params;\n        const chats = await EmbedChats.where({\n          embed_config: { uuid: String(embedUuid) },\n        });\n        response.status(200).json({ chats });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/v1/embed/:embedUuid/chats/:sessionUuid\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Embed']\n      #swagger.description = 'Get chats for a specific embed and session'\n      #swagger.parameters['embedUuid'] = {\n        in: 'path',\n        description: 'UUID of the embed',\n        required: true,\n        type: 'string'\n      }\n      #swagger.parameters['sessionUuid'] = {\n        in: 'path',\n        description: 'UUID of the session',\n        required: true,\n        type: 'string'\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                chats: [\n                  {\n                    \"id\": 1,\n                    \"prompt\": \"Hello\",\n                    \"response\": \"Hi there!\",\n                    \"createdAt\": \"2023-04-01T12:00:00Z\"\n                  }\n                ]\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      #swagger.responses[404] = {\n        description: \"Embed or session not found\",\n      }\n    */\n      try {\n        const { embedUuid, sessionUuid } = request.params;\n        const chats = await EmbedChats.where({\n          embed_config: { uuid: String(embedUuid) },\n          session_id: String(sessionUuid),\n        });\n        response.status(200).json({ chats });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\"/v1/embed/new\", [validApiKey], async (request, response) => {\n    /*\n      #swagger.tags = ['Embed']\n      #swagger.description = 'Create a new embed configuration'\n      #swagger.requestBody = {\n        description: 'JSON object containing embed configuration details',\n        required: true,\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                \"workspace_slug\": \"workspace-slug-1\",\n                \"chat_mode\": \"chat\",\n                \"allowlist_domains\": [\"example.com\"],\n                \"allow_model_override\": false,\n                \"allow_temperature_override\": false,\n                \"allow_prompt_override\": false,\n                \"max_chats_per_day\": 100,\n                \"max_chats_per_session\": 10\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                \"embed\": {\n                  \"id\": 1,\n                  \"uuid\": \"embed-uuid-1\",\n                  \"enabled\": true,\n                  \"chat_mode\": \"chat\",\n                  \"allowlist_domains\": [\"example.com\"],\n                  \"allow_model_override\": false,\n                  \"allow_temperature_override\": false,\n                  \"allow_prompt_override\": false,\n                  \"max_chats_per_day\": 100,\n                  \"max_chats_per_session\": 10,\n                  \"createdAt\": \"2023-04-01T12:00:00Z\",\n                  \"workspace_slug\": \"workspace-slug-1\"\n                },\n                \"error\": null\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      #swagger.responses[404] = {\n        description: \"Workspace not found\"\n      }\n    */\n    try {\n      const data = reqBody(request);\n\n      if (!data.workspace_slug)\n        return response\n          .status(400)\n          .json({ error: \"Workspace slug is required\" });\n      const workspace = await Workspace.get({\n        slug: String(data.workspace_slug),\n      });\n\n      if (!workspace)\n        return response.status(404).json({ error: \"Workspace not found\" });\n\n      const { embed, message: error } = await EmbedConfig.new({\n        ...data,\n        workspace_id: workspace.id,\n      });\n\n      response.status(200).json({ embed, error });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.post(\"/v1/embed/:embedUuid\", [validApiKey], async (request, response) => {\n    /*\n      #swagger.tags = ['Embed']\n      #swagger.description = 'Update an existing embed configuration'\n      #swagger.parameters['embedUuid'] = {\n        in: 'path',\n        description: 'UUID of the embed to update',\n        required: true,\n        type: 'string'\n      }\n      #swagger.requestBody = {\n        description: 'JSON object containing embed configuration updates',\n        required: true,\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                \"enabled\": true,\n                \"chat_mode\": \"chat\",\n                \"allowlist_domains\": [\"example.com\"],\n                \"allow_model_override\": false,\n                \"allow_temperature_override\": false,\n                \"allow_prompt_override\": false,\n                \"max_chats_per_day\": 100,\n                \"max_chats_per_session\": 10\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                \"success\": true,\n                \"error\": null\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      #swagger.responses[404] = {\n        description: \"Embed not found\"\n      }\n    */\n    try {\n      const { embedUuid } = request.params;\n      const data = reqBody(request);\n\n      const embed = await EmbedConfig.get({ uuid: String(embedUuid) });\n      if (!embed) {\n        return response.status(404).json({ error: \"Embed not found\" });\n      }\n\n      const { success, error } = await EmbedConfig.update(embed.id, data);\n      response.status(200).json({ success, error });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.delete(\n    \"/v1/embed/:embedUuid\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Embed']\n      #swagger.description = 'Delete an existing embed configuration'\n      #swagger.parameters['embedUuid'] = {\n        in: 'path',\n        description: 'UUID of the embed to delete',\n        required: true,\n        type: 'string'\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                \"success\": true,\n                \"error\": null\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      #swagger.responses[404] = {\n        description: \"Embed not found\"\n      }\n    */\n      try {\n        const { embedUuid } = request.params;\n        const embed = await EmbedConfig.get({ uuid: String(embedUuid) });\n        if (!embed)\n          return response.status(404).json({ error: \"Embed not found\" });\n        const success = await EmbedConfig.delete({ id: embed.id });\n        response\n          .status(200)\n          .json({ success, error: success ? null : \"Failed to delete embed\" });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { apiEmbedEndpoints };\n"
  },
  {
    "path": "server/endpoints/api/index.js",
    "content": "const { useSwagger } = require(\"../../swagger/utils\");\nconst { apiAdminEndpoints } = require(\"./admin\");\nconst { apiAuthEndpoints } = require(\"./auth\");\nconst { apiDocumentEndpoints } = require(\"./document\");\nconst { apiSystemEndpoints } = require(\"./system\");\nconst { apiWorkspaceEndpoints } = require(\"./workspace\");\nconst { apiWorkspaceThreadEndpoints } = require(\"./workspaceThread\");\nconst { apiUserManagementEndpoints } = require(\"./userManagement\");\nconst { apiOpenAICompatibleEndpoints } = require(\"./openai\");\nconst { apiEmbedEndpoints } = require(\"./embed\");\n\n// All endpoints must be documented and pass through the validApiKey Middleware.\n// How to JSDoc an endpoint\n// https://www.npmjs.com/package/swagger-autogen#openapi-3x\nfunction developerEndpoints(app, router) {\n  if (!router) return;\n  useSwagger(app);\n  apiAuthEndpoints(router);\n  apiAdminEndpoints(router);\n  apiSystemEndpoints(router);\n  apiWorkspaceEndpoints(router);\n  apiDocumentEndpoints(router);\n  apiWorkspaceThreadEndpoints(router);\n  apiUserManagementEndpoints(router);\n  apiOpenAICompatibleEndpoints(router);\n  apiEmbedEndpoints(router);\n}\n\nmodule.exports = { developerEndpoints };\n"
  },
  {
    "path": "server/endpoints/api/openai/compatibility-test-script.cjs",
    "content": "const OpenAI = require(\"openai\");\n\n/**\n * @type {import(\"openai\").OpenAI}\n */\nconst client = new OpenAI({\n  baseURL: \"http://localhost:3001/api/v1/openai\",\n  apiKey: \"ENTER_ANYTHINGLLM_API_KEY_HERE\",\n});\n\n(async () => {\n  // Models endpoint testing.\n  console.log(\"Fetching /models\");\n  const modelList = await client.models.list();\n  for await (const model of modelList) {\n    console.log({ model });\n  }\n\n  // Test sync chat completion\n  console.log(\"Running synchronous chat message\");\n  const syncCompletion = await client.chat.completions.create({\n    messages: [\n      {\n        role: \"system\",\n        content: \"You are a helpful assistant who only speaks like a pirate.\",\n      },\n      { role: \"user\", content: \"What is AnythingLLM?\" },\n      // {\n      //   role: 'assistant',\n      //   content: \"Arrr, matey! AnythingLLM be a fine tool fer sailin' the treacherous sea o' information with a powerful language model at yer helm. It's a potent instrument to handle all manner o' tasks involvin' text, like answerin' questions, generating prose, or even havin' a chat with digital scallywags like meself. Be there any specific treasure ye seek in the realm o' AnythingLLM?\"\n      // },\n      // { role: \"user\", content: \"Why are you talking like a pirate?\" },\n    ],\n    model: \"anythingllm\", // must be workspace-slug\n  });\n  console.log(syncCompletion.choices[0]);\n\n  // Test sync chat streaming completion\n  console.log(\"Running asynchronous chat message\");\n  const asyncCompletion = await client.chat.completions.create({\n    messages: [\n      {\n        role: \"system\",\n        content: \"You are a helpful assistant who only speaks like a pirate.\",\n      },\n      { role: \"user\", content: \"What is AnythingLLM?\" },\n    ],\n    model: \"anythingllm\", // must be workspace-slug\n    stream: true,\n  });\n\n  let message = \"\";\n  for await (const chunk of asyncCompletion) {\n    message += chunk.choices[0].delta.content;\n    console.log({ message });\n  }\n\n  // Test embeddings creation\n  console.log(\"Creating embeddings\");\n  const embedding = await client.embeddings.create({\n    model: null, // model is optional for AnythingLLM\n    input: \"This is a test string for embedding\",\n    encoding_format: \"float\",\n  });\n  console.log(\"Embedding created successfully:\");\n  console.log(`Dimensions: ${embedding.data[0].embedding.length}`);\n  console.log(\n    `First few values:`,\n    embedding.data[0].embedding.slice(0, 5),\n    `+ ${embedding.data[0].embedding.length - 5} more`\n  );\n\n  // Vector DB functionality\n  console.log(\"Fetching /vector_stores\");\n  const vectorDBList = await client.beta.vectorStores.list();\n  for await (const db of vectorDBList) {\n    console.log(db);\n  }\n})();\n"
  },
  {
    "path": "server/endpoints/api/openai/helpers.js",
    "content": "/**\n * Extracts text content from a multimodal message\n * If the content has multiple text items, it will join them together with a newline.\n * @param {string|Array} content - Message content that could be string or array of content objects\n * @returns {string} - The text content\n */\nfunction extractTextContent(content) {\n  if (!Array.isArray(content)) return content;\n  return content\n    .filter((item) => item.type === \"text\")\n    .map((item) => item.text)\n    .join(\"\\n\");\n}\n\n/**\n * Detects mime type from a base64 data URL string, defaults to PNG if not detected\n * @param {string} dataUrl - The data URL string (e.g. data:image/jpeg;base64,...)\n * @returns {string} - The mime type or 'image/png' if not detected\n */\nfunction getMimeTypeFromDataUrl(dataUrl) {\n  try {\n    const matches = dataUrl.match(/^data:([^;]+);base64,/);\n    return matches ? matches[1].toLowerCase() : \"image/png\";\n  } catch {\n    return \"image/png\";\n  }\n}\n\n/**\n * Extracts attachments from a multimodal message\n * The attachments provided are in OpenAI format since this util is used in the OpenAI compatible chat.\n * However, our backend internal chat uses the Attachment type we use elsewhere in the app so we have to convert it.\n * @param {Array} content - Message content that could be string or array of content objects\n * @returns {import(\"../../../utils/helpers\").Attachment[]} - The attachments\n */\nfunction extractAttachments(content) {\n  if (!Array.isArray(content)) return [];\n  return content\n    .filter((item) => item.type === \"image_url\")\n    .map((item, index) => ({\n      name: `uploaded_image_${index}`,\n      mime: getMimeTypeFromDataUrl(item.image_url.url),\n      contentString: item.image_url.url,\n    }));\n}\n\nmodule.exports = {\n  extractTextContent,\n  extractAttachments,\n};\n"
  },
  {
    "path": "server/endpoints/api/openai/index.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst { Document } = require(\"../../../models/documents\");\nconst { Telemetry } = require(\"../../../models/telemetry\");\nconst { Workspace } = require(\"../../../models/workspace\");\nconst {\n  getLLMProvider,\n  getEmbeddingEngineSelection,\n} = require(\"../../../utils/helpers\");\nconst { reqBody } = require(\"../../../utils/http\");\nconst { validApiKey } = require(\"../../../utils/middleware/validApiKey\");\nconst { EventLogs } = require(\"../../../models/eventLogs\");\nconst {\n  OpenAICompatibleChat,\n} = require(\"../../../utils/chats/openaiCompatible\");\nconst { getModelTag } = require(\"../../utils\");\nconst { extractTextContent, extractAttachments } = require(\"./helpers\");\n\nfunction apiOpenAICompatibleEndpoints(app) {\n  if (!app) return;\n\n  app.get(\"/v1/openai/models\", [validApiKey], async (_, response) => {\n    /*\n    #swagger.tags = ['OpenAI Compatible Endpoints']\n    #swagger.description = 'Get all available \"models\" which are workspaces you can use for chatting.'\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          \"schema\": {\n            \"type\": \"object\",\n            \"example\": {\n              \"object\": \"list\",\n              \"data\": [\n                {\n                  \"id\": \"model-id-0\",\n                  \"object\": \"model\",\n                  \"created\": 1686935002,\n                  \"owned_by\": \"organization-owner\"\n                },\n                {\n                  \"id\": \"model-id-1\",\n                  \"object\": \"model\",\n                  \"created\": 1686935002,\n                  \"owned_by\": \"organization-owner\"\n                }\n              ]\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n    try {\n      const data = [];\n      const workspaces = await Workspace.where();\n      for (const workspace of workspaces) {\n        const provider = workspace?.chatProvider ?? process.env.LLM_PROVIDER;\n        let LLMProvider = getLLMProvider({\n          provider,\n          model: workspace?.chatModel,\n        });\n        data.push({\n          id: workspace.slug,\n          object: \"model\",\n          created: Math.floor(Number(new Date(workspace.createdAt)) / 1000),\n          owned_by: `${provider}-${LLMProvider.model}`,\n        });\n      }\n      return response.status(200).json({\n        object: \"list\",\n        data,\n      });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.post(\n    \"/v1/openai/chat/completions\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['OpenAI Compatible Endpoints']\n      #swagger.description = 'Execute a chat with a workspace with OpenAI compatibility. Supports streaming as well. Model must be a workspace slug from /models.'\n      #swagger.requestBody = {\n          description: 'Send a prompt to the workspace with full use of documents as if sending a chat in AnythingLLM. Only supports some values of OpenAI API. See example below.',\n          required: true,\n          content: {\n            \"application/json\": {\n              example: {\n                messages: [\n                {\"role\":\"system\", content: \"You are a helpful assistant\"},\n                {\"role\":\"user\", content: \"What is AnythingLLM?\"},\n                {\"role\":\"assistant\", content: \"AnythingLLM is....\"},\n                {\"role\":\"user\", content: \"Follow up question...\"}\n                ],\n                model: \"sample-workspace\",\n                stream: true,\n                temperature: 0.7\n              }\n            }\n          }\n        }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      */\n      try {\n        const {\n          model,\n          messages = [],\n          temperature,\n          stream = false,\n        } = reqBody(request);\n        const workspace = await Workspace.get({ slug: String(model) });\n        if (!workspace) return response.status(401).end();\n\n        const userMessage = messages.pop();\n        if (userMessage.role !== \"user\") {\n          return response.status(400).json({\n            id: uuidv4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error:\n              \"No user prompt found. Must be last element in message array with 'user' role.\",\n          });\n        }\n\n        const systemPrompt =\n          messages.find((chat) => chat.role === \"system\")?.content ?? null;\n        const history = messages.filter((chat) => chat.role !== \"system\") ?? [];\n\n        if (!stream) {\n          const chatResult = await OpenAICompatibleChat.chatSync({\n            workspace,\n            systemPrompt,\n            history,\n            prompt: extractTextContent(userMessage.content),\n            attachments: extractAttachments(userMessage.content),\n            temperature: Number(temperature),\n          });\n\n          await Telemetry.sendTelemetry(\"sent_chat\", {\n            LLMSelection:\n              workspace.chatProvider ?? process.env.LLM_PROVIDER ?? \"openai\",\n            Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n            VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n            TTSSelection: process.env.TTS_PROVIDER || \"native\",\n          });\n          await EventLogs.logEvent(\"api_sent_chat\", {\n            workspaceName: workspace?.name,\n            chatModel: workspace?.chatModel || \"System Default\",\n          });\n          return response.status(200).json(chatResult);\n        }\n\n        response.setHeader(\"Cache-Control\", \"no-cache\");\n        response.setHeader(\"Content-Type\", \"text/event-stream\");\n        response.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n        response.setHeader(\"Connection\", \"keep-alive\");\n        response.flushHeaders();\n\n        await OpenAICompatibleChat.streamChat({\n          workspace,\n          systemPrompt,\n          history,\n          prompt: extractTextContent(userMessage.content),\n          attachments: extractAttachments(userMessage.content),\n          temperature: Number(temperature),\n          response,\n        });\n        await Telemetry.sendTelemetry(\"sent_chat\", {\n          LLMSelection: process.env.LLM_PROVIDER || \"openai\",\n          Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n          VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n          TTSSelection: process.env.TTS_PROVIDER || \"native\",\n          LLMModel: getModelTag(),\n        });\n        await EventLogs.logEvent(\"api_sent_chat\", {\n          workspaceName: workspace?.name,\n          chatModel: workspace?.chatModel || \"System Default\",\n        });\n        response.end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.status(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/openai/embeddings\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['OpenAI Compatible Endpoints']\n      #swagger.description = 'Get the embeddings of any arbitrary text string. This will use the embedder provider set in the system. Please ensure the token length of each string fits within the context of your embedder model.'\n      #swagger.requestBody = {\n          description: 'The input string(s) to be embedded. If the text is too long for the embedder model context, it will fail to embed. The vector and associated chunk metadata will be returned in the array order provided',\n          required: true,\n          content: {\n            \"application/json\": {\n              example: {\n                input: [\n                \"This is my first string to embed\",\n                \"This is my second string to embed\",\n                ],\n                model: null,\n              }\n            }\n          }\n        }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      */\n      try {\n        const body = reqBody(request);\n        // Support input or \"inputs\" (for backwards compatibility) as an array of strings or a single string\n        // TODO: \"inputs\" key support will eventually be fully removed.\n        let input = body?.input || body?.inputs || [];\n        // if input is not an array, make it an array and force to string content\n        if (!Array.isArray(input)) input = [String(input)];\n\n        if (Array.isArray(input)) {\n          if (input.length === 0)\n            throw new Error(\"Input array cannot be empty.\");\n          const validArray = input.every((text) => typeof text === \"string\");\n          if (!validArray)\n            throw new Error(\"All inputs to be embedded must be strings.\");\n        }\n\n        const Embedder = getEmbeddingEngineSelection();\n        const embeddings = await Embedder.embedChunks(input);\n        const data = [];\n        embeddings.forEach((embedding, index) => {\n          data.push({\n            object: \"embedding\",\n            embedding,\n            index,\n          });\n        });\n\n        return response.status(200).json({\n          object: \"list\",\n          data,\n          model: Embedder.model,\n        });\n      } catch (e) {\n        console.error(e.message, e);\n        response.status(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/v1/openai/vector_stores\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['OpenAI Compatible Endpoints']\n      #swagger.description = 'List all the vector database collections connected to AnythingLLM. These are essentially workspaces but return their unique vector db identifier - this is the same as the workspace slug.'\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            \"schema\": {\n              \"type\": \"object\",\n              \"example\": {\n                \"data\": [\n                  {\n                    \"id\": \"slug-here\",\n                    \"object\": \"vector_store\",\n                    \"name\": \"My workspace\",\n                    \"file_counts\": {\n                      \"total\": 3\n                    },\n                    \"provider\": \"LanceDB\"\n                  }\n                ]\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      */\n      try {\n        // We dump all in the first response and despite saying there is\n        // not more data the library still checks with a query param so if\n        // we detect one - respond with nothing.\n        if (Object.keys(request?.query ?? {}).length !== 0) {\n          return response.status(200).json({\n            data: [],\n            has_more: false,\n          });\n        }\n\n        const data = [];\n        const VectorDBProvider = process.env.VECTOR_DB || \"lancedb\";\n        const workspaces = await Workspace.where();\n\n        for (const workspace of workspaces) {\n          data.push({\n            id: workspace.slug,\n            object: \"vector_store\",\n            name: workspace.name,\n            file_counts: {\n              total: await Document.count({\n                workspaceId: Number(workspace.id),\n              }),\n            },\n            provider: VectorDBProvider,\n          });\n        }\n        return response.status(200).json({\n          first_id: [...data].splice(0)?.[0]?.id,\n          last_id: [...data].splice(-1)?.[0]?.id ?? data.splice(1)?.[0]?.id,\n          data,\n          has_more: false,\n        });\n      } catch (e) {\n        console.error(e.message, e);\n        response.status(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { apiOpenAICompatibleEndpoints };\n"
  },
  {
    "path": "server/endpoints/api/system/index.js",
    "content": "const { EventLogs } = require(\"../../../models/eventLogs\");\nconst { SystemSettings } = require(\"../../../models/systemSettings\");\nconst { purgeDocument } = require(\"../../../utils/files/purgeDocument\");\nconst { getVectorDbClass } = require(\"../../../utils/helpers\");\nconst { exportChatsAsType } = require(\"../../../utils/helpers/chat/convertTo\");\nconst { dumpENV, updateENV } = require(\"../../../utils/helpers/updateENV\");\nconst { reqBody } = require(\"../../../utils/http\");\nconst { validApiKey } = require(\"../../../utils/middleware/validApiKey\");\n\nfunction apiSystemEndpoints(app) {\n  if (!app) return;\n\n  app.get(\"/v1/system/env-dump\", async (_, response) => {\n    /*\n   #swagger.tags = ['System Settings']\n   #swagger.description = 'Dump all settings to file storage'\n   #swagger.responses[403] = {\n     schema: {\n       \"$ref\": \"#/definitions/InvalidAPIKey\"\n     }\n   }\n   */\n    try {\n      if (process.env.NODE_ENV !== \"production\")\n        return response.sendStatus(200).end();\n      dumpENV();\n      response.sendStatus(200).end();\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.get(\"/v1/system\", [validApiKey], async (_, response) => {\n    /*\n    #swagger.tags = ['System Settings']\n    #swagger.description = 'Get all current system settings that are defined.'\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n             \"settings\": {\n                \"VectorDB\": \"pinecone\",\n                \"PineConeKey\": true,\n                \"PineConeIndex\": \"my-pinecone-index\",\n                \"LLMProvider\": \"azure\",\n                \"[KEY_NAME]\": \"KEY_VALUE\",\n              }\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n    try {\n      const settings = await SystemSettings.currentSettings();\n      response.status(200).json({ settings });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.get(\"/v1/system/vector-count\", [validApiKey], async (_, response) => {\n    /*\n    #swagger.tags = ['System Settings']\n    #swagger.description = 'Number of all vectors in connected vector database'\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n             \"vectorCount\": 5450\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n    try {\n      const VectorDb = getVectorDbClass();\n      const vectorCount = await VectorDb.totalVectors();\n      response.status(200).json({ vectorCount });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.post(\n    \"/v1/system/update-env\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['System Settings']\n      #swagger.description = 'Update a system setting or preference.'\n      #swagger.requestBody = {\n        description: 'Key pair object that matches a valid setting and value. Get keys from GET /v1/system or refer to codebase.',\n        required: true,\n        content: {\n          \"application/json\": {\n            example: {\n              VectorDB: \"lancedb\",\n              AnotherKey: \"updatedValue\"\n            }\n          }\n        }\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                newValues: {\"[ENV_KEY]\": 'Value'},\n                error: 'error goes here, otherwise null'\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      */\n      try {\n        const body = reqBody(request);\n        const { newValues, error } = await updateENV(body);\n        response.status(200).json({ newValues, error });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/v1/system/export-chats\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['System Settings']\n    #swagger.description = 'Export all of the chats from the system in a known format. Output depends on the type sent. Will be send with the correct header for the output.'\n   #swagger.parameters['type'] = {\n      in: 'query',\n      description: \"Export format jsonl, json, csv, jsonAlpaca\",\n      required: false,\n      type: 'string'\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: [\n              {\n                \"role\": \"user\",\n                \"content\": \"What is AnythinglLM?\"\n              },\n              {\n                \"role\": \"assistant\",\n                \"content\": \"AnythingLLM is a knowledge graph and vector database management system built using NodeJS express server. It provides an interface for handling all interactions, including vectorDB management and LLM (Language Model) interactions.\"\n              },\n            ]\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n      try {\n        const { type = \"jsonl\" } = request.query;\n        const { contentType, data } = await exportChatsAsType(\n          type,\n          \"workspace\"\n        );\n        await EventLogs.logEvent(\"exported_chats\", {\n          type,\n        });\n        response.setHeader(\"Content-Type\", contentType);\n        response.status(200).send(data);\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n  app.delete(\n    \"/v1/system/remove-documents\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['System Settings']\n      #swagger.description = 'Permanently remove documents from the system.'\n      #swagger.requestBody = {\n        description: 'Array of document names to be removed permanently.',\n        required: true,\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              properties: {\n                names: {\n                  type: 'array',\n                  items: {\n                    type: 'string'\n                  },\n                  example: [\n                    \"custom-documents/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json\"\n                  ]\n                }\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[200] = {\n        description: 'Documents removed successfully.',\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                success: true,\n                message: 'Documents removed successfully'\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        description: 'Forbidden',\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      #swagger.responses[500] = {\n        description: 'Internal Server Error'\n      }\n      */\n      try {\n        const { names } = reqBody(request);\n        for await (const name of names) await purgeDocument(name);\n        response\n          .status(200)\n          .json({ success: true, message: \"Documents removed successfully\" })\n          .end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { apiSystemEndpoints };\n"
  },
  {
    "path": "server/endpoints/api/userManagement/index.js",
    "content": "const { User } = require(\"../../../models/user\");\nconst { TemporaryAuthToken } = require(\"../../../models/temporaryAuthToken\");\nconst { multiUserMode } = require(\"../../../utils/http\");\nconst {\n  simpleSSOEnabled,\n} = require(\"../../../utils/middleware/simpleSSOEnabled\");\nconst { validApiKey } = require(\"../../../utils/middleware/validApiKey\");\n\nfunction apiUserManagementEndpoints(app) {\n  if (!app) return;\n\n  app.get(\"/v1/users\", [validApiKey], async (request, response) => {\n    /*\n      #swagger.tags = ['User Management']\n      #swagger.description = 'List all users'\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                users: [\n                  {\n                    \"id\": 1,\n                    \"username\": \"john_doe\",\n                    \"role\": \"admin\"\n                  },\n                  {\n                    \"id\": 2,\n                    \"username\": \"jane_smith\",\n                    \"role\": \"default\"\n                  }\n                ]\n              }\n            }\n          }\n        }\n      }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n     #swagger.responses[401] = {\n      description: \"Instance is not in Multi-User mode. Permission denied.\",\n    }\n      */\n    try {\n      if (!multiUserMode(response))\n        return response\n          .status(401)\n          .send(\"Instance is not in Multi-User mode. Permission denied.\");\n\n      const users = await User.where();\n      const filteredUsers = users.map((user) => ({\n        id: user.id,\n        username: user.username,\n        role: user.role,\n      }));\n      response.status(200).json({ users: filteredUsers });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.get(\n    \"/v1/users/:id/issue-auth-token\",\n    [validApiKey, simpleSSOEnabled],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['User Management']\n      #swagger.description = 'Issue a temporary auth token for a user'\n      #swagger.parameters['id'] = {\n        in: 'path',\n        description: 'The ID of the user to issue a temporary auth token for',\n        required: true,\n        type: 'string'\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                token: \"1234567890\",\n                loginPath: \"/sso/simple?token=1234567890\"\n              }\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n     #swagger.responses[401] = {\n      description: \"Instance is not in Multi-User mode. Permission denied.\",\n    }\n      */\n      try {\n        const { id: userId } = request.params;\n        const user = await User.get({ id: Number(userId) });\n        if (!user)\n          return response.status(404).json({ error: \"User not found\" });\n\n        const { token, error } = await TemporaryAuthToken.issue(userId);\n        if (error) return response.status(500).json({ error: error });\n\n        response.status(200).json({\n          token: String(token),\n          loginPath: `/sso/simple?token=${token}`,\n        });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { apiUserManagementEndpoints };\n"
  },
  {
    "path": "server/endpoints/api/workspace/index.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst { Document } = require(\"../../../models/documents\");\nconst { Telemetry } = require(\"../../../models/telemetry\");\nconst { DocumentVectors } = require(\"../../../models/vectors\");\nconst { Workspace } = require(\"../../../models/workspace\");\nconst { WorkspaceChats } = require(\"../../../models/workspaceChats\");\nconst { getVectorDbClass, getLLMProvider } = require(\"../../../utils/helpers\");\nconst { multiUserMode, reqBody } = require(\"../../../utils/http\");\nconst { validApiKey } = require(\"../../../utils/middleware/validApiKey\");\nconst { VALID_CHAT_MODE } = require(\"../../../utils/chats/stream\");\nconst { EventLogs } = require(\"../../../models/eventLogs\");\nconst {\n  convertToChatHistory,\n  writeResponseChunk,\n} = require(\"../../../utils/helpers/chat/responses\");\nconst { ApiChatHandler } = require(\"../../../utils/chats/apiChatHandler\");\nconst { getModelTag } = require(\"../../utils\");\n\nfunction apiWorkspaceEndpoints(app) {\n  if (!app) return;\n\n  app.post(\"/v1/workspace/new\", [validApiKey], async (request, response) => {\n    /*\n    #swagger.tags = ['Workspaces']\n    #swagger.description = 'Create a new workspace'\n    #swagger.requestBody = {\n      description: 'JSON object containing workspace configuration.',\n      required: true,\n      content: {\n        \"application/json\": {\n          example: {\n            name: \"My New Workspace\",\n            similarityThreshold: 0.7,\n            openAiTemp: 0.7,\n            openAiHistory: 20,\n            openAiPrompt: \"Custom prompt for responses\",\n            queryRefusalResponse: \"Custom refusal message\",\n            chatMode: \"chat\",\n            topN: 4\n          }\n        }\n      }\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              workspace: {\n                \"id\": 79,\n                \"name\": \"Sample workspace\",\n                \"slug\": \"sample-workspace\",\n                \"createdAt\": \"2023-08-17 00:45:03\",\n                \"openAiTemp\": null,\n                \"lastUpdatedAt\": \"2023-08-17 00:45:03\",\n                \"openAiHistory\": 20,\n                \"openAiPrompt\": null\n              },\n              message: 'Workspace created'\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n    try {\n      const { name = null, ...additionalFields } = reqBody(request);\n      const { workspace, message } = await Workspace.new(\n        name,\n        null,\n        additionalFields\n      );\n\n      if (!workspace) {\n        response.status(400).json({ workspace: null, message });\n        return;\n      }\n\n      await Telemetry.sendTelemetry(\"workspace_created\", {\n        multiUserMode: multiUserMode(response),\n        LLMSelection: process.env.LLM_PROVIDER || \"openai\",\n        Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n        VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n        TTSSelection: process.env.TTS_PROVIDER || \"native\",\n        LLMModel: getModelTag(),\n      });\n      await EventLogs.logEvent(\"api_workspace_created\", {\n        workspaceName: workspace?.name || \"Unknown Workspace\",\n      });\n      response.status(200).json({ workspace, message });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.get(\"/v1/workspaces\", [validApiKey], async (request, response) => {\n    /*\n    #swagger.tags = ['Workspaces']\n    #swagger.description = 'List all current workspaces'\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              workspaces: [\n                {\n                  \"id\": 79,\n                  \"name\": \"Sample workspace\",\n                  \"slug\": \"sample-workspace\",\n                  \"createdAt\": \"2023-08-17 00:45:03\",\n                  \"openAiTemp\": null,\n                  \"lastUpdatedAt\": \"2023-08-17 00:45:03\",\n                  \"openAiHistory\": 20,\n                  \"openAiPrompt\": null,\n                  \"threads\": []\n                }\n              ],\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n    try {\n      const workspaces = await Workspace._findMany({\n        where: {},\n        include: {\n          threads: {\n            select: {\n              user_id: true,\n              slug: true,\n              name: true,\n            },\n          },\n        },\n      });\n      response.status(200).json({ workspaces });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.get(\"/v1/workspace/:slug\", [validApiKey], async (request, response) => {\n    /*\n    #swagger.tags = ['Workspaces']\n    #swagger.description = 'Get a workspace by its unique slug.'\n    #swagger.parameters['slug'] = {\n        in: 'path',\n        description: 'Unique slug of workspace to find',\n        required: true,\n        type: 'string'\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              workspace: [\n                {\n                  \"id\": 79,\n                  \"name\": \"My workspace\",\n                  \"slug\": \"my-workspace-123\",\n                  \"createdAt\": \"2023-08-17 00:45:03\",\n                  \"openAiTemp\": null,\n                  \"lastUpdatedAt\": \"2023-08-17 00:45:03\",\n                  \"openAiHistory\": 20,\n                  \"openAiPrompt\": null,\n                  \"documents\": [],\n                  \"threads\": []\n                }\n              ]\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n    try {\n      const { slug } = request.params;\n      const workspace = await Workspace._findMany({\n        where: {\n          slug: String(slug),\n        },\n        include: {\n          documents: true,\n          threads: {\n            select: {\n              user_id: true,\n              slug: true,\n            },\n          },\n        },\n      });\n\n      response.status(200).json({ workspace });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.delete(\n    \"/v1/workspace/:slug\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Workspaces']\n    #swagger.description = 'Deletes a workspace by its slug.'\n    #swagger.parameters['slug'] = {\n        in: 'path',\n        description: 'Unique slug of workspace to delete',\n        required: true,\n        type: 'string'\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n      try {\n        const { slug = \"\" } = request.params;\n        const VectorDb = getVectorDbClass();\n        const workspace = await Workspace.get({ slug });\n\n        if (!workspace) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        const workspaceId = Number(workspace.id);\n        await WorkspaceChats.delete({ workspaceId: workspaceId });\n        await DocumentVectors.deleteForWorkspace(workspaceId);\n        await Document.delete({ workspaceId: workspaceId });\n        await Workspace.delete({ id: workspaceId });\n\n        await EventLogs.logEvent(\"api_workspace_deleted\", {\n          workspaceName: workspace?.name || \"Unknown Workspace\",\n        });\n        try {\n          await VectorDb[\"delete-namespace\"]({ namespace: slug });\n        } catch (e) {\n          console.error(e.message);\n        }\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/workspace/:slug/update\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Workspaces']\n    #swagger.description = 'Update workspace settings by its unique slug.'\n    #swagger.parameters['slug'] = {\n        in: 'path',\n        description: 'Unique slug of workspace to find',\n        required: true,\n        type: 'string'\n    }\n    #swagger.requestBody = {\n      description: 'JSON object containing new settings to update a workspace. All keys are optional and will not update unless provided',\n      required: true,\n      content: {\n        \"application/json\": {\n          example: {\n            \"name\": 'Updated Workspace Name',\n            \"openAiTemp\": 0.2,\n            \"openAiHistory\": 20,\n            \"openAiPrompt\": \"Respond to all inquires and questions in binary - do not respond in any other format.\"\n          }\n        }\n      }\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              workspace: {\n                \"id\": 79,\n                \"name\": \"My workspace\",\n                \"slug\": \"my-workspace-123\",\n                \"createdAt\": \"2023-08-17 00:45:03\",\n                \"openAiTemp\": null,\n                \"lastUpdatedAt\": \"2023-08-17 00:45:03\",\n                \"openAiHistory\": 20,\n                \"openAiPrompt\": null,\n                \"documents\": []\n              },\n              message: null,\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n      try {\n        const { slug = null } = request.params;\n        const data = reqBody(request);\n        const currWorkspace = await Workspace.get({ slug });\n\n        if (!currWorkspace) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        const { workspace, message } = await Workspace.update(\n          currWorkspace.id,\n          data\n        );\n        response.status(200).json({ workspace, message });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/v1/workspace/:slug/chats\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Workspaces']\n    #swagger.description = 'Get a workspaces chats regardless of user by its unique slug.'\n    #swagger.parameters['slug'] = {\n        in: 'path',\n        description: 'Unique slug of workspace to find',\n        required: true,\n        type: 'string'\n    }\n    #swagger.parameters['apiSessionId'] = {\n        in: 'query',\n        description: 'Optional apiSessionId to filter by',\n        required: false,\n        type: 'string'\n    }\n    #swagger.parameters['limit'] = {\n        in: 'query',\n        description: 'Optional number of chat messages to return (default: 100)',\n        required: false,\n        type: 'integer'\n    }\n    #swagger.parameters['orderBy'] = {\n        in: 'query',\n        description: 'Optional order of chat messages (asc or desc)',\n        required: false,\n        type: 'string'\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              history: [\n                {\n                  \"role\": \"user\",\n                  \"content\": \"What is AnythingLLM?\",\n                  \"sentAt\": 1692851630\n                },\n                {\n                  \"role\": \"assistant\",\n                  \"content\": \"AnythingLLM is a platform that allows you to convert notes, PDFs, and other source materials into a chatbot. It ensures privacy, cites its answers, and allows multiple people to interact with the same documents simultaneously. It is particularly useful for businesses to enhance the visibility and readability of various written communications such as SOPs, contracts, and sales calls. You can try it out with a free trial to see if it meets your business needs.\",\n                  \"sources\": [{\"source\": \"object about source document and snippets used\"}]\n                }\n              ]\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n      try {\n        const { slug } = request.params;\n        const {\n          apiSessionId = null,\n          limit = 100,\n          orderBy = \"asc\",\n        } = request.query;\n        const workspace = await Workspace.get({ slug });\n\n        if (!workspace) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        const validLimit = Math.max(1, parseInt(limit));\n        const validOrderBy = [\"asc\", \"desc\"].includes(orderBy)\n          ? orderBy\n          : \"asc\";\n\n        const history = apiSessionId\n          ? await WorkspaceChats.forWorkspaceByApiSessionId(\n              workspace.id,\n              apiSessionId,\n              validLimit,\n              { createdAt: validOrderBy }\n            )\n          : await WorkspaceChats.forWorkspace(workspace.id, validLimit, {\n              createdAt: validOrderBy,\n            });\n        response.status(200).json({ history: convertToChatHistory(history) });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/workspace/:slug/update-embeddings\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Workspaces']\n    #swagger.description = 'Add or remove documents from a workspace by its unique slug.'\n    #swagger.parameters['slug'] = {\n        in: 'path',\n        description: 'Unique slug of workspace to find',\n        required: true,\n        type: 'string'\n    }\n    #swagger.requestBody = {\n      description: 'JSON object of additions and removals of documents to add to update a workspace. The value should be the folder + filename with the exclusions of the top-level documents path.',\n      required: true,\n      content: {\n        \"application/json\": {\n          example: {\n            adds: [\"custom-documents/my-pdf.pdf-hash.json\"],\n            deletes: [\"custom-documents/anythingllm.txt-hash.json\"]\n          }\n        }\n      }\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              workspace: {\n                \"id\": 79,\n                \"name\": \"My workspace\",\n                \"slug\": \"my-workspace-123\",\n                \"createdAt\": \"2023-08-17 00:45:03\",\n                \"openAiTemp\": null,\n                \"lastUpdatedAt\": \"2023-08-17 00:45:03\",\n                \"openAiHistory\": 20,\n                \"openAiPrompt\": null,\n                \"documents\": []\n              },\n              message: null,\n            }\n          }\n        }\n      }\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n      try {\n        const { slug = null } = request.params;\n        const { adds = [], deletes = [] } = reqBody(request);\n        const currWorkspace = await Workspace.get({ slug });\n\n        if (!currWorkspace) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        await Document.removeDocuments(currWorkspace, deletes);\n        await Document.addDocuments(currWorkspace, adds);\n        const updatedWorkspace = await Workspace.get({\n          id: Number(currWorkspace.id),\n        });\n        response.status(200).json({ workspace: updatedWorkspace });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/workspace/:slug/update-pin\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Workspaces']\n      #swagger.description = 'Add or remove pin from a document in a workspace by its unique slug.'\n      #swagger.parameters['slug'] = {\n          in: 'path',\n          description: 'Unique slug of workspace to find',\n          required: true,\n          type: 'string'\n      }\n      #swagger.requestBody = {\n        description: 'JSON object with the document path and pin status to update.',\n        required: true,\n        content: {\n          \"application/json\": {\n            example: {\n              docPath: \"custom-documents/my-pdf.pdf-hash.json\",\n              pinStatus: true\n            }\n          }\n        }\n      }\n      #swagger.responses[200] = {\n        description: 'OK',\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                message: 'Pin status updated successfully'\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[404] = {\n        description: 'Document not found'\n      }\n      #swagger.responses[500] = {\n        description: 'Internal Server Error'\n      }\n      */\n      try {\n        const { slug = null } = request.params;\n        const { docPath, pinStatus = false } = reqBody(request);\n        const workspace = await Workspace.get({ slug });\n\n        const document = await Document.get({\n          workspaceId: workspace.id,\n          docpath: docPath,\n        });\n        if (!document) return response.sendStatus(404).end();\n\n        await Document.update(document.id, { pinned: pinStatus });\n        return response\n          .status(200)\n          .json({ message: \"Pin status updated successfully\" })\n          .end();\n      } catch (error) {\n        console.error(\"Error processing the pin status update:\", error);\n        return response.status(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/workspace/:slug/chat\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n   #swagger.tags = ['Workspaces']\n   #swagger.description = 'Execute a chat with a workspace'\n   #swagger.requestBody = {\n       description: 'Send a prompt to the workspace and the type of conversation (automatic, query or chat).<br/><b>Query:</b> Will not use LLM unless there are relevant sources from vectorDB & does not recall chat history.<br/><b>Automatic:</b> Will use tool-calling if the provider supports native tool calling without needing to invoke @agent.<br/><b>Chat:</b> Uses LLM general knowledge w/custom embeddings to produce output, uses rolling chat history.<br/><b>Attachments:</b> Can include images and documents.<br/><b>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Document attachments:</b> must have the mime type <code>application/anythingllm-document</code> - otherwise it will be passed to the LLM as an image and may fail to process. This uses the built-in document processor to first parse the document to text before injecting it into the context window.',\n       required: true,\n       content: {\n         \"application/json\": {\n           example: {\n             message: \"What is AnythingLLM?\",\n             mode:\"automatic | query | chat\",\n             sessionId: \"identifier-to-partition-chats-by-external-id\",\n             attachments: [\n               {\n                 name: \"image.png\",\n                 mime: \"image/png\",\n                 contentString: \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n               },\n               {\n                 name: \"this is a document.pdf\",\n                 mime: \"application/anythingllm-document\",\n                 contentString: \"data:application/pdf;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n               }\n             ],\n             reset: false\n           }\n         }\n       }\n     }\n   #swagger.responses[200] = {\n     content: {\n       \"application/json\": {\n         schema: {\n           type: 'object',\n           example: {\n              id: 'chat-uuid',\n              type: \"abort | textResponse\",\n              textResponse: \"Response to your query\",\n              sources: [{title: \"anythingllm.txt\", chunk: \"This is a context chunk used in the answer of the prompt by the LLM,\"}],\n              close: true,\n              error: \"null | text string of the failure mode.\"\n           }\n         }\n       }\n     }\n   }\n   #swagger.responses[403] = {\n     schema: {\n       \"$ref\": \"#/definitions/InvalidAPIKey\"\n     }\n   }\n   */\n      try {\n        const { slug } = request.params;\n        const {\n          message,\n          mode = \"query\",\n          sessionId = null,\n          attachments = [],\n          reset = false,\n        } = reqBody(request);\n        const workspace = await Workspace.get({ slug: String(slug) });\n\n        if (!workspace) {\n          response.status(400).json({\n            id: uuidv4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: `Workspace ${slug} is not a valid workspace.`,\n          });\n          return;\n        }\n\n        if ((!message?.length || !VALID_CHAT_MODE.includes(mode)) && !reset) {\n          response.status(400).json({\n            id: uuidv4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: !message?.length\n              ? \"Message is empty\"\n              : `${mode} is not a valid mode.`,\n          });\n          return;\n        }\n\n        const result = await ApiChatHandler.chatSync({\n          workspace,\n          message,\n          mode,\n          user: null,\n          thread: null,\n          sessionId: !!sessionId ? String(sessionId) : null,\n          attachments,\n          reset,\n        });\n\n        await Telemetry.sendTelemetry(\"sent_chat\", {\n          LLMSelection:\n            workspace.chatProvider ?? process.env.LLM_PROVIDER ?? \"openai\",\n          Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n          VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n          TTSSelection: process.env.TTS_PROVIDER || \"native\",\n        });\n        await EventLogs.logEvent(\"api_sent_chat\", {\n          workspaceName: workspace?.name,\n          chatModel: workspace?.chatModel || \"System Default\",\n        });\n        return response.status(200).json({ ...result });\n      } catch (e) {\n        console.error(e.message, e);\n        response.status(500).json({\n          id: uuidv4(),\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: e.message,\n        });\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/workspace/:slug/stream-chat\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n   #swagger.tags = ['Workspaces']\n   #swagger.description = 'Execute a streamable chat with a workspace'\n   #swagger.requestBody = {\n       description: 'Send a prompt to the workspace and the type of conversation (automatic, query or chat).<br/><b>Query:</b> Will not use LLM unless there are relevant sources from vectorDB & does not recall chat history.<br/><b>Automatic:</b> Will use tool-calling if the provider supports native tool calling without needing to invoke @agent.<br/><b>Chat:</b> Uses LLM general knowledge w/custom embeddings to produce output, uses rolling chat history.<br/><b>Attachments:</b> Can include images and documents.<br/><b>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Document attachments:</b> must have the mime type <code>application/anythingllm-document</code> - otherwise it will be passed to the LLM as an image and may fail to process. This uses the built-in document processor to first parse the document to text before injecting it into the context window.',\n       required: true,\n       content: {\n         \"application/json\": {\n           example: {\n             message: \"What is AnythingLLM?\",\n             mode: \"automatic | query | chat\",\n             sessionId: \"identifier-to-partition-chats-by-external-id\",\n             attachments: [\n               {\n                 name: \"image.png\",\n                 mime: \"image/png\",\n                 contentString: \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n               },\n               {\n                 name: \"this is a document.pdf\",\n                 mime: \"application/anythingllm-document\",\n                 contentString: \"data:application/pdf;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n               }\n             ],\n             reset: false\n           }\n         }\n       }\n     }\n   #swagger.responses[200] = {\n     content: {\n       \"text/event-stream\": {\n         schema: {\n           type: 'array',\n           items: {\n              type: 'string',\n          },\n           example: [\n            {\n              id: 'uuid-123',\n              type: \"abort | textResponseChunk\",\n              textResponse: \"First chunk\",\n              sources: [],\n              close: false,\n              error: \"null | text string of the failure mode.\"\n            },\n            {\n              id: 'uuid-123',\n              type: \"abort | textResponseChunk\",\n              textResponse: \"chunk two\",\n              sources: [],\n              close: false,\n              error: \"null | text string of the failure mode.\"\n            },\n             {\n              id: 'uuid-123',\n              type: \"abort | textResponseChunk\",\n              textResponse: \"final chunk of LLM output!\",\n              sources: [{title: \"anythingllm.txt\", chunk: \"This is a context chunk used in the answer of the prompt by the LLM. This will only return in the final chunk.\"}],\n              close: true,\n              error: \"null | text string of the failure mode.\"\n            }\n          ]\n         }\n       }\n     }\n   }\n   #swagger.responses[403] = {\n     schema: {\n       \"$ref\": \"#/definitions/InvalidAPIKey\"\n     }\n   }\n   */\n      try {\n        const { slug } = request.params;\n        const {\n          message,\n          mode = \"query\",\n          sessionId = null,\n          attachments = [],\n          reset = false,\n        } = reqBody(request);\n        const workspace = await Workspace.get({ slug: String(slug) });\n\n        if (!workspace) {\n          response.status(400).json({\n            id: uuidv4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: `Workspace ${slug} is not a valid workspace.`,\n          });\n          return;\n        }\n\n        if ((!message?.length || !VALID_CHAT_MODE.includes(mode)) && !reset) {\n          response.status(400).json({\n            id: uuidv4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: !message?.length\n              ? \"Message is empty\"\n              : `${mode} is not a valid mode.`,\n          });\n          return;\n        }\n\n        response.setHeader(\"Cache-Control\", \"no-cache\");\n        response.setHeader(\"Content-Type\", \"text/event-stream\");\n        response.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n        response.setHeader(\"Connection\", \"keep-alive\");\n        response.flushHeaders();\n\n        await ApiChatHandler.streamChat({\n          response,\n          workspace,\n          message,\n          mode,\n          user: null,\n          thread: null,\n          sessionId: !!sessionId ? String(sessionId) : null,\n          attachments,\n          reset,\n        });\n        await Telemetry.sendTelemetry(\"sent_chat\", {\n          LLMSelection:\n            workspace.chatProvider ?? process.env.LLM_PROVIDER ?? \"openai\",\n          Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n          VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n          TTSSelection: process.env.TTS_PROVIDER || \"native\",\n        });\n        await EventLogs.logEvent(\"api_sent_chat\", {\n          workspaceName: workspace?.name,\n          chatModel: workspace?.chatModel || \"System Default\",\n        });\n        response.end();\n      } catch (e) {\n        console.error(e.message, e);\n        writeResponseChunk(response, {\n          id: uuidv4(),\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: e.message,\n        });\n        response.end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/workspace/:slug/vector-search\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Workspaces']\n    #swagger.description = 'Perform a vector similarity search in a workspace'\n    #swagger.parameters['slug'] = {\n        in: 'path',\n        description: 'Unique slug of workspace to search in',\n        required: true,\n        type: 'string'\n    }\n    #swagger.requestBody = {\n      description: 'Query to perform vector search with and optional parameters',\n      required: true,\n      content: {\n        \"application/json\": {\n          example: {\n            query: \"What is the meaning of life?\",\n            topN: 4,\n            scoreThreshold: 0.75\n          }\n        }\n      }\n    }\n    #swagger.responses[200] = {\n      content: {\n        \"application/json\": {\n          schema: {\n            type: 'object',\n            example: {\n              results: [\n                {\n                  id: \"5a6bee0a-306c-47fc-942b-8ab9bf3899c4\",\n                  text: \"Document chunk content...\",\n                  metadata: {\n                    url: \"file://document.txt\",\n                    title: \"document.txt\",\n                    author: \"no author specified\",\n                    description: \"no description found\",\n                    docSource: \"post:123456\",\n                    chunkSource: \"document.txt\",\n                    published: \"12/1/2024, 11:39:39 AM\",\n                    wordCount: 8,\n                    tokenCount: 9\n                  },\n                  distance: 0.541887640953064,\n                  score: 0.45811235904693604\n                }\n              ]\n            }\n          }\n        }\n      }\n    }\n    */\n      try {\n        const { slug } = request.params;\n        const { query, topN, scoreThreshold } = reqBody(request);\n        const workspace = await Workspace.get({ slug: String(slug) });\n\n        if (!workspace)\n          return response.status(400).json({\n            message: `Workspace ${slug} is not a valid workspace.`,\n          });\n\n        if (!query?.length)\n          return response.status(400).json({\n            message: \"Query parameter cannot be empty.\",\n          });\n\n        const VectorDb = getVectorDbClass();\n        const hasVectorizedSpace = await VectorDb.hasNamespace(workspace.slug);\n        const embeddingsCount = await VectorDb.namespaceCount(workspace.slug);\n\n        if (!hasVectorizedSpace || embeddingsCount === 0)\n          return response.status(200).json({\n            results: [],\n            message: \"No embeddings found for this workspace.\",\n          });\n\n        const parseSimilarityThreshold = () => {\n          let input = parseFloat(scoreThreshold);\n          if (isNaN(input) || input < 0 || input > 1)\n            return workspace?.similarityThreshold ?? 0.25;\n          return input;\n        };\n\n        const parseTopN = () => {\n          let input = Number(topN);\n          if (isNaN(input) || input < 1) return workspace?.topN ?? 4;\n          return input;\n        };\n\n        const results = await VectorDb.performSimilaritySearch({\n          namespace: workspace.slug,\n          input: String(query),\n          LLMConnector: getLLMProvider(),\n          similarityThreshold: parseSimilarityThreshold(),\n          topN: parseTopN(),\n          rerank: workspace?.vectorSearchMode === \"rerank\",\n        });\n\n        response.status(200).json({\n          results: results.sources.map((source) => ({\n            id: source.id,\n            text: source.text,\n            metadata: {\n              url: source.url,\n              title: source.title,\n              author: source.docAuthor,\n              description: source.description,\n              docSource: source.docSource,\n              chunkSource: source.chunkSource,\n              published: source.published,\n              wordCount: source.wordCount,\n              tokenCount: source.token_count_estimate,\n            },\n            distance: source._distance,\n            score: source.score,\n          })),\n        });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { apiWorkspaceEndpoints };\n"
  },
  {
    "path": "server/endpoints/api/workspaceThread/index.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst { WorkspaceThread } = require(\"../../../models/workspaceThread\");\nconst { Workspace } = require(\"../../../models/workspace\");\nconst { validApiKey } = require(\"../../../utils/middleware/validApiKey\");\nconst { reqBody, multiUserMode } = require(\"../../../utils/http\");\nconst { VALID_CHAT_MODE } = require(\"../../../utils/chats/stream\");\nconst { Telemetry } = require(\"../../../models/telemetry\");\nconst { EventLogs } = require(\"../../../models/eventLogs\");\nconst {\n  writeResponseChunk,\n  convertToChatHistory,\n} = require(\"../../../utils/helpers/chat/responses\");\nconst { WorkspaceChats } = require(\"../../../models/workspaceChats\");\nconst { User } = require(\"../../../models/user\");\nconst { ApiChatHandler } = require(\"../../../utils/chats/apiChatHandler\");\nconst { getModelTag } = require(\"../../utils\");\n\nfunction apiWorkspaceThreadEndpoints(app) {\n  if (!app) return;\n\n  app.post(\n    \"/v1/workspace/:slug/thread/new\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Workspace Threads']\n      #swagger.description = 'Create a new workspace thread'\n      #swagger.parameters['slug'] = {\n          in: 'path',\n          description: 'Unique slug of workspace',\n          required: true,\n          type: 'string'\n      }\n      #swagger.requestBody = {\n        description: 'Optional userId associated with the thread, thread slug and thread name',\n        required: false,\n        content: {\n          \"application/json\": {\n            example: {\n              userId: 1,\n              name: 'Name',\n              slug: 'thread-slug'\n            }\n          }\n        }\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                thread: {\n                  \"id\": 1,\n                  \"name\": \"Thread\",\n                  \"slug\": \"thread-uuid\",\n                  \"user_id\": 1,\n                  \"workspace_id\": 1\n                },\n                message: null\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      */\n      try {\n        const wslug = request.params.slug;\n        let { userId = null, name = null, slug = null } = reqBody(request);\n        const workspace = await Workspace.get({ slug: wslug });\n\n        if (!workspace) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        // If the system is not multi-user and you pass in a userId\n        // it needs to be nullified as no users exist. This can still fail validation\n        // as we don't check if the userID is valid.\n        if (!response.locals.multiUserMode && !!userId) userId = null;\n\n        const { thread, message } = await WorkspaceThread.new(\n          workspace,\n          userId ? Number(userId) : null,\n          { name, slug }\n        );\n\n        await Telemetry.sendTelemetry(\"workspace_thread_created\", {\n          multiUserMode: multiUserMode(response),\n          LLMSelection: process.env.LLM_PROVIDER || \"openai\",\n          Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n          VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n          TTSSelection: process.env.TTS_PROVIDER || \"native\",\n        });\n        await EventLogs.logEvent(\"api_workspace_thread_created\", {\n          workspaceName: workspace?.name || \"Unknown Workspace\",\n        });\n        response.status(200).json({ thread, message });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/workspace/:slug/thread/:threadSlug/update\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Workspace Threads']\n      #swagger.description = 'Update thread name by its unique slug.'\n      #swagger.parameters['slug'] = {\n          in: 'path',\n          description: 'Unique slug of workspace',\n          required: true,\n          type: 'string'\n      }\n      #swagger.parameters['threadSlug'] = {\n          in: 'path',\n          description: 'Unique slug of thread',\n          required: true,\n          type: 'string'\n      }\n      #swagger.requestBody = {\n        description: 'JSON object containing new name to update the thread.',\n        required: true,\n        content: {\n          \"application/json\": {\n            example: {\n              \"name\": 'Updated Thread Name'\n            }\n          }\n        }\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                thread: {\n                  \"id\": 1,\n                  \"name\": \"Updated Thread Name\",\n                  \"slug\": \"thread-uuid\",\n                  \"user_id\": 1,\n                  \"workspace_id\": 1\n                },\n                message: null,\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      */\n      try {\n        const { slug, threadSlug } = request.params;\n        const { name } = reqBody(request);\n        const workspace = await Workspace.get({ slug });\n        const thread = await WorkspaceThread.get({\n          slug: threadSlug,\n          workspace_id: workspace.id,\n        });\n\n        if (!workspace || !thread) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        const { thread: updatedThread, message } = await WorkspaceThread.update(\n          thread,\n          { name }\n        );\n        response.status(200).json({ thread: updatedThread, message });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/v1/workspace/:slug/thread/:threadSlug\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n    #swagger.tags = ['Workspace Threads']\n    #swagger.description = 'Delete a workspace thread'\n    #swagger.parameters['slug'] = {\n        in: 'path',\n        description: 'Unique slug of workspace',\n        required: true,\n        type: 'string'\n    }\n    #swagger.parameters['threadSlug'] = {\n        in: 'path',\n        description: 'Unique slug of thread',\n        required: true,\n        type: 'string'\n    }\n    #swagger.responses[200] = {\n      description: 'Thread deleted successfully'\n    }\n    #swagger.responses[403] = {\n      schema: {\n        \"$ref\": \"#/definitions/InvalidAPIKey\"\n      }\n    }\n    */\n      try {\n        const { slug, threadSlug } = request.params;\n        const workspace = await Workspace.get({ slug });\n\n        if (!workspace) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        await WorkspaceThread.delete({\n          slug: threadSlug,\n          workspace_id: workspace.id,\n        });\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/v1/workspace/:slug/thread/:threadSlug/chats\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Workspace Threads']\n      #swagger.description = 'Get chats for a workspace thread'\n      #swagger.parameters['slug'] = {\n          in: 'path',\n          description: 'Unique slug of workspace',\n          required: true,\n          type: 'string'\n      }\n      #swagger.parameters['threadSlug'] = {\n          in: 'path',\n          description: 'Unique slug of thread',\n          required: true,\n          type: 'string'\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                history: [\n                  {\n                    \"role\": \"user\",\n                    \"content\": \"What is AnythingLLM?\",\n                    \"sentAt\": 1692851630\n                  },\n                  {\n                    \"role\": \"assistant\",\n                    \"content\": \"AnythingLLM is a platform that allows you to convert notes, PDFs, and other source materials into a chatbot. It ensures privacy, cites its answers, and allows multiple people to interact with the same documents simultaneously. It is particularly useful for businesses to enhance the visibility and readability of various written communications such as SOPs, contracts, and sales calls. You can try it out with a free trial to see if it meets your business needs.\",\n                    \"sources\": [{\"source\": \"object about source document and snippets used\"}]\n                  }\n                ]\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      */\n      try {\n        const { slug, threadSlug } = request.params;\n        const workspace = await Workspace.get({ slug });\n        const thread = await WorkspaceThread.get({\n          slug: threadSlug,\n          workspace_id: workspace.id,\n        });\n\n        if (!workspace || !thread) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        const history = await WorkspaceChats.where(\n          {\n            workspaceId: workspace.id,\n            thread_id: thread.id,\n            api_session_id: null, // Do not include API session chats.\n            include: true,\n          },\n          null,\n          { id: \"asc\" }\n        );\n\n        response.status(200).json({ history: convertToChatHistory(history) });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/workspace/:slug/thread/:threadSlug/chat\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Workspace Threads']\n      #swagger.description = 'Chat with a workspace thread'\n      #swagger.parameters['slug'] = {\n          in: 'path',\n          description: 'Unique slug of workspace',\n          required: true,\n          type: 'string'\n      }\n      #swagger.parameters['threadSlug'] = {\n          in: 'path',\n          description: 'Unique slug of thread',\n          required: true,\n          type: 'string'\n      }\n      #swagger.requestBody = {\n        description: 'Send a prompt to the workspace thread and the type of conversation (query or chat).',\n        required: true,\n        content: {\n          \"application/json\": {\n            example: {\n              message: \"What is AnythingLLM?\",\n              mode: \"query | chat\",\n              userId: 1,\n              attachments: [\n               {\n                 name: \"image.png\",\n                 mime: \"image/png\",\n                 contentString: \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n               }\n              ],\n              reset: false\n            }\n          }\n        }\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"application/json\": {\n            schema: {\n              type: 'object',\n              example: {\n                id: 'chat-uuid',\n                type: \"abort | textResponse\",\n                textResponse: \"Response to your query\",\n                sources: [{title: \"anythingllm.txt\", chunk: \"This is a context chunk used in the answer of the prompt by the LLM.\"}],\n                close: true,\n                error: \"null | text string of the failure mode.\"\n              }\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      */\n      try {\n        const { slug, threadSlug } = request.params;\n        const {\n          message,\n          mode = \"query\",\n          userId,\n          attachments = [],\n          reset = false,\n        } = reqBody(request);\n        const workspace = await Workspace.get({ slug });\n        const thread = await WorkspaceThread.get({\n          slug: threadSlug,\n          workspace_id: workspace.id,\n        });\n\n        if (!workspace || !thread) {\n          response.status(400).json({\n            id: uuidv4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: `Workspace ${slug} or thread ${threadSlug} is not valid.`,\n          });\n          return;\n        }\n\n        if ((!message?.length || !VALID_CHAT_MODE.includes(mode)) && !reset) {\n          response.status(400).json({\n            id: uuidv4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: !message?.length\n              ? \"Message is empty\"\n              : `${mode} is not a valid mode.`,\n          });\n          return;\n        }\n\n        const user = userId ? await User.get({ id: Number(userId) }) : null;\n        const result = await ApiChatHandler.chatSync({\n          workspace,\n          message,\n          mode,\n          user,\n          thread,\n          attachments,\n          reset,\n        });\n        await Telemetry.sendTelemetry(\"sent_chat\", {\n          LLMSelection: process.env.LLM_PROVIDER || \"openai\",\n          Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n          VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n          TTSSelection: process.env.TTS_PROVIDER || \"native\",\n          LLMModel: getModelTag(),\n        });\n        await EventLogs.logEvent(\"api_sent_chat\", {\n          workspaceName: workspace?.name,\n          chatModel: workspace?.chatModel || \"System Default\",\n          threadName: thread?.name,\n          userId: user?.id,\n        });\n        response.status(200).json({ ...result });\n      } catch (e) {\n        console.error(e.message, e);\n        response.status(500).json({\n          id: uuidv4(),\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: e.message,\n        });\n      }\n    }\n  );\n\n  app.post(\n    \"/v1/workspace/:slug/thread/:threadSlug/stream-chat\",\n    [validApiKey],\n    async (request, response) => {\n      /*\n      #swagger.tags = ['Workspace Threads']\n      #swagger.description = 'Stream chat with a workspace thread'\n      #swagger.parameters['slug'] = {\n          in: 'path',\n          description: 'Unique slug of workspace',\n          required: true,\n          type: 'string'\n      }\n      #swagger.parameters['threadSlug'] = {\n          in: 'path',\n          description: 'Unique slug of thread',\n          required: true,\n          type: 'string'\n      }\n      #swagger.requestBody = {\n        description: 'Send a prompt to the workspace thread and the type of conversation (query or chat).',\n        required: true,\n        content: {\n          \"application/json\": {\n            example: {\n              message: \"What is AnythingLLM?\",\n              mode: \"query | chat\",\n              userId: 1,\n              attachments: [\n               {\n                 name: \"image.png\",\n                 mime: \"image/png\",\n                 contentString: \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n               },\n               {\n                 name: \"this is a document.pdf\",\n                 mime: \"application/anythingllm-document\",\n                 contentString: \"data:application/pdf;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n               }\n              ],\n              reset: false\n            }\n          }\n        }\n      }\n      #swagger.responses[200] = {\n        content: {\n          \"text/event-stream\": {\n            schema: {\n              type: 'array',\n              items: {\n                  type: 'string',\n              },\n              example: [\n                {\n                  id: 'uuid-123',\n                  type: \"abort | textResponseChunk\",\n                  textResponse: \"First chunk\",\n                  sources: [],\n                  close: false,\n                  error: \"null | text string of the failure mode.\"\n                },\n                {\n                  id: 'uuid-123',\n                  type: \"abort | textResponseChunk\",\n                  textResponse: \"chunk two\",\n                  sources: [],\n                  close: false,\n                  error: \"null | text string of the failure mode.\"\n                },\n                {\n                  id: 'uuid-123',\n                  type: \"abort | textResponseChunk\",\n                  textResponse: \"final chunk of LLM output!\",\n                  sources: [{title: \"anythingllm.txt\", chunk: \"This is a context chunk used in the answer of the prompt by the LLM. This will only return in the final chunk.\"}],\n                  close: true,\n                  error: \"null | text string of the failure mode.\"\n                }\n              ]\n            }\n          }\n        }\n      }\n      #swagger.responses[403] = {\n        schema: {\n          \"$ref\": \"#/definitions/InvalidAPIKey\"\n        }\n      }\n      */\n      try {\n        const { slug, threadSlug } = request.params;\n        const {\n          message,\n          mode = \"query\",\n          userId,\n          attachments = [],\n          reset = false,\n        } = reqBody(request);\n        const workspace = await Workspace.get({ slug });\n        const thread = await WorkspaceThread.get({\n          slug: threadSlug,\n          workspace_id: workspace.id,\n        });\n\n        if (!workspace || !thread) {\n          response.status(400).json({\n            id: uuidv4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: `Workspace ${slug} or thread ${threadSlug} is not valid.`,\n          });\n          return;\n        }\n\n        if ((!message?.length || !VALID_CHAT_MODE.includes(mode)) && !reset) {\n          response.status(400).json({\n            id: uuidv4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: !message?.length\n              ? \"Message is empty\"\n              : `${mode} is not a valid mode.`,\n          });\n          return;\n        }\n\n        const user = userId ? await User.get({ id: Number(userId) }) : null;\n\n        response.setHeader(\"Cache-Control\", \"no-cache\");\n        response.setHeader(\"Content-Type\", \"text/event-stream\");\n        response.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n        response.setHeader(\"Connection\", \"keep-alive\");\n        response.flushHeaders();\n\n        await ApiChatHandler.streamChat({\n          response,\n          workspace,\n          message,\n          mode,\n          user,\n          thread,\n          attachments,\n          reset,\n        });\n        await Telemetry.sendTelemetry(\"sent_chat\", {\n          LLMSelection: process.env.LLM_PROVIDER || \"openai\",\n          Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n          VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n          TTSSelection: process.env.TTS_PROVIDER || \"native\",\n          LLMModel: getModelTag(),\n        });\n        await EventLogs.logEvent(\"api_sent_chat\", {\n          workspaceName: workspace?.name,\n          chatModel: workspace?.chatModel || \"System Default\",\n          threadName: thread?.name,\n          userId: user?.id,\n        });\n        response.end();\n      } catch (e) {\n        console.error(e.message, e);\n        writeResponseChunk(response, {\n          id: uuidv4(),\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: e.message,\n        });\n        response.end();\n      }\n    }\n  );\n}\n\nmodule.exports = { apiWorkspaceThreadEndpoints };\n"
  },
  {
    "path": "server/endpoints/browserExtension.js",
    "content": "const { Workspace } = require(\"../models/workspace\");\nconst { BrowserExtensionApiKey } = require(\"../models/browserExtensionApiKey\");\nconst { Document } = require(\"../models/documents\");\nconst {\n  validBrowserExtensionApiKey,\n} = require(\"../utils/middleware/validBrowserExtensionApiKey\");\nconst { CollectorApi } = require(\"../utils/collectorApi\");\nconst { reqBody, multiUserMode, userFromSession } = require(\"../utils/http\");\nconst { validatedRequest } = require(\"../utils/middleware/validatedRequest\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../utils/middleware/multiUserProtected\");\nconst { Telemetry } = require(\"../models/telemetry\");\n\nfunction browserExtensionEndpoints(app) {\n  if (!app) return;\n\n  app.get(\n    \"/browser-extension/check\",\n    [validBrowserExtensionApiKey],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const workspaces = multiUserMode(response)\n          ? await Workspace.whereWithUser(user)\n          : await Workspace.where();\n\n        const apiKeyId = response.locals.apiKey.id;\n        response.status(200).json({\n          connected: true,\n          workspaces,\n          apiKeyId,\n        });\n      } catch (error) {\n        console.error(error);\n        response\n          .status(500)\n          .json({ connected: false, error: \"Failed to fetch workspaces\" });\n      }\n    }\n  );\n\n  app.delete(\n    \"/browser-extension/disconnect\",\n    [validBrowserExtensionApiKey],\n    async (_request, response) => {\n      try {\n        const apiKeyId = response.locals.apiKey.id;\n        const { success, error } =\n          await BrowserExtensionApiKey.delete(apiKeyId);\n        if (!success) throw new Error(error);\n        response.status(200).json({ success: true });\n      } catch (error) {\n        console.error(error);\n        response\n          .status(500)\n          .json({ error: \"Failed to disconnect and revoke API key\" });\n      }\n    }\n  );\n\n  app.get(\n    \"/browser-extension/workspaces\",\n    [validBrowserExtensionApiKey],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const workspaces = multiUserMode(response)\n          ? await Workspace.whereWithUser(user)\n          : await Workspace.where();\n\n        response.status(200).json({ workspaces });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({ error: \"Failed to fetch workspaces\" });\n      }\n    }\n  );\n\n  app.post(\n    \"/browser-extension/embed-content\",\n    [validBrowserExtensionApiKey],\n    async (request, response) => {\n      try {\n        const { workspaceId, textContent, metadata } = reqBody(request);\n        const user = await userFromSession(request, response);\n        const workspace = multiUserMode(response)\n          ? await Workspace.getWithUser(user, { id: parseInt(workspaceId) })\n          : await Workspace.get({ id: parseInt(workspaceId) });\n\n        if (!workspace) {\n          response.status(404).json({ error: \"Workspace not found\" });\n          return;\n        }\n\n        const Collector = new CollectorApi();\n        const { success, reason, documents } = await Collector.processRawText(\n          textContent,\n          metadata\n        );\n\n        if (!success) {\n          response.status(500).json({ success: false, error: reason });\n          return;\n        }\n\n        const { failedToEmbed = [], errors = [] } = await Document.addDocuments(\n          workspace,\n          [documents[0].location],\n          user?.id\n        );\n\n        if (failedToEmbed.length > 0) {\n          response.status(500).json({ success: false, error: errors[0] });\n          return;\n        }\n\n        await Telemetry.sendTelemetry(\"browser_extension_embed_content\");\n        response.status(200).json({ success: true });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({ error: \"Failed to embed content\" });\n      }\n    }\n  );\n\n  app.post(\n    \"/browser-extension/upload-content\",\n    [validBrowserExtensionApiKey],\n    async (request, response) => {\n      try {\n        const { textContent, metadata } = reqBody(request);\n        const Collector = new CollectorApi();\n        const { success, reason } = await Collector.processRawText(\n          textContent,\n          metadata\n        );\n\n        if (!success) {\n          response.status(500).json({ success: false, error: reason });\n          return;\n        }\n\n        await Telemetry.sendTelemetry(\"browser_extension_upload_content\");\n        response.status(200).json({ success: true });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({ error: \"Failed to embed content\" });\n      }\n    }\n  );\n\n  // Internal endpoints for managing API keys\n  app.get(\n    \"/browser-extension/api-keys\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const apiKeys = multiUserMode(response)\n          ? await BrowserExtensionApiKey.whereWithUser(user)\n          : await BrowserExtensionApiKey.where();\n\n        response.status(200).json({ success: true, apiKeys });\n      } catch (error) {\n        console.error(error);\n        response\n          .status(500)\n          .json({ success: false, error: \"Failed to fetch API keys\" });\n      }\n    }\n  );\n\n  app.post(\n    \"/browser-extension/api-keys/new\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const { apiKey, error } = await BrowserExtensionApiKey.create(\n          user?.id || null\n        );\n        if (error) throw new Error(error);\n        response.status(200).json({\n          apiKey: apiKey.key,\n        });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({ error: \"Failed to create API key\" });\n      }\n    }\n  );\n\n  app.delete(\n    \"/browser-extension/api-keys/:id\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { id } = request.params;\n        const user = await userFromSession(request, response);\n\n        if (multiUserMode(response) && user.role !== ROLES.admin) {\n          const apiKey = await BrowserExtensionApiKey.get({\n            id: parseInt(id),\n            user_id: user?.id,\n          });\n          if (!apiKey) {\n            return response.status(403).json({ error: \"Unauthorized\" });\n          }\n        }\n\n        const { success, error } = await BrowserExtensionApiKey.delete(id);\n        if (!success) throw new Error(error);\n        response.status(200).json({ success: true });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({ error: \"Failed to revoke API key\" });\n      }\n    }\n  );\n}\n\nmodule.exports = { browserExtensionEndpoints };\n"
  },
  {
    "path": "server/endpoints/chat.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst { reqBody, userFromSession, multiUserMode } = require(\"../utils/http\");\nconst { validatedRequest } = require(\"../utils/middleware/validatedRequest\");\nconst { Telemetry } = require(\"../models/telemetry\");\nconst { streamChatWithWorkspace } = require(\"../utils/chats/stream\");\nconst {\n  ROLES,\n  flexUserRoleValid,\n} = require(\"../utils/middleware/multiUserProtected\");\nconst { EventLogs } = require(\"../models/eventLogs\");\nconst {\n  validWorkspaceAndThreadSlug,\n  validWorkspaceSlug,\n} = require(\"../utils/middleware/validWorkspace\");\nconst { writeResponseChunk } = require(\"../utils/helpers/chat/responses\");\nconst { WorkspaceThread } = require(\"../models/workspaceThread\");\nconst { User } = require(\"../models/user\");\nconst truncate = require(\"truncate\");\nconst { getModelTag } = require(\"./utils\");\n\nfunction chatEndpoints(app) {\n  if (!app) return;\n\n  app.post(\n    \"/workspace/:slug/stream-chat\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const { message, attachments = [] } = reqBody(request);\n        const workspace = response.locals.workspace;\n\n        if (typeof message !== \"string\" || message.trim().length === 0) {\n          response.status(400).json({\n            id: uuidv4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: \"Message is empty.\",\n          });\n          return;\n        }\n\n        response.setHeader(\"Cache-Control\", \"no-cache\");\n        response.setHeader(\"Content-Type\", \"text/event-stream\");\n        response.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n        response.setHeader(\"Connection\", \"keep-alive\");\n        response.flushHeaders();\n\n        if (multiUserMode(response) && !(await User.canSendChat(user))) {\n          writeResponseChunk(response, {\n            id: uuidv4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: `You have met your maximum 24 hour chat quota of ${user.dailyMessageLimit} chats. Try again later.`,\n          });\n          return;\n        }\n\n        await streamChatWithWorkspace(\n          response,\n          workspace,\n          message,\n          workspace?.chatMode,\n          user,\n          null,\n          attachments\n        );\n        await Telemetry.sendTelemetry(\"sent_chat\", {\n          multiUserMode: multiUserMode(response),\n          LLMSelection: process.env.LLM_PROVIDER || \"openai\",\n          Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n          VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n          multiModal: Array.isArray(attachments) && attachments?.length !== 0,\n          TTSSelection: process.env.TTS_PROVIDER || \"native\",\n          LLMModel: getModelTag(),\n        });\n\n        await EventLogs.logEvent(\n          \"sent_chat\",\n          {\n            workspaceName: workspace?.name,\n            chatModel: workspace?.chatModel || \"System Default\",\n          },\n          user?.id\n        );\n        response.end();\n      } catch (e) {\n        console.error(e);\n        writeResponseChunk(response, {\n          id: uuidv4(),\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: e.message,\n        });\n        response.end();\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/thread/:threadSlug/stream-chat\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.all]),\n      validWorkspaceAndThreadSlug,\n    ],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const { message, attachments = [] } = reqBody(request);\n        const workspace = response.locals.workspace;\n        const thread = response.locals.thread;\n\n        if (typeof message !== \"string\" || message.trim().length === 0) {\n          response.status(400).json({\n            id: uuidv4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: \"Message is empty.\",\n          });\n          return;\n        }\n\n        response.setHeader(\"Cache-Control\", \"no-cache\");\n        response.setHeader(\"Content-Type\", \"text/event-stream\");\n        response.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n        response.setHeader(\"Connection\", \"keep-alive\");\n        response.flushHeaders();\n\n        if (multiUserMode(response) && !(await User.canSendChat(user))) {\n          writeResponseChunk(response, {\n            id: uuidv4(),\n            type: \"abort\",\n            textResponse: null,\n            sources: [],\n            close: true,\n            error: `You have met your maximum 24 hour chat quota of ${user.dailyMessageLimit} chats. Try again later.`,\n          });\n          return;\n        }\n\n        await streamChatWithWorkspace(\n          response,\n          workspace,\n          message,\n          workspace?.chatMode,\n          user,\n          thread,\n          attachments\n        );\n\n        // If thread was renamed emit event to frontend via special `action` response.\n        await WorkspaceThread.autoRenameThread({\n          thread,\n          workspace,\n          user,\n          newName: truncate(message, 22),\n          onRename: (thread) => {\n            writeResponseChunk(response, {\n              action: \"rename_thread\",\n              thread: {\n                slug: thread.slug,\n                name: thread.name,\n              },\n            });\n          },\n        });\n\n        await Telemetry.sendTelemetry(\"sent_chat\", {\n          multiUserMode: multiUserMode(response),\n          LLMSelection: process.env.LLM_PROVIDER || \"openai\",\n          Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n          VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n          multiModal: Array.isArray(attachments) && attachments?.length !== 0,\n          TTSSelection: process.env.TTS_PROVIDER || \"native\",\n          LLMModel: getModelTag(),\n        });\n\n        await EventLogs.logEvent(\n          \"sent_chat\",\n          {\n            workspaceName: workspace.name,\n            thread: thread.name,\n            chatModel: workspace?.chatModel || \"System Default\",\n          },\n          user?.id\n        );\n        response.end();\n      } catch (e) {\n        console.error(e);\n        writeResponseChunk(response, {\n          id: uuidv4(),\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: e.message,\n        });\n        response.end();\n      }\n    }\n  );\n}\n\nmodule.exports = { chatEndpoints };\n"
  },
  {
    "path": "server/endpoints/communityHub.js",
    "content": "const { SystemSettings } = require(\"../models/systemSettings\");\nconst { validatedRequest } = require(\"../utils/middleware/validatedRequest\");\nconst { reqBody } = require(\"../utils/http\");\nconst { CommunityHub } = require(\"../models/communityHub\");\nconst {\n  communityHubDownloadsEnabled,\n  communityHubItem,\n} = require(\"../utils/middleware/communityHubDownloadsEnabled\");\nconst { EventLogs } = require(\"../models/eventLogs\");\nconst { Telemetry } = require(\"../models/telemetry\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../utils/middleware/multiUserProtected\");\n\nfunction communityHubEndpoints(app) {\n  if (!app) return;\n\n  app.get(\n    \"/community-hub/settings\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (_, response) => {\n      try {\n        const { connectionKey } = await SystemSettings.hubSettings();\n        response.status(200).json({ success: true, connectionKey });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({ success: false, error: error.message });\n      }\n    }\n  );\n\n  app.post(\n    \"/community-hub/settings\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const data = reqBody(request);\n        const result = await SystemSettings.updateSettings(data);\n        if (result.error) throw new Error(result.error);\n        response.status(200).json({ success: true, error: null });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({ success: false, error: error.message });\n      }\n    }\n  );\n\n  app.get(\n    \"/community-hub/explore\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (_, response) => {\n      try {\n        const exploreItems = await CommunityHub.fetchExploreItems();\n        response.status(200).json({ success: true, result: exploreItems });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({\n          success: false,\n          result: null,\n          error: error.message,\n        });\n      }\n    }\n  );\n\n  app.post(\n    \"/community-hub/item\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin]), communityHubItem],\n    async (_request, response) => {\n      try {\n        response.status(200).json({\n          success: true,\n          item: response.locals.bundleItem,\n          error: null,\n        });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({\n          success: false,\n          item: null,\n          error: error.message,\n        });\n      }\n    }\n  );\n\n  /**\n   * Apply an item to the AnythingLLM instance. Used for simple items like slash commands and system prompts.\n   */\n  app.post(\n    \"/community-hub/apply\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin]), communityHubItem],\n    async (request, response) => {\n      try {\n        const { options = {} } = reqBody(request);\n        const item = response.locals.bundleItem;\n        const { error: applyError } = await CommunityHub.applyItem(item, {\n          ...options,\n          currentUser: response.locals?.user,\n        });\n        if (applyError) throw new Error(applyError);\n\n        await Telemetry.sendTelemetry(\"community_hub_import\", {\n          itemType: response.locals.bundleItem.itemType,\n          visibility: response.locals.bundleItem.visibility,\n        });\n        await EventLogs.logEvent(\n          \"community_hub_import\",\n          {\n            itemId: response.locals.bundleItem.id,\n            itemType: response.locals.bundleItem.itemType,\n          },\n          response.locals?.user?.id\n        );\n\n        response.status(200).json({ success: true, error: null });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({ success: false, error: error.message });\n      }\n    }\n  );\n\n  /**\n   * Import a bundle item to the AnythingLLM instance by downloading the zip file and importing it.\n   * or whatever the item type requires. This is not used if the item is a simple text responses like\n   * slash commands or system prompts.\n   */\n  app.post(\n    \"/community-hub/import\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin]),\n      communityHubItem,\n      communityHubDownloadsEnabled,\n    ],\n    async (_, response) => {\n      try {\n        const { error: importError } = await CommunityHub.importBundleItem({\n          url: response.locals.bundleUrl,\n          item: response.locals.bundleItem,\n        });\n        if (importError) throw new Error(importError);\n\n        await Telemetry.sendTelemetry(\"community_hub_import\", {\n          itemType: response.locals.bundleItem.itemType,\n          visibility: response.locals.bundleItem.visibility,\n        });\n        await EventLogs.logEvent(\n          \"community_hub_import\",\n          {\n            itemId: response.locals.bundleItem.id,\n            itemType: response.locals.bundleItem.itemType,\n          },\n          response.locals?.user?.id\n        );\n\n        response.status(200).json({ success: true, error: null });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({\n          success: false,\n          error: error.message,\n        });\n      }\n    }\n  );\n\n  app.get(\n    \"/community-hub/items\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (_, response) => {\n      try {\n        const { connectionKey } = await SystemSettings.hubSettings();\n        const items = await CommunityHub.fetchUserItems(connectionKey);\n        response.status(200).json({ success: true, ...items });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({ success: false, error: error.message });\n      }\n    }\n  );\n\n  app.post(\n    \"/community-hub/:communityHubItemType/create\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { communityHubItemType } = request.params;\n        const { connectionKey } = await SystemSettings.hubSettings();\n        if (!connectionKey)\n          throw new Error(\"Community Hub connection key not found\");\n\n        const data = reqBody(request);\n        const { success, error, itemId } = await CommunityHub.createStaticItem(\n          communityHubItemType,\n          data,\n          connectionKey\n        );\n        if (!success) throw new Error(error);\n\n        await EventLogs.logEvent(\n          \"community_hub_publish\",\n          { itemType: communityHubItemType },\n          response.locals?.user?.id\n        );\n        response\n          .status(200)\n          .json({ success: true, error: null, item: { id: itemId } });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({ success: false, error: error.message });\n      }\n    }\n  );\n}\n\nmodule.exports = { communityHubEndpoints };\n"
  },
  {
    "path": "server/endpoints/document.js",
    "content": "const { Document } = require(\"../models/documents\");\nconst { normalizePath, documentsPath, isWithin } = require(\"../utils/files\");\nconst { reqBody } = require(\"../utils/http\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../utils/middleware/multiUserProtected\");\nconst { validatedRequest } = require(\"../utils/middleware/validatedRequest\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\nfunction documentEndpoints(app) {\n  if (!app) return;\n  app.post(\n    \"/document/create-folder\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { name } = reqBody(request);\n        const storagePath = path.join(documentsPath, normalizePath(name));\n        if (!isWithin(path.resolve(documentsPath), path.resolve(storagePath)))\n          throw new Error(\"Invalid folder name.\");\n\n        if (fs.existsSync(storagePath)) {\n          response.status(500).json({\n            success: false,\n            message: \"Folder by that name already exists\",\n          });\n          return;\n        }\n\n        fs.mkdirSync(storagePath, { recursive: true });\n        response.status(200).json({ success: true, message: null });\n      } catch (e) {\n        console.error(e);\n        response.status(500).json({\n          success: false,\n          message: `Failed to create folder: ${e.message} `,\n        });\n      }\n    }\n  );\n\n  app.post(\n    \"/document/move-files\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { files } = reqBody(request);\n        const docpaths = files.map(({ from }) => from);\n        const documents = await Document.where({ docpath: { in: docpaths } });\n\n        const embeddedFiles = documents.map((doc) => doc.docpath);\n        const moveableFiles = files.filter(\n          ({ from }) => !embeddedFiles.includes(from)\n        );\n\n        const movePromises = moveableFiles.map(({ from, to }) => {\n          const sourcePath = path.join(documentsPath, normalizePath(from));\n          const destinationPath = path.join(documentsPath, normalizePath(to));\n\n          return new Promise((resolve, reject) => {\n            if (\n              !isWithin(documentsPath, sourcePath) ||\n              !isWithin(documentsPath, destinationPath)\n            )\n              return reject(\"Invalid file location\");\n\n            fs.rename(sourcePath, destinationPath, (err) => {\n              if (err) {\n                console.error(`Error moving file ${from} to ${to}:`, err);\n                reject(err);\n              } else {\n                resolve();\n              }\n            });\n          });\n        });\n\n        Promise.all(movePromises)\n          .then(() => {\n            const unmovableCount = files.length - moveableFiles.length;\n            if (unmovableCount > 0) {\n              response.status(200).json({\n                success: true,\n                message: `${unmovableCount}/${files.length} files not moved. Unembed them from all workspaces.`,\n              });\n            } else {\n              response.status(200).json({\n                success: true,\n                message: null,\n              });\n            }\n          })\n          .catch((err) => {\n            console.error(\"Error moving files:\", err);\n            response\n              .status(500)\n              .json({ success: false, message: \"Failed to move some files.\" });\n          });\n      } catch (e) {\n        console.error(e);\n        response\n          .status(500)\n          .json({ success: false, message: \"Failed to move files.\" });\n      }\n    }\n  );\n}\n\nmodule.exports = { documentEndpoints };\n"
  },
  {
    "path": "server/endpoints/embed/index.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst { reqBody, multiUserMode } = require(\"../../utils/http\");\nconst { Telemetry } = require(\"../../models/telemetry\");\nconst { streamChatWithForEmbed } = require(\"../../utils/chats/embed\");\nconst { EmbedChats } = require(\"../../models/embedChats\");\nconst {\n  validEmbedConfig,\n  canRespond,\n  setConnectionMeta,\n} = require(\"../../utils/middleware/embedMiddleware\");\nconst {\n  convertToChatHistory,\n  writeResponseChunk,\n} = require(\"../../utils/helpers/chat/responses\");\n\nfunction embeddedEndpoints(app) {\n  if (!app) return;\n\n  app.post(\n    \"/embed/:embedId/stream-chat\",\n    [validEmbedConfig, setConnectionMeta, canRespond],\n    async (request, response) => {\n      try {\n        const embed = response.locals.embedConfig;\n        const {\n          sessionId,\n          message,\n          // optional keys for override of defaults if enabled.\n          prompt = null,\n          model = null,\n          temperature = null,\n          username = null,\n        } = reqBody(request);\n\n        response.setHeader(\"Cache-Control\", \"no-cache\");\n        response.setHeader(\"Content-Type\", \"text/event-stream\");\n        response.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n        response.setHeader(\"Connection\", \"keep-alive\");\n        response.flushHeaders();\n\n        await streamChatWithForEmbed(response, embed, message, sessionId, {\n          promptOverride: prompt,\n          modelOverride: model,\n          temperatureOverride: temperature,\n          username,\n        });\n        await Telemetry.sendTelemetry(\"embed_sent_chat\", {\n          multiUserMode: multiUserMode(response),\n          LLMSelection: process.env.LLM_PROVIDER || \"openai\",\n          Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n          VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n        });\n        response.end();\n      } catch (e) {\n        console.error(e);\n        writeResponseChunk(response, {\n          id: uuidv4(),\n          type: \"abort\",\n          sources: [],\n          textResponse: null,\n          close: true,\n          error: e.message,\n        });\n        response.end();\n      }\n    }\n  );\n\n  app.get(\n    \"/embed/:embedId/:sessionId\",\n    [validEmbedConfig],\n    async (request, response) => {\n      try {\n        const { sessionId } = request.params;\n        const embed = response.locals.embedConfig;\n        const history = await EmbedChats.forEmbedByUser(\n          embed.id,\n          sessionId,\n          null,\n          null,\n          true\n        );\n\n        response.status(200).json({ history: convertToChatHistory(history) });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/embed/:embedId/:sessionId\",\n    [validEmbedConfig],\n    async (request, response) => {\n      try {\n        const { sessionId } = request.params;\n        const embed = response.locals.embedConfig;\n\n        await EmbedChats.markHistoryInvalid(embed.id, sessionId);\n        response.status(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { embeddedEndpoints };\n"
  },
  {
    "path": "server/endpoints/embedManagement.js",
    "content": "const { EmbedChats } = require(\"../models/embedChats\");\nconst { EmbedConfig } = require(\"../models/embedConfig\");\nconst { EventLogs } = require(\"../models/eventLogs\");\nconst { reqBody, userFromSession } = require(\"../utils/http\");\nconst { validEmbedConfigId } = require(\"../utils/middleware/embedMiddleware\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../utils/middleware/multiUserProtected\");\nconst { validatedRequest } = require(\"../utils/middleware/validatedRequest\");\nconst {\n  chatHistoryViewable,\n} = require(\"../utils/middleware/chatHistoryViewable\");\n\nfunction embedManagementEndpoints(app) {\n  if (!app) return;\n\n  app.get(\n    \"/embeds\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (_, response) => {\n      try {\n        const embeds = await EmbedConfig.whereWithWorkspace({}, null, {\n          createdAt: \"desc\",\n        });\n        response.status(200).json({ embeds });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/embeds/new\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const data = reqBody(request);\n        const { embed, message: error } = await EmbedConfig.new(data, user?.id);\n        await EventLogs.logEvent(\n          \"embed_created\",\n          { embedId: embed.id },\n          user?.id\n        );\n        response.status(200).json({ embed, error });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/embed/update/:embedId\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin]), validEmbedConfigId],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const { embedId } = request.params;\n        const updates = reqBody(request);\n        const { success, error } = await EmbedConfig.update(embedId, updates);\n        await EventLogs.logEvent(\"embed_updated\", { embedId }, user?.id);\n        response.status(200).json({ success, error });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/embed/:embedId\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin]), validEmbedConfigId],\n    async (request, response) => {\n      try {\n        const { embedId } = request.params;\n        await EmbedConfig.delete({ id: Number(embedId) });\n        await EventLogs.logEvent(\n          \"embed_deleted\",\n          { embedId },\n          response?.locals?.user?.id\n        );\n        response.status(200).json({ success: true, error: null });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/embed/chats\",\n    [chatHistoryViewable, validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { offset = 0, limit = 20 } = reqBody(request);\n        const embedChats = await EmbedChats.whereWithEmbedAndWorkspace(\n          {},\n          limit,\n          { id: \"desc\" },\n          offset * limit\n        );\n        const totalChats = await EmbedChats.count();\n        const hasPages = totalChats > (offset + 1) * limit;\n        response.status(200).json({ chats: embedChats, hasPages, totalChats });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/embed/chats/:chatId\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { chatId } = request.params;\n        await EmbedChats.delete({ id: Number(chatId) });\n        response.status(200).json({ success: true, error: null });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { embedManagementEndpoints };\n"
  },
  {
    "path": "server/endpoints/experimental/imported-agent-plugins.js",
    "content": "const ImportedPlugin = require(\"../../utils/agents/imported\");\nconst { reqBody } = require(\"../../utils/http\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../../utils/middleware/multiUserProtected\");\nconst { validatedRequest } = require(\"../../utils/middleware/validatedRequest\");\n\nfunction importedAgentPluginEndpoints(app) {\n  if (!app) return;\n\n  app.post(\n    \"/experimental/agent-plugins/:hubId/toggle\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    (request, response) => {\n      try {\n        const { hubId } = request.params;\n        const { active } = reqBody(request);\n        const updatedConfig = ImportedPlugin.updateImportedPlugin(hubId, {\n          active: Boolean(active),\n        });\n        response.status(200).json(updatedConfig);\n      } catch (e) {\n        console.error(e);\n        response.status(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/experimental/agent-plugins/:hubId/config\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    (request, response) => {\n      try {\n        const { hubId } = request.params;\n        const { updates } = reqBody(request);\n        const updatedConfig = ImportedPlugin.updateImportedPlugin(\n          hubId,\n          updates\n        );\n        response.status(200).json(updatedConfig);\n      } catch (e) {\n        console.error(e);\n        response.status(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/experimental/agent-plugins/:hubId\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { hubId } = request.params;\n        const result = ImportedPlugin.deletePlugin(hubId);\n        response.status(200).json(result);\n      } catch (e) {\n        console.error(e);\n        response.status(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { importedAgentPluginEndpoints };\n"
  },
  {
    "path": "server/endpoints/experimental/index.js",
    "content": "const { liveSyncEndpoints } = require(\"./liveSync\");\nconst { importedAgentPluginEndpoints } = require(\"./imported-agent-plugins\");\n\n// All endpoints here are not stable and can move around - have breaking changes\n// or are opt-in features that are not fully released.\n// When a feature is promoted it should be removed from here and added to the appropriate scope.\nfunction experimentalEndpoints(router) {\n  liveSyncEndpoints(router);\n  importedAgentPluginEndpoints(router);\n}\n\nmodule.exports = { experimentalEndpoints };\n"
  },
  {
    "path": "server/endpoints/experimental/liveSync.js",
    "content": "const { DocumentSyncQueue } = require(\"../../models/documentSyncQueue\");\nconst { Document } = require(\"../../models/documents\");\nconst { EventLogs } = require(\"../../models/eventLogs\");\nconst { SystemSettings } = require(\"../../models/systemSettings\");\nconst { Telemetry } = require(\"../../models/telemetry\");\nconst { reqBody } = require(\"../../utils/http\");\nconst {\n  featureFlagEnabled,\n} = require(\"../../utils/middleware/featureFlagEnabled\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../../utils/middleware/multiUserProtected\");\nconst { validWorkspaceSlug } = require(\"../../utils/middleware/validWorkspace\");\nconst { validatedRequest } = require(\"../../utils/middleware/validatedRequest\");\n\nfunction liveSyncEndpoints(app) {\n  if (!app) return;\n\n  app.post(\n    \"/experimental/toggle-live-sync\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { updatedStatus = false } = reqBody(request);\n        const newStatus =\n          SystemSettings.validations.experimental_live_file_sync(updatedStatus);\n        const currentStatus =\n          (await SystemSettings.get({ label: \"experimental_live_file_sync\" }))\n            ?.value || \"disabled\";\n        if (currentStatus === newStatus)\n          return response\n            .status(200)\n            .json({ liveSyncEnabled: newStatus === \"enabled\" });\n\n        // Already validated earlier - so can hot update.\n        await SystemSettings._updateSettings({\n          experimental_live_file_sync: newStatus,\n        });\n        if (newStatus === \"enabled\") {\n          await Telemetry.sendTelemetry(\"experimental_feature_enabled\", {\n            feature: \"live_file_sync\",\n          });\n          await EventLogs.logEvent(\"experimental_feature_enabled\", {\n            feature: \"live_file_sync\",\n          });\n          DocumentSyncQueue.bootWorkers();\n        } else {\n          DocumentSyncQueue.killWorkers();\n        }\n\n        response.status(200).json({ liveSyncEnabled: newStatus === \"enabled\" });\n      } catch (e) {\n        console.error(e);\n        response.status(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/experimental/live-sync/queues\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin]),\n      featureFlagEnabled(DocumentSyncQueue.featureKey),\n    ],\n    async (_, response) => {\n      const queues = await DocumentSyncQueue.where(\n        {},\n        null,\n        { createdAt: \"asc\" },\n        {\n          workspaceDoc: {\n            include: {\n              workspace: true,\n            },\n          },\n        }\n      );\n      response.status(200).json({ queues });\n    }\n  );\n\n  // Should be in workspace routes, but is here for now.\n  app.post(\n    \"/workspace/:slug/update-watch-status\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin, ROLES.manager]),\n      validWorkspaceSlug,\n      featureFlagEnabled(DocumentSyncQueue.featureKey),\n    ],\n    async (request, response) => {\n      try {\n        const { docPath, watchStatus = false } = reqBody(request);\n        const workspace = response.locals.workspace;\n\n        const document = await Document.get({\n          workspaceId: workspace.id,\n          docpath: docPath,\n        });\n        if (!document) return response.sendStatus(404).end();\n\n        await DocumentSyncQueue.toggleWatchStatus(document, watchStatus);\n        return response.status(200).end();\n      } catch (error) {\n        console.error(\"Error processing the watch status update:\", error);\n        return response.status(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { liveSyncEndpoints };\n"
  },
  {
    "path": "server/endpoints/extensions/index.js",
    "content": "const { Telemetry } = require(\"../../models/telemetry\");\nconst { CollectorApi } = require(\"../../utils/collectorApi\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../../utils/middleware/multiUserProtected\");\nconst { validatedRequest } = require(\"../../utils/middleware/validatedRequest\");\nconst {\n  isSupportedRepoProvider,\n} = require(\"../../utils/middleware/isSupportedRepoProviders\");\n\nfunction extensionEndpoints(app) {\n  if (!app) return;\n\n  app.post(\n    \"/ext/:repo_platform/branches\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin, ROLES.manager]),\n      isSupportedRepoProvider,\n    ],\n    async (request, response) => {\n      try {\n        const { repo_platform } = request.params;\n        const responseFromProcessor =\n          await new CollectorApi().forwardExtensionRequest({\n            endpoint: `/ext/${repo_platform}-repo/branches`,\n            method: \"POST\",\n            body: request.body,\n          });\n        response.status(200).json(responseFromProcessor);\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/ext/:repo_platform/repo\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin, ROLES.manager]),\n      isSupportedRepoProvider,\n    ],\n    async (request, response) => {\n      try {\n        const { repo_platform } = request.params;\n        const responseFromProcessor =\n          await new CollectorApi().forwardExtensionRequest({\n            endpoint: `/ext/${repo_platform}-repo`,\n            method: \"POST\",\n            body: request.body,\n          });\n        await Telemetry.sendTelemetry(\"extension_invoked\", {\n          type: `${repo_platform}_repo`,\n        });\n        response.status(200).json(responseFromProcessor);\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/ext/youtube/transcript\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const responseFromProcessor =\n          await new CollectorApi().forwardExtensionRequest({\n            endpoint: \"/ext/youtube-transcript\",\n            method: \"POST\",\n            body: request.body,\n          });\n        await Telemetry.sendTelemetry(\"extension_invoked\", {\n          type: \"youtube_transcript\",\n        });\n        response.status(200).json(responseFromProcessor);\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/ext/confluence\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const responseFromProcessor =\n          await new CollectorApi().forwardExtensionRequest({\n            endpoint: \"/ext/confluence\",\n            method: \"POST\",\n            body: request.body,\n          });\n        await Telemetry.sendTelemetry(\"extension_invoked\", {\n          type: \"confluence\",\n        });\n        response.status(200).json(responseFromProcessor);\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n  app.post(\n    \"/ext/website-depth\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const responseFromProcessor =\n          await new CollectorApi().forwardExtensionRequest({\n            endpoint: \"/ext/website-depth\",\n            method: \"POST\",\n            body: request.body,\n          });\n        await Telemetry.sendTelemetry(\"extension_invoked\", {\n          type: \"website_depth\",\n        });\n        response.status(200).json(responseFromProcessor);\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n  app.post(\n    \"/ext/drupalwiki\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const responseFromProcessor =\n          await new CollectorApi().forwardExtensionRequest({\n            endpoint: \"/ext/drupalwiki\",\n            method: \"POST\",\n            body: request.body,\n          });\n        await Telemetry.sendTelemetry(\"extension_invoked\", {\n          type: \"drupalwiki\",\n        });\n        response.status(200).json(responseFromProcessor);\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/ext/obsidian/vault\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const responseFromProcessor =\n          await new CollectorApi().forwardExtensionRequest({\n            endpoint: \"/ext/obsidian/vault\",\n            method: \"POST\",\n            body: request.body,\n          });\n        await Telemetry.sendTelemetry(\"extension_invoked\", {\n          type: \"obsidian_vault\",\n        });\n        response.status(200).json(responseFromProcessor);\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/ext/paperless-ngx\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const responseFromProcessor =\n          await new CollectorApi().forwardExtensionRequest({\n            endpoint: \"/ext/paperless-ngx\",\n            method: \"POST\",\n            body: request.body,\n          });\n        await Telemetry.sendTelemetry(\"extension_invoked\", {\n          type: \"paperless_ngx\",\n        });\n        response.status(200).json(responseFromProcessor);\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { extensionEndpoints };\n"
  },
  {
    "path": "server/endpoints/invite.js",
    "content": "const { EventLogs } = require(\"../models/eventLogs\");\nconst { Invite } = require(\"../models/invite\");\nconst { User } = require(\"../models/user\");\nconst { reqBody } = require(\"../utils/http\");\nconst {\n  simpleSSOLoginDisabledMiddleware,\n} = require(\"../utils/middleware/simpleSSOEnabled\");\n\nfunction inviteEndpoints(app) {\n  if (!app) return;\n\n  app.get(\"/invite/:code\", async (request, response) => {\n    try {\n      const { code } = request.params;\n      const invite = await Invite.get({ code });\n      if (!invite) {\n        response.status(200).json({ invite: null, error: \"Invite not found.\" });\n        return;\n      }\n\n      if (invite.status !== \"pending\") {\n        response\n          .status(200)\n          .json({ invite: null, error: \"Invite is no longer valid.\" });\n        return;\n      }\n\n      response\n        .status(200)\n        .json({ invite: { code, status: invite.status }, error: null });\n    } catch (e) {\n      console.error(e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.post(\n    \"/invite/:code\",\n    [simpleSSOLoginDisabledMiddleware],\n    async (request, response) => {\n      try {\n        const { code } = request.params;\n        const { username, password } = reqBody(request);\n        const invite = await Invite.get({ code });\n        if (!invite || invite.status !== \"pending\") {\n          response\n            .status(200)\n            .json({ success: false, error: \"Invite not found or is invalid.\" });\n          return;\n        }\n\n        const { user, error } = await User.create({\n          username,\n          password,\n          role: \"default\",\n        });\n        if (!user) {\n          console.error(\"Accepting invite:\", error);\n          response.status(200).json({ success: false, error });\n          return;\n        }\n\n        await Invite.markClaimed(invite.id, user);\n        await EventLogs.logEvent(\n          \"invite_accepted\",\n          {\n            username: user.username,\n          },\n          user.id\n        );\n\n        response.status(200).json({ success: true, error: null });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { inviteEndpoints };\n"
  },
  {
    "path": "server/endpoints/mcpServers.js",
    "content": "const { reqBody } = require(\"../utils/http\");\nconst MCPCompatibilityLayer = require(\"../utils/MCP\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../utils/middleware/multiUserProtected\");\nconst { validatedRequest } = require(\"../utils/middleware/validatedRequest\");\n\nfunction mcpServersEndpoints(app) {\n  if (!app) return;\n\n  app.get(\n    \"/mcp-servers/force-reload\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (_request, response) => {\n      try {\n        const mcp = new MCPCompatibilityLayer();\n        await mcp.reloadMCPServers();\n        return response.status(200).json({\n          success: true,\n          error: null,\n          servers: await mcp.servers(),\n        });\n      } catch (error) {\n        console.error(\"Error force reloading MCP servers:\", error);\n        return response.status(500).json({\n          success: false,\n          error: error.message,\n          servers: [],\n        });\n      }\n    }\n  );\n\n  app.get(\n    \"/mcp-servers/list\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (_request, response) => {\n      try {\n        const servers = await new MCPCompatibilityLayer().servers();\n        return response.status(200).json({\n          success: true,\n          servers,\n        });\n      } catch (error) {\n        console.error(\"Error listing MCP servers:\", error);\n        return response.status(500).json({\n          success: false,\n          error: error.message,\n        });\n      }\n    }\n  );\n\n  app.post(\n    \"/mcp-servers/toggle\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { name } = reqBody(request);\n        const result = await new MCPCompatibilityLayer().toggleServerStatus(\n          name\n        );\n        return response.status(200).json({\n          success: result.success,\n          error: result.error,\n        });\n      } catch (error) {\n        console.error(\"Error toggling MCP server:\", error);\n        return response.status(500).json({\n          success: false,\n          error: error.message,\n        });\n      }\n    }\n  );\n\n  app.post(\n    \"/mcp-servers/delete\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { name } = reqBody(request);\n        const result = await new MCPCompatibilityLayer().deleteServer(name);\n        return response.status(200).json({\n          success: result.success,\n          error: result.error,\n        });\n      } catch (error) {\n        console.error(\"Error deleting MCP server:\", error);\n        return response.status(500).json({\n          success: false,\n          error: error.message,\n        });\n      }\n    }\n  );\n\n  app.post(\n    \"/mcp-servers/toggle-tool\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { serverName, toolName, enabled } = reqBody(request);\n        const result = await new MCPCompatibilityLayer().toggleToolSuppression(\n          serverName,\n          toolName,\n          enabled\n        );\n        return response.status(200).json({\n          success: result.success,\n          error: result.error,\n          suppressedTools: result.suppressedTools,\n        });\n      } catch (error) {\n        console.error(\"Error toggling MCP tool:\", error);\n        return response.status(500).json({\n          success: false,\n          error: error.message,\n          suppressedTools: [],\n        });\n      }\n    }\n  );\n}\n\nmodule.exports = { mcpServersEndpoints };\n"
  },
  {
    "path": "server/endpoints/mobile/index.js",
    "content": "const { validatedRequest } = require(\"../../utils/middleware/validatedRequest\");\nconst { MobileDevice } = require(\"../../models/mobileDevice\");\nconst { handleMobileCommand } = require(\"./utils\");\nconst { validDeviceToken, validRegistrationToken } = require(\"./middleware\");\nconst { reqBody } = require(\"../../utils/http\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../../utils/middleware/multiUserProtected\");\n\nfunction mobileEndpoints(app) {\n  if (!app) return;\n\n  /**\n   * Gets all the devices from the database.\n   * @param {import(\"express\").Request} request\n   * @param {import(\"express\").Response} response\n   */\n  app.get(\n    \"/mobile/devices\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (_request, response) => {\n      try {\n        const devices = await MobileDevice.where({}, null, null, {\n          user: { select: { id: true, username: true } },\n        });\n        return response.status(200).json({ devices });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  /**\n   * Updates the device status via an updates object.\n   * @param {import(\"express\").Request} request\n   * @param {import(\"express\").Response} response\n   */\n  app.post(\n    \"/mobile/update/:id\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const body = reqBody(request);\n        const updates = await MobileDevice.update(\n          Number(request.params.id),\n          body\n        );\n        if (updates.error)\n          return response.status(400).json({ error: updates.error });\n        return response.status(200).json({ updates });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  /**\n   * Deletes a device from the database.\n   * @param {import(\"express\").Request} request\n   * @param {import(\"express\").Response} response\n   */\n  app.delete(\n    \"/mobile/:id\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const device = await MobileDevice.get({\n          id: Number(request.params.id),\n        });\n        if (!device)\n          return response.status(404).json({ error: \"Device not found\" });\n        await MobileDevice.delete(device.id);\n        return response.status(200).json({ message: \"Device deleted\" });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/mobile/connect-info\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (_request, response) => {\n      try {\n        return response.status(200).json({\n          connectionUrl: MobileDevice.connectionURL(response.locals?.user),\n        });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  /**\n   * Checks if the device auth token is valid\n   * against approved devices.\n   */\n  app.get(\"/mobile/auth\", [validDeviceToken], async (_, response) => {\n    try {\n      return response\n        .status(200)\n        .json({ success: true, message: \"Device authenticated\" });\n    } catch (e) {\n      console.error(e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  /**\n   * Registers a new device (is open so that the mobile app can register itself)\n   * Will create a new device in the database but requires approval by the user\n   * before it can be used.\n   * @param {import(\"express\").Request} request\n   * @param {import(\"express\").Response} response\n   */\n  app.post(\n    \"/mobile/register\",\n    [validRegistrationToken],\n    async (request, response) => {\n      try {\n        const body = reqBody(request);\n        const result = await MobileDevice.create({\n          deviceOs: body.deviceOs,\n          deviceName: body.deviceName,\n          userId: response.locals?.user?.id,\n        });\n\n        if (result.error)\n          return response.status(400).json({ error: result.error });\n        return response.status(200).json({\n          token: result.device.token,\n          platform: MobileDevice.platform,\n        });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/mobile/send/:command\",\n    [validDeviceToken],\n    async (request, response) => {\n      try {\n        return handleMobileCommand(request, response);\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { mobileEndpoints };\n"
  },
  {
    "path": "server/endpoints/mobile/middleware/index.js",
    "content": "const { MobileDevice } = require(\"../../../models/mobileDevice\");\nconst { SystemSettings } = require(\"../../../models/systemSettings\");\nconst { User } = require(\"../../../models/user\");\n\n/**\n * Validates the device id from the request headers by checking if the device\n * exists in the database and is approved.\n * @param {import(\"express\").Request} request\n * @param {import(\"express\").Response} response\n * @param {import(\"express\").NextFunction} next\n */\nasync function validDeviceToken(request, response, next) {\n  try {\n    const token = request.header(\"x-anythingllm-mobile-device-token\");\n    if (!token)\n      return response.status(400).json({ error: \"Device token is required\" });\n\n    const device = await MobileDevice.get(\n      { token: String(token) },\n      { user: true }\n    );\n    if (!device)\n      return response.status(400).json({ error: \"Device not found\" });\n    if (!device.approved)\n      return response.status(400).json({ error: \"Device not approved\" });\n\n    // If the device is associated with a user then we can associate it with the locals\n    // so we can reuse it later.\n    if (device.user) {\n      if (device.user.suspended)\n        return response.status(400).json({ error: \"User is suspended.\" });\n      response.locals.user = device.user;\n    }\n\n    delete device.user;\n    response.locals.device = device;\n    next();\n  } catch (error) {\n    console.error(\"validDeviceToken\", error);\n    response.status(500).json({ error: \"Invalid middleware response\" });\n  }\n}\n\n/**\n * Validates a temporary registration token that is passed in the request\n * and associates the user with the token (if valid). Temporary token is consumed\n * and cannot be used again after this middleware is called.\n * @param {*} request\n * @param {*} response\n * @param {*} next\n */\nasync function validRegistrationToken(request, response, next) {\n  try {\n    const authHeader = request.header(\"Authorization\");\n    const tempToken = authHeader ? authHeader.split(\" \")[1] : null;\n    if (!tempToken)\n      return response\n        .status(400)\n        .json({ error: \"Registration token is required\" });\n\n    const tempTokenData = MobileDevice.tempToken(tempToken);\n    if (!tempTokenData)\n      return response\n        .status(400)\n        .json({ error: \"Invalid or expired registration token\" });\n\n    // If in multi-user mode, we need to validate the user id\n    // associated exists, is not banned and then associate with locals so we can reuse it later.\n    // If not in multi-user mode then simply having a valid token is enough.\n    const multiUserMode = await SystemSettings.isMultiUserMode();\n    if (multiUserMode) {\n      if (!tempTokenData.userId)\n        return response\n          .status(400)\n          .json({ error: \"User id not found in registration token\" });\n      const user = await User.get({ id: Number(tempTokenData.userId) });\n      if (!user) return response.status(400).json({ error: \"User not found\" });\n      if (user.suspended)\n        return response\n          .status(400)\n          .json({ error: \"User is suspended - cannot register device\" });\n      response.locals.user = user;\n    }\n\n    next();\n  } catch (error) {\n    console.error(\"validRegistrationToken:error\", error);\n    response.status(500).json({\n      error: \"Invalid middleware response from validRegistrationToken\",\n    });\n  }\n}\n\nmodule.exports = {\n  validDeviceToken,\n  validRegistrationToken,\n};\n"
  },
  {
    "path": "server/endpoints/mobile/utils/index.js",
    "content": "const { Workspace } = require(\"../../../models/workspace\");\nconst { WorkspaceChats } = require(\"../../../models/workspaceChats\");\nconst { WorkspaceThread } = require(\"../../../models/workspaceThread\");\nconst { ApiChatHandler } = require(\"../../../utils/chats/apiChatHandler\");\nconst { reqBody } = require(\"../../../utils/http\");\nconst prisma = require(\"../../../utils/prisma\");\nconst { getModelTag } = require(\"../../utils\");\nconst { MobileDevice } = require(\"../../../models/mobileDevice\");\n\n/**\n *\n * @param {import(\"express\").Request} request\n * @param {import(\"express\").Response} response\n * @returns\n */\nasync function handleMobileCommand(request, response) {\n  const { command } = request.params;\n  const user = response.locals.user ?? null;\n  const body = reqBody(request);\n\n  if (command === \"workspaces\") {\n    const workspaces = user\n      ? await Workspace.whereWithUser(user, {})\n      : await Workspace.where({});\n    for (const workspace of workspaces) {\n      const [threadCount, chatCount] = await Promise.all([\n        prisma.workspace_threads.count({\n          where: {\n            workspace_id: workspace.id,\n            ...(user ? { user_id: user.id } : {}),\n          },\n        }),\n        prisma.workspace_chats.count({\n          where: {\n            workspaceId: workspace.id,\n            include: true,\n            ...(user ? { user_id: user.id } : {}),\n          },\n        }),\n      ]);\n      workspace.threadCount = threadCount;\n      workspace.chatCount = chatCount;\n      workspace.platform = MobileDevice.platform;\n    }\n    return response.status(200).json({ workspaces });\n  }\n\n  if (command === \"workspace-content\") {\n    const workspace = user\n      ? await Workspace.getWithUser(user, { slug: String(body.workspaceSlug) })\n      : await Workspace.get({ slug: String(body.workspaceSlug) });\n\n    if (!workspace)\n      return response.status(400).json({ error: \"Workspace not found\" });\n    const threads = [\n      {\n        id: 0,\n        name: \"Default Thread\",\n        slug: \"default-thread\",\n        workspace_id: workspace.id,\n        createdAt: new Date(),\n        lastUpdatedAt: new Date(),\n      },\n      ...(await prisma.workspace_threads.findMany({\n        where: {\n          workspace_id: workspace.id,\n          ...(user ? { user_id: user.id } : {}),\n        },\n      })),\n    ];\n    const chats = (\n      await prisma.workspace_chats.findMany({\n        where: {\n          workspaceId: workspace.id,\n          include: true,\n          ...(user ? { user_id: user.id } : {}),\n        },\n      })\n    ).map((chat) => ({\n      ...chat,\n      // Create a dummy thread_id for the default thread so the chats can be mapped correctly.\n      ...(chat.thread_id === null ? { thread_id: 0 } : {}),\n      createdAt: chat.createdAt.toISOString(),\n      lastUpdatedAt: chat.lastUpdatedAt.toISOString(),\n    }));\n    return response.status(200).json({ threads, chats });\n  }\n\n  // Get the model for this workspace (workspace -> system)\n  if (command === \"model-tag\") {\n    const { workspaceSlug } = body;\n    const workspace = user\n      ? await Workspace.getWithUser(user, { slug: String(workspaceSlug) })\n      : await Workspace.get({ slug: String(workspaceSlug) });\n\n    if (!workspace)\n      return response.status(400).json({ error: \"Workspace not found\" });\n    if (workspace.chatModel)\n      return response.status(200).json({ model: workspace.chatModel });\n    else return response.status(200).json({ model: getModelTag() });\n  }\n\n  if (command === \"reset-chat\") {\n    const { workspaceSlug, threadSlug } = body;\n    const workspace = user\n      ? await Workspace.getWithUser(user, { slug: String(workspaceSlug) })\n      : await Workspace.get({ slug: String(workspaceSlug) });\n\n    if (!workspace)\n      return response.status(400).json({ error: \"Workspace not found\" });\n    const threadId = threadSlug\n      ? await prisma.workspace_threads.findFirst({\n          where: {\n            workspace_id: workspace.id,\n            slug: String(threadSlug),\n            ...(user ? { user_id: user.id } : {}),\n          },\n        })?.id\n      : null;\n\n    await WorkspaceChats.markThreadHistoryInvalidV2({\n      workspaceId: workspace.id,\n      ...(user ? { user_id: user.id } : {}),\n      thread_id: threadId, // if threadId is null, this will reset the default thread.\n    });\n    return response.status(200).json({ success: true });\n  }\n\n  if (command === \"new-thread\") {\n    const { workspaceSlug } = body;\n    const workspace = user\n      ? await Workspace.getWithUser(user, { slug: String(workspaceSlug) })\n      : await Workspace.get({ slug: String(workspaceSlug) });\n\n    if (!workspace)\n      return response.status(400).json({ error: \"Workspace not found\" });\n    const { thread } = await WorkspaceThread.new(workspace, user?.id);\n    return response.status(200).json({ thread });\n  }\n\n  if (command === \"stream-chat\") {\n    const { workspaceSlug = null, threadSlug = null, message } = body;\n    if (!workspaceSlug)\n      return response.status(400).json({ error: \"Workspace ID is required\" });\n    else if (!message)\n      return response.status(400).json({ error: \"Message is required\" });\n\n    const workspace = user\n      ? await Workspace.getWithUser(user, { slug: String(workspaceSlug) })\n      : await Workspace.get({ slug: String(workspaceSlug) });\n\n    if (!workspace)\n      return response.status(400).json({ error: \"Workspace not found\" });\n    const thread = threadSlug\n      ? await prisma.workspace_threads.findFirst({\n          where: {\n            workspace_id: workspace.id,\n            slug: String(threadSlug),\n            ...(user ? { user_id: user.id } : {}),\n          },\n        })\n      : null;\n\n    response.setHeader(\"Cache-Control\", \"no-cache\");\n    response.setHeader(\"Content-Type\", \"text/event-stream\");\n    response.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n    response.setHeader(\"Connection\", \"keep-alive\");\n    response.flushHeaders();\n    await ApiChatHandler.streamChat({\n      response,\n      workspace,\n      thread,\n      message,\n      mode: \"chat\",\n      user: user,\n      sessionId: null,\n      attachments: [],\n      reset: false,\n    });\n    return response.end();\n  }\n\n  if (command === \"unregister-device\") {\n    if (!response.locals.device)\n      return response.status(200).json({ success: true });\n    await MobileDevice.delete(response.locals.device.id);\n    return response.status(200).json({ success: true });\n  }\n\n  return response.status(400).json({ error: \"Invalid command\" });\n}\n\nmodule.exports = {\n  handleMobileCommand,\n};\n"
  },
  {
    "path": "server/endpoints/system.js",
    "content": "process.env.NODE_ENV === \"development\"\n  ? require(\"dotenv\").config({ path: `.env.${process.env.NODE_ENV}` })\n  : require(\"dotenv\").config();\nconst { viewLocalFiles, normalizePath, isWithin } = require(\"../utils/files\");\nconst { purgeDocument, purgeFolder } = require(\"../utils/files/purgeDocument\");\nconst { getVectorDbClass } = require(\"../utils/helpers\");\nconst { updateENV, dumpENV } = require(\"../utils/helpers/updateENV\");\nconst {\n  reqBody,\n  makeJWT,\n  userFromSession,\n  multiUserMode,\n  queryParams,\n} = require(\"../utils/http\");\nconst { handleAssetUpload, handlePfpUpload } = require(\"../utils/files/multer\");\nconst { v4 } = require(\"uuid\");\nconst { SystemSettings } = require(\"../models/systemSettings\");\nconst { User } = require(\"../models/user\");\nconst { validatedRequest } = require(\"../utils/middleware/validatedRequest\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst {\n  getDefaultFilename,\n  determineLogoFilepath,\n  fetchLogo,\n  validFilename,\n  renameLogoFile,\n  removeCustomLogo,\n  LOGO_FILENAME,\n  isDefaultFilename,\n} = require(\"../utils/files/logo\");\nconst { Telemetry } = require(\"../models/telemetry\");\nconst { ApiKey } = require(\"../models/apiKeys\");\nconst { getCustomModels } = require(\"../utils/helpers/customModels\");\nconst { WorkspaceChats } = require(\"../models/workspaceChats\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n  isMultiUserSetup,\n} = require(\"../utils/middleware/multiUserProtected\");\nconst { fetchPfp, determinePfpFilepath } = require(\"../utils/files/pfp\");\nconst { exportChatsAsType } = require(\"../utils/helpers/chat/convertTo\");\nconst { EventLogs } = require(\"../models/eventLogs\");\nconst { CollectorApi } = require(\"../utils/collectorApi\");\nconst {\n  recoverAccount,\n  resetPassword,\n  generateRecoveryCodes,\n} = require(\"../utils/PasswordRecovery\");\nconst { SlashCommandPresets } = require(\"../models/slashCommandsPresets\");\nconst { EncryptionManager } = require(\"../utils/EncryptionManager\");\nconst { BrowserExtensionApiKey } = require(\"../models/browserExtensionApiKey\");\nconst {\n  chatHistoryViewable,\n} = require(\"../utils/middleware/chatHistoryViewable\");\nconst {\n  simpleSSOEnabled,\n  simpleSSOLoginDisabled,\n} = require(\"../utils/middleware/simpleSSOEnabled\");\nconst { TemporaryAuthToken } = require(\"../models/temporaryAuthToken\");\nconst { SystemPromptVariables } = require(\"../models/systemPromptVariables\");\nconst { VALID_COMMANDS } = require(\"../utils/chats\");\n\nfunction systemEndpoints(app) {\n  if (!app) return;\n\n  app.get(\"/ping\", (_, response) => {\n    response.status(200).json({ online: true });\n  });\n\n  app.get(\"/migrate\", async (_, response) => {\n    response.sendStatus(200);\n  });\n\n  app.get(\"/env-dump\", async (_, response) => {\n    if (process.env.NODE_ENV !== \"production\")\n      return response.sendStatus(200).end();\n    dumpENV();\n    response.sendStatus(200).end();\n  });\n\n  app.get(\"/onboarding\", async (_, response) => {\n    try {\n      const results = await SystemSettings.isOnboardingComplete();\n      response.status(200).json({ onboardingComplete: results });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.post(\"/onboarding\", [validatedRequest], async (_, response) => {\n    try {\n      await SystemSettings.markOnboardingComplete();\n      response.sendStatus(200).end();\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.get(\"/setup-complete\", async (_, response) => {\n    try {\n      const results = await SystemSettings.currentSettings();\n      response.status(200).json({ results });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.get(\n    \"/system/check-token\",\n    [validatedRequest],\n    async (request, response) => {\n      try {\n        if (multiUserMode(response)) {\n          const user = await userFromSession(request, response);\n          if (!user || user.suspended) {\n            response.sendStatus(403).end();\n            return;\n          }\n\n          response.sendStatus(200).end();\n          return;\n        }\n\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  /**\n   * Refreshes the user object from the session from a provided token.\n   * This does not refresh the token itself - if that is expired or invalid, the user will be logged out.\n   * This simply keeps the user object in sync with the database over the course of the session.\n   * @returns {Promise<{success: boolean, user: Object | null, message: string | null}>}\n   */\n  app.get(\n    \"/system/refresh-user\",\n    [validatedRequest],\n    async (request, response) => {\n      try {\n        if (!multiUserMode(response))\n          return response\n            .status(200)\n            .json({ success: true, user: null, message: null });\n\n        const user = await userFromSession(request, response);\n        if (!user)\n          return response.status(200).json({\n            success: false,\n            user: null,\n            message: \"Session expired or invalid.\",\n          });\n\n        if (user.suspended)\n          return response.status(200).json({\n            success: false,\n            user: null,\n            message: \"User is suspended.\",\n          });\n\n        return response.status(200).json({\n          success: true,\n          user: User.filterFields(user),\n          message: null,\n        });\n      } catch (e) {\n        return response.status(500).json({\n          success: false,\n          user: null,\n          message: e.message,\n        });\n      }\n    }\n  );\n\n  app.post(\"/request-token\", async (request, response) => {\n    try {\n      const bcrypt = require(\"bcryptjs\");\n\n      if (await SystemSettings.isMultiUserMode()) {\n        if (simpleSSOLoginDisabled()) {\n          response.status(403).json({\n            user: null,\n            valid: false,\n            token: null,\n            message:\n              \"[005] Login via credentials has been disabled by the administrator.\",\n          });\n          return;\n        }\n\n        const { username, password } = reqBody(request);\n        const existingUser = await User._get({ username: String(username) });\n\n        if (!existingUser) {\n          await EventLogs.logEvent(\n            \"failed_login_invalid_username\",\n            {\n              ip: request.ip || \"Unknown IP\",\n              username: username || \"Unknown user\",\n            },\n            existingUser?.id\n          );\n          response.status(200).json({\n            user: null,\n            valid: false,\n            token: null,\n            message: \"[001] Invalid login credentials.\",\n          });\n          return;\n        }\n\n        if (!bcrypt.compareSync(String(password), existingUser.password)) {\n          await EventLogs.logEvent(\n            \"failed_login_invalid_password\",\n            {\n              ip: request.ip || \"Unknown IP\",\n              username: username || \"Unknown user\",\n            },\n            existingUser?.id\n          );\n          response.status(200).json({\n            user: null,\n            valid: false,\n            token: null,\n            message: \"[002] Invalid login credentials.\",\n          });\n          return;\n        }\n\n        if (existingUser.suspended) {\n          await EventLogs.logEvent(\n            \"failed_login_account_suspended\",\n            {\n              ip: request.ip || \"Unknown IP\",\n              username: username || \"Unknown user\",\n            },\n            existingUser?.id\n          );\n          response.status(200).json({\n            user: null,\n            valid: false,\n            token: null,\n            message: \"[004] Account suspended by admin.\",\n          });\n          return;\n        }\n\n        await Telemetry.sendTelemetry(\n          \"login_event\",\n          { multiUserMode: false },\n          existingUser?.id\n        );\n\n        await EventLogs.logEvent(\n          \"login_event\",\n          {\n            ip: request.ip || \"Unknown IP\",\n            username: existingUser.username || \"Unknown user\",\n          },\n          existingUser?.id\n        );\n\n        // Generate a session token for the user then check if they have seen the recovery codes\n        // and if not, generate recovery codes and return them to the frontend.\n        const sessionToken = makeJWT(\n          { id: existingUser.id, username: existingUser.username },\n          process.env.JWT_EXPIRY\n        );\n        if (!existingUser.seen_recovery_codes) {\n          const plainTextCodes = await generateRecoveryCodes(existingUser.id);\n          response.status(200).json({\n            valid: true,\n            user: User.filterFields(existingUser),\n            token: sessionToken,\n            message: null,\n            recoveryCodes: plainTextCodes,\n          });\n          return;\n        }\n\n        response.status(200).json({\n          valid: true,\n          user: User.filterFields(existingUser),\n          token: sessionToken,\n          message: null,\n        });\n        return;\n      } else {\n        const { password } = reqBody(request);\n        if (\n          !bcrypt.compareSync(\n            password,\n            bcrypt.hashSync(process.env.AUTH_TOKEN, 10)\n          )\n        ) {\n          await EventLogs.logEvent(\"failed_login_invalid_password\", {\n            ip: request.ip || \"Unknown IP\",\n            multiUserMode: false,\n          });\n          response.status(401).json({\n            valid: false,\n            token: null,\n            message: \"[003] Invalid password provided\",\n          });\n          return;\n        }\n\n        await Telemetry.sendTelemetry(\"login_event\", { multiUserMode: false });\n        await EventLogs.logEvent(\"login_event\", {\n          ip: request.ip || \"Unknown IP\",\n          multiUserMode: false,\n        });\n        response.status(200).json({\n          valid: true,\n          token: makeJWT(\n            { p: new EncryptionManager().encrypt(password) },\n            process.env.JWT_EXPIRY\n          ),\n          message: null,\n        });\n      }\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.get(\n    \"/request-token/sso/simple\",\n    [simpleSSOEnabled],\n    async (request, response) => {\n      const { token: tempAuthToken } = request.query;\n      const { sessionToken, token, error } =\n        await TemporaryAuthToken.validate(tempAuthToken);\n\n      if (error) {\n        await EventLogs.logEvent(\"failed_login_invalid_temporary_auth_token\", {\n          ip: request.ip || \"Unknown IP\",\n          multiUserMode: true,\n        });\n        return response.status(401).json({\n          valid: false,\n          token: null,\n          message: `[001] An error occurred while validating the token: ${error}`,\n        });\n      }\n\n      await Telemetry.sendTelemetry(\n        \"login_event\",\n        { multiUserMode: true },\n        token.user.id\n      );\n      await EventLogs.logEvent(\n        \"login_event\",\n        {\n          ip: request.ip || \"Unknown IP\",\n          username: token.user.username || \"Unknown user\",\n        },\n        token.user.id\n      );\n\n      response.status(200).json({\n        valid: true,\n        user: User.filterFields(token.user),\n        token: sessionToken,\n        message: null,\n      });\n    }\n  );\n\n  app.post(\n    \"/system/recover-account\",\n    [isMultiUserSetup],\n    async (request, response) => {\n      try {\n        const { username, recoveryCodes } = reqBody(request);\n        const { success, resetToken, error } = await recoverAccount(\n          username,\n          recoveryCodes\n        );\n\n        if (success) {\n          response.status(200).json({ success, resetToken });\n        } else {\n          response.status(400).json({ success, message: error });\n        }\n      } catch (error) {\n        console.error(\"Error recovering account:\", error);\n        response\n          .status(500)\n          .json({ success: false, message: \"Internal server error\" });\n      }\n    }\n  );\n\n  app.post(\n    \"/system/reset-password\",\n    [isMultiUserSetup],\n    async (request, response) => {\n      try {\n        const { token, newPassword, confirmPassword } = reqBody(request);\n        const { success, message, error } = await resetPassword(\n          token,\n          newPassword,\n          confirmPassword\n        );\n\n        if (success) {\n          response.status(200).json({ success, message });\n        } else {\n          response.status(400).json({ success, error });\n        }\n      } catch (error) {\n        console.error(\"Error resetting password:\", error);\n        response.status(500).json({ success: false, message: error.message });\n      }\n    }\n  );\n\n  app.get(\n    \"/system/system-vectors\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const query = queryParams(request);\n        const VectorDb = getVectorDbClass();\n        const vectorCount = !!query.slug\n          ? await VectorDb.namespaceCount(query.slug)\n          : await VectorDb.totalVectors();\n        response.status(200).json({ vectorCount });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/system/remove-document\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { name } = reqBody(request);\n        await purgeDocument(name);\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/system/remove-documents\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { names } = reqBody(request);\n        for await (const name of names) await purgeDocument(name);\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/system/remove-folder\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { name } = reqBody(request);\n        await purgeFolder(name);\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/system/local-files\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (_, response) => {\n      try {\n        const localFiles = await viewLocalFiles();\n        response.status(200).json({ localFiles });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/system/document-processing-status\",\n    [validatedRequest],\n    async (_, response) => {\n      try {\n        const online = await new CollectorApi().online();\n        response.sendStatus(online ? 200 : 503);\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/system/accepted-document-types\",\n    [validatedRequest],\n    async (_, response) => {\n      try {\n        const types = await new CollectorApi().acceptedFileTypes();\n        if (!types) {\n          response.sendStatus(404).end();\n          return;\n        }\n\n        response.status(200).json({ types });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/system/update-env\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const body = reqBody(request);\n        const { newValues, error } = await updateENV(\n          body,\n          false,\n          response?.locals?.user?.id\n        );\n        response.status(200).json({ newValues, error });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/system/update-password\",\n    [validatedRequest],\n    async (request, response) => {\n      try {\n        // Cannot update password in multi - user mode.\n        if (multiUserMode(response)) {\n          response.sendStatus(401).end();\n          return;\n        }\n\n        let error = null;\n        const { usePassword, newPassword } = reqBody(request);\n        if (!usePassword) {\n          // Password is being disabled so directly unset everything to bypass validation.\n          process.env.AUTH_TOKEN = \"\";\n          process.env.JWT_SECRET = \"\";\n        } else {\n          error = await updateENV(\n            {\n              AuthToken: newPassword,\n              JWTSecret: v4(),\n            },\n            true\n          )?.error;\n        }\n        response.status(200).json({ success: !error, error });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/system/enable-multi-user\",\n    [validatedRequest],\n    async (request, response) => {\n      try {\n        if (response.locals.multiUserMode) {\n          response.status(200).json({\n            success: false,\n            error: \"Multi-user mode is already enabled.\",\n          });\n          return;\n        }\n\n        const { username, password } = reqBody(request);\n        const { user, error } = await User.create({\n          username,\n          password,\n          role: ROLES.admin,\n        });\n\n        if (error || !user) {\n          response.status(400).json({\n            success: false,\n            error: error || \"Failed to enable multi-user mode.\",\n          });\n          return;\n        }\n\n        await SystemSettings._updateSettings({\n          multi_user_mode: true,\n        });\n        await BrowserExtensionApiKey.migrateApiKeysToMultiUser(user.id);\n\n        await updateENV(\n          {\n            JWTSecret: process.env.JWT_SECRET || v4(),\n          },\n          true\n        );\n        await Telemetry.sendTelemetry(\"enabled_multi_user_mode\", {\n          multiUserMode: true,\n        });\n        await EventLogs.logEvent(\"multi_user_mode_enabled\", {}, user?.id);\n        response.status(200).json({ success: !!user, error });\n      } catch (e) {\n        await User.delete({});\n        await SystemSettings._updateSettings({\n          multi_user_mode: false,\n        });\n\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\"/system/multi-user-mode\", async (_, response) => {\n    try {\n      const multiUserMode = await SystemSettings.isMultiUserMode();\n      response.status(200).json({ multiUserMode });\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  app.get(\"/system/logo\", async function (request, response) {\n    try {\n      const darkMode =\n        !request?.query?.theme || request?.query?.theme === \"default\";\n      const defaultFilename = getDefaultFilename(darkMode);\n      const logoPath = await determineLogoFilepath(defaultFilename);\n      const { found, buffer, size, mime } = fetchLogo(logoPath);\n\n      if (!found) {\n        response.sendStatus(204).end();\n        return;\n      }\n\n      const currentLogoFilename = await SystemSettings.currentLogoFilename();\n      response.writeHead(200, {\n        \"Access-Control-Expose-Headers\":\n          \"Content-Disposition,X-Is-Custom-Logo,Content-Type,Content-Length\",\n        \"Content-Type\": mime || \"image/png\",\n        \"Content-Disposition\": `attachment; filename=${path.basename(\n          logoPath\n        )}`,\n        \"Content-Length\": size,\n        \"X-Is-Custom-Logo\":\n          currentLogoFilename !== null &&\n          currentLogoFilename !== defaultFilename &&\n          !isDefaultFilename(currentLogoFilename),\n      });\n      response.end(Buffer.from(buffer, \"base64\"));\n      return;\n    } catch (error) {\n      console.error(\"Error processing the logo request:\", error);\n      response.status(500).json({ message: \"Internal server error\" });\n    }\n  });\n\n  app.get(\"/system/footer-data\", [validatedRequest], async (_, response) => {\n    try {\n      const footerData =\n        (await SystemSettings.get({ label: \"footer_data\" }))?.value ??\n        JSON.stringify([]);\n      response.status(200).json({ footerData: footerData });\n    } catch (error) {\n      console.error(\"Error fetching footer data:\", error);\n      response.status(500).json({ message: \"Internal server error\" });\n    }\n  });\n\n  app.get(\"/system/support-email\", [validatedRequest], async (_, response) => {\n    try {\n      const supportEmail =\n        (\n          await SystemSettings.get({\n            label: \"support_email\",\n          })\n        )?.value ?? null;\n      response.status(200).json({ supportEmail: supportEmail });\n    } catch (error) {\n      console.error(\"Error fetching support email:\", error);\n      response.status(500).json({ message: \"Internal server error\" });\n    }\n  });\n\n  // No middleware protection in order to get this on the login page\n  app.get(\"/system/custom-app-name\", async (_, response) => {\n    try {\n      const customAppName =\n        (\n          await SystemSettings.get({\n            label: \"custom_app_name\",\n          })\n        )?.value ?? null;\n      response.status(200).json({ customAppName: customAppName });\n    } catch (error) {\n      console.error(\"Error fetching custom app name:\", error);\n      response.status(500).json({ message: \"Internal server error\" });\n    }\n  });\n\n  app.get(\n    \"/system/pfp/:id\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async function (request, response) {\n      try {\n        const { id } = request.params;\n        if (response.locals?.user?.id !== Number(id))\n          return response.sendStatus(204).end();\n\n        const pfpPath = await determinePfpFilepath(id);\n        if (!pfpPath) return response.sendStatus(204).end();\n\n        const { found, buffer, size, mime } = fetchPfp(pfpPath);\n        if (!found) return response.sendStatus(204).end();\n\n        response.writeHead(200, {\n          \"Content-Type\": mime || \"image/png\",\n          \"Content-Disposition\": `attachment; filename=${path.basename(pfpPath)}`,\n          \"Content-Length\": size,\n        });\n        response.end(Buffer.from(buffer, \"base64\"));\n        return;\n      } catch (error) {\n        console.error(\"Error processing the logo request:\", error);\n        response.status(500).json({ message: \"Internal server error\" });\n      }\n    }\n  );\n\n  app.post(\n    \"/system/upload-pfp\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), handlePfpUpload],\n    async function (request, response) {\n      try {\n        const user = await userFromSession(request, response);\n        const uploadedFileName = request.randomFileName;\n        if (!uploadedFileName) {\n          return response.status(400).json({ message: \"File upload failed.\" });\n        }\n\n        const userRecord = await User.get({ id: user.id });\n        const oldPfpFilename = userRecord.pfpFilename;\n        if (oldPfpFilename) {\n          const storagePath = path.join(__dirname, \"../storage/assets/pfp\");\n          const oldPfpPath = path.join(\n            storagePath,\n            normalizePath(userRecord.pfpFilename)\n          );\n          if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))\n            throw new Error(\"Invalid path name\");\n          if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);\n        }\n\n        const { success, error } = await User.update(user.id, {\n          pfpFilename: uploadedFileName,\n        });\n\n        return response.status(success ? 200 : 500).json({\n          message: success\n            ? \"Profile picture uploaded successfully.\"\n            : error || \"Failed to update with new profile picture.\",\n        });\n      } catch (error) {\n        console.error(\"Error processing the profile picture upload:\", error);\n        response.status(500).json({ message: \"Internal server error\" });\n      }\n    }\n  );\n  app.get(\n    \"/system/default-system-prompt\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async (_, response) => {\n      try {\n        const defaultSystemPrompt = await SystemSettings.get({\n          label: \"default_system_prompt\",\n        });\n\n        response.status(200).json({\n          success: true,\n          defaultSystemPrompt:\n            defaultSystemPrompt?.value ||\n            SystemSettings.saneDefaultSystemPrompt,\n          saneDefaultSystemPrompt: SystemSettings.saneDefaultSystemPrompt,\n        });\n      } catch (error) {\n        console.error(\"Error fetching default system prompt:\", error);\n        response\n          .status(500)\n          .json({ success: false, message: \"Internal server error\" });\n      }\n    }\n  );\n\n  app.post(\n    \"/system/default-system-prompt\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { defaultSystemPrompt } = reqBody(request);\n        const { success, error } = await SystemSettings.updateSettings({\n          default_system_prompt: defaultSystemPrompt,\n        });\n        if (!success)\n          throw new Error(\n            error.message || \"Failed to update default system prompt.\"\n          );\n        response.status(200).json({\n          success: true,\n          message: \"Default system prompt updated successfully.\",\n        });\n      } catch (error) {\n        console.error(\"Error updating default system prompt:\", error);\n        response.status(500).json({\n          success: false,\n          message: error.message || \"Internal server error\",\n        });\n      }\n    }\n  );\n\n  app.delete(\n    \"/system/remove-pfp\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async function (request, response) {\n      try {\n        const user = await userFromSession(request, response);\n        const userRecord = await User.get({ id: user.id });\n        const oldPfpFilename = userRecord.pfpFilename;\n\n        if (oldPfpFilename) {\n          const storagePath = path.join(__dirname, \"../storage/assets/pfp\");\n          const oldPfpPath = path.join(\n            storagePath,\n            normalizePath(oldPfpFilename)\n          );\n          if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))\n            throw new Error(\"Invalid path name\");\n          if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);\n        }\n\n        const { success, error } = await User.update(user.id, {\n          pfpFilename: null,\n        });\n\n        return response.status(success ? 200 : 500).json({\n          message: success\n            ? \"Profile picture removed successfully.\"\n            : error || \"Failed to remove profile picture.\",\n        });\n      } catch (error) {\n        console.error(\"Error processing the profile picture removal:\", error);\n        response.status(500).json({ message: \"Internal server error\" });\n      }\n    }\n  );\n\n  app.post(\n    \"/system/upload-logo\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin, ROLES.manager]),\n      handleAssetUpload,\n    ],\n    async (request, response) => {\n      if (!request?.file || !request?.file.originalname) {\n        return response.status(400).json({ message: \"No logo file provided.\" });\n      }\n\n      if (!validFilename(request.file.originalname)) {\n        return response.status(400).json({\n          message: \"Invalid file name. Please choose a different file.\",\n        });\n      }\n\n      try {\n        const newFilename = await renameLogoFile(request.file.originalname);\n        const existingLogoFilename = await SystemSettings.currentLogoFilename();\n        await removeCustomLogo(existingLogoFilename);\n\n        const { success, error } = await SystemSettings._updateSettings({\n          logo_filename: newFilename,\n        });\n\n        return response.status(success ? 200 : 500).json({\n          message: success\n            ? \"Logo uploaded successfully.\"\n            : error || \"Failed to update with new logo.\",\n        });\n      } catch (error) {\n        console.error(\"Error processing the logo upload:\", error);\n        response.status(500).json({ message: \"Error uploading the logo.\" });\n      }\n    }\n  );\n\n  app.get(\"/system/is-default-logo\", async (_, response) => {\n    try {\n      const currentLogoFilename = await SystemSettings.currentLogoFilename();\n      const isDefaultLogo =\n        !currentLogoFilename || currentLogoFilename === LOGO_FILENAME;\n      response.status(200).json({ isDefaultLogo });\n    } catch (error) {\n      console.error(\"Error processing the logo request:\", error);\n      response.status(500).json({ message: \"Internal server error\" });\n    }\n  });\n\n  app.get(\n    \"/system/remove-logo\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (_request, response) => {\n      try {\n        const currentLogoFilename = await SystemSettings.currentLogoFilename();\n        await removeCustomLogo(currentLogoFilename);\n        const { success, error } = await SystemSettings._updateSettings({\n          logo_filename: LOGO_FILENAME,\n        });\n\n        return response.status(success ? 200 : 500).json({\n          message: success\n            ? \"Logo removed successfully.\"\n            : error || \"Failed to update with new logo.\",\n        });\n      } catch (error) {\n        console.error(\"Error processing the logo removal:\", error);\n        response.status(500).json({ message: \"Error removing the logo.\" });\n      }\n    }\n  );\n\n  app.get(\"/system/api-keys\", [validatedRequest], async (_, response) => {\n    try {\n      if (response.locals.multiUserMode) {\n        return response.sendStatus(401).end();\n      }\n\n      const apiKeys = await ApiKey.where({});\n      return response.status(200).json({\n        apiKeys,\n        error: null,\n      });\n    } catch (error) {\n      console.error(error);\n      response.status(500).json({\n        apiKey: null,\n        error: \"Could not find an API Key.\",\n      });\n    }\n  });\n\n  app.post(\n    \"/system/generate-api-key\",\n    [validatedRequest],\n    async (_, response) => {\n      try {\n        if (response.locals.multiUserMode) {\n          return response.sendStatus(401).end();\n        }\n\n        const { apiKey, error } = await ApiKey.create();\n        await EventLogs.logEvent(\n          \"api_key_created\",\n          {},\n          response?.locals?.user?.id\n        );\n        return response.status(200).json({\n          apiKey,\n          error,\n        });\n      } catch (error) {\n        console.error(error);\n        response.status(500).json({\n          apiKey: null,\n          error: \"Error generating api key.\",\n        });\n      }\n    }\n  );\n\n  // TODO: This endpoint is replicated in the admin endpoints file.\n  // and should be consolidated to be a single endpoint with flexible role protection.\n  app.delete(\n    \"/system/api-key/:id\",\n    [validatedRequest],\n    async (request, response) => {\n      try {\n        if (response.locals.multiUserMode)\n          return response.sendStatus(401).end();\n        const { id } = request.params;\n        if (!id || isNaN(Number(id))) return response.sendStatus(400).end();\n\n        await ApiKey.delete({ id: Number(id) });\n        await EventLogs.logEvent(\n          \"api_key_deleted\",\n          { deletedBy: response.locals?.user?.username },\n          response?.locals?.user?.id\n        );\n        return response.status(200).end();\n      } catch (error) {\n        console.error(error);\n        response.status(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/system/custom-models\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { provider, apiKey = null, basePath = null } = reqBody(request);\n        const { models, error } = await getCustomModels(\n          provider,\n          apiKey,\n          basePath\n        );\n        return response.status(200).json({\n          models,\n          error,\n        });\n      } catch (error) {\n        console.error(error);\n        response.status(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/system/event-logs\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { offset = 0, limit = 10 } = reqBody(request);\n        const logs = await EventLogs.whereWithData({}, limit, offset * limit, {\n          id: \"desc\",\n        });\n        const totalLogs = await EventLogs.count();\n        const hasPages = totalLogs > (offset + 1) * limit;\n\n        response.status(200).json({ logs: logs, hasPages, totalLogs });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/system/event-logs\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (_, response) => {\n      try {\n        await EventLogs.delete();\n        await EventLogs.logEvent(\n          \"event_logs_cleared\",\n          {},\n          response?.locals?.user?.id\n        );\n        response.json({ success: true });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/system/workspace-chats\",\n    [\n      chatHistoryViewable,\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin, ROLES.manager]),\n    ],\n    async (request, response) => {\n      try {\n        const { offset = 0, limit = 20 } = reqBody(request);\n        const chats = await WorkspaceChats.whereWithData(\n          {},\n          limit,\n          offset * limit,\n          { id: \"desc\" }\n        );\n        const totalChats = await WorkspaceChats.count();\n        const hasPages = totalChats > (offset + 1) * limit;\n\n        response.status(200).json({ chats: chats, hasPages, totalChats });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/system/workspace-chats/:id\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { id } = request.params;\n        Number(id) === -1\n          ? await WorkspaceChats.delete({}, true)\n          : await WorkspaceChats.delete({ id: Number(id) });\n        response.json({ success: true, error: null });\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/system/export-chats\",\n    [\n      chatHistoryViewable,\n      validatedRequest,\n      flexUserRoleValid([ROLES.manager, ROLES.admin]),\n    ],\n    async (request, response) => {\n      try {\n        const { type = \"jsonl\", chatType = \"workspace\" } = request.query;\n        const { contentType, data } = await exportChatsAsType(type, chatType);\n        await EventLogs.logEvent(\n          \"exported_chats\",\n          {\n            type,\n            chatType,\n          },\n          response.locals.user?.id\n        );\n        response.setHeader(\"Content-Type\", contentType);\n        response.status(200).send(data);\n      } catch (e) {\n        console.error(e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  // Used for when a user in multi-user updates their own profile\n  // from the UI.\n  app.post(\"/system/user\", [validatedRequest], async (request, response) => {\n    try {\n      const sessionUser = await userFromSession(request, response);\n      const { username, password, bio } = reqBody(request);\n      const id = Number(sessionUser.id);\n\n      if (!id) {\n        response.status(400).json({ success: false, error: \"Invalid user ID\" });\n        return;\n      }\n\n      const updates = {};\n      // If the username is being changed, validate it.\n      // Otherwise, do not attempt to validate it to allow existing users to keep their username if not changing it.\n      if (username !== sessionUser.username)\n        updates.username = User.validations.username(String(username));\n      if (password) updates.password = String(password);\n      if (bio) updates.bio = String(bio);\n\n      if (Object.keys(updates).length === 0) {\n        response\n          .status(400)\n          .json({ success: false, error: \"No updates provided\" });\n        return;\n      }\n\n      const { success, error } = await User.update(id, updates);\n      response.status(200).json({ success, error });\n    } catch (e) {\n      console.error(e);\n      response\n        .status(500)\n        .json({ success: false, error: e.message || \"Internal server error\" });\n    }\n  });\n\n  app.get(\n    \"/system/slash-command-presets\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const userPresets = await SlashCommandPresets.getUserPresets(user?.id);\n        response.status(200).json({ presets: userPresets });\n      } catch (error) {\n        console.error(\"Error fetching slash command presets:\", error);\n        response.status(500).json({ message: \"Internal server error\" });\n      }\n    }\n  );\n\n  app.post(\n    \"/system/slash-command-presets\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const { command, prompt, description } = reqBody(request);\n        const formattedCommand = SlashCommandPresets.formatCommand(\n          String(command)\n        );\n\n        if (Object.keys(VALID_COMMANDS).includes(formattedCommand)) {\n          return response.status(400).json({\n            message:\n              \"Cannot create a preset with a command that matches a system command\",\n          });\n        }\n\n        const presetData = {\n          command: formattedCommand,\n          prompt: String(prompt),\n          description: String(description),\n        };\n\n        const preset = await SlashCommandPresets.create(user?.id, presetData);\n        if (!preset) {\n          return response\n            .status(500)\n            .json({ message: \"Failed to create preset\" });\n        }\n        response.status(201).json({ preset });\n      } catch (error) {\n        console.error(\"Error creating slash command preset:\", error);\n        response.status(500).json({ message: \"Internal server error\" });\n      }\n    }\n  );\n\n  app.post(\n    \"/system/slash-command-presets/:slashCommandId\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const { slashCommandId } = request.params;\n        const { command, prompt, description } = reqBody(request);\n        const formattedCommand = SlashCommandPresets.formatCommand(\n          String(command)\n        );\n\n        if (Object.keys(VALID_COMMANDS).includes(formattedCommand)) {\n          return response.status(400).json({\n            message:\n              \"Cannot update a preset to use a command that matches a system command\",\n          });\n        }\n\n        // Valid user running owns the preset if user session is valid.\n        const ownsPreset = await SlashCommandPresets.get({\n          userId: user?.id ?? null,\n          id: Number(slashCommandId),\n        });\n        if (!ownsPreset)\n          return response.status(404).json({ message: \"Preset not found\" });\n\n        const updates = {\n          command: formattedCommand,\n          prompt: String(prompt),\n          description: String(description),\n        };\n\n        const preset = await SlashCommandPresets.update(\n          Number(slashCommandId),\n          updates\n        );\n        if (!preset) return response.sendStatus(422);\n        response.status(200).json({ preset: { ...ownsPreset, ...updates } });\n      } catch (error) {\n        console.error(\"Error updating slash command preset:\", error);\n        response.status(500).json({ message: \"Internal server error\" });\n      }\n    }\n  );\n\n  app.delete(\n    \"/system/slash-command-presets/:slashCommandId\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async (request, response) => {\n      try {\n        const { slashCommandId } = request.params;\n        const user = await userFromSession(request, response);\n\n        // Valid user running owns the preset if user session is valid.\n        const ownsPreset = await SlashCommandPresets.get({\n          userId: user?.id ?? null,\n          id: Number(slashCommandId),\n        });\n        if (!ownsPreset)\n          return response\n            .status(403)\n            .json({ message: \"Failed to delete preset\" });\n\n        await SlashCommandPresets.delete(Number(slashCommandId));\n        response.sendStatus(204);\n      } catch (error) {\n        console.error(\"Error deleting slash command preset:\", error);\n        response.status(500).json({ message: \"Internal server error\" });\n      }\n    }\n  );\n\n  app.get(\n    \"/system/prompt-variables\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const variables = await SystemPromptVariables.getAll(user?.id);\n        response.status(200).json({ variables });\n      } catch (error) {\n        console.error(\"Error fetching system prompt variables:\", error);\n        response.status(500).json({\n          success: false,\n          error: `Failed to fetch system prompt variables: ${error.message}`,\n        });\n      }\n    }\n  );\n\n  app.post(\n    \"/system/prompt-variables\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const { key, value, description = null } = reqBody(request);\n\n        if (!key || !value) {\n          return response.status(400).json({\n            success: false,\n            error: \"Key and value are required\",\n          });\n        }\n\n        const variable = await SystemPromptVariables.create({\n          key,\n          value,\n          description,\n          userId: user?.id || null,\n        });\n\n        response.status(200).json({\n          success: true,\n          variable,\n        });\n      } catch (error) {\n        console.error(\"Error creating system prompt variable:\", error);\n        response.status(500).json({\n          success: false,\n          error: `Failed to create system prompt variable: ${error.message}`,\n        });\n      }\n    }\n  );\n\n  app.put(\n    \"/system/prompt-variables/:id\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { id } = request.params;\n        const { key, value, description = null } = reqBody(request);\n\n        if (!key || !value) {\n          return response.status(400).json({\n            success: false,\n            error: \"Key and value are required\",\n          });\n        }\n\n        const variable = await SystemPromptVariables.update(Number(id), {\n          key,\n          value,\n          description,\n        });\n\n        if (!variable) {\n          return response.status(404).json({\n            success: false,\n            error: \"Variable not found\",\n          });\n        }\n\n        response.status(200).json({\n          success: true,\n          variable,\n        });\n      } catch (error) {\n        console.error(\"Error updating system prompt variable:\", error);\n        response.status(500).json({\n          success: false,\n          error: `Failed to update system prompt variable: ${error.message}`,\n        });\n      }\n    }\n  );\n\n  app.delete(\n    \"/system/prompt-variables/:id\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { id } = request.params;\n        const success = await SystemPromptVariables.delete(Number(id));\n\n        if (!success) {\n          return response.status(404).json({\n            success: false,\n            error: \"System prompt variable not found or could not be deleted\",\n          });\n        }\n\n        response.status(200).json({\n          success: true,\n        });\n      } catch (error) {\n        console.error(\"Error deleting system prompt variable:\", error);\n        response.status(500).json({\n          success: false,\n          error: `Failed to delete system prompt variable: ${error.message}`,\n        });\n      }\n    }\n  );\n\n  app.post(\n    \"/system/validate-sql-connection\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      const { engine, connectionString } = reqBody(request);\n      try {\n        if (!engine || !connectionString) {\n          return response.status(400).json({\n            success: false,\n            error: \"Both engine and connection details are required.\",\n          });\n        }\n\n        const {\n          validateConnection,\n        } = require(\"../utils/agents/aibitat/plugins/sql-agent/SQLConnectors\");\n        const result = await validateConnection(engine, { connectionString });\n\n        if (!result.success) {\n          return response.status(200).json({\n            success: false,\n            error: `Unable to connect to ${engine}. Please verify your connection details.`,\n          });\n        }\n\n        response.status(200).json(result);\n      } catch (error) {\n        console.error(\"SQL validation error:\", error);\n        response.status(500).json({\n          success: false,\n          error: `Unable to connect to ${engine}. Please verify your connection details.`,\n        });\n      }\n    }\n  );\n}\n\nmodule.exports = { systemEndpoints };\n"
  },
  {
    "path": "server/endpoints/utils/dockerModelRunnerUtils.js",
    "content": "const { validatedRequest } = require(\"../../utils/middleware/validatedRequest\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../../utils/middleware/multiUserProtected\");\nconst { reqBody } = require(\"../../utils/http\");\nconst { safeJsonParse, decodeHtmlEntities } = require(\"../../utils/http\");\n\nfunction dockerModelRunnerUtilsEndpoints(app) {\n  if (!app) return;\n  const {\n    parseDockerModelRunnerEndpoint,\n  } = require(\"../../utils/AiProviders/dockerModelRunner\");\n\n  app.post(\n    \"/utils/dmr/download-model\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { modelId, basePath = \"\" } = reqBody(request);\n        const dmrUrl = new URL(\n          parseDockerModelRunnerEndpoint(\n            basePath ?? process.env.DOCKER_MODEL_RUNNER_BASE_PATH,\n            \"dmr\"\n          )\n        );\n        dmrUrl.pathname = \"/models/create\";\n        response.writeHead(200, {\n          \"Content-Type\": \"text/event-stream\",\n          \"Cache-Control\": \"no-cache\",\n          Connection: \"keep-alive\",\n        });\n\n        const dmrResponse = await fetch(dmrUrl.toString(), {\n          method: \"POST\",\n          headers: {\n            \"Content-Type\": \"application/json\",\n          },\n          body: JSON.stringify({ from: String(modelId) }),\n        });\n        if (!dmrResponse.ok)\n          throw new Error(\n            dmrResponse.statusText ||\n              \"An error occurred while downloading the model\"\n          );\n        const reader = dmrResponse.body.getReader();\n        let done = false;\n        while (!done) {\n          const { value, done: readerDone } = await reader.read();\n          if (readerDone) done = true;\n\n          const chunk = new TextDecoder(\"utf-8\").decode(value);\n          const lines = chunk.split(\"\\n\");\n          for (const line of lines) {\n            if (!line.trim()) continue;\n            const decodedLine = decodeHtmlEntities(line);\n            const data = safeJsonParse(decodedLine);\n            if (!data) continue;\n\n            if (data.type === \"error\") {\n              throw new Error(\n                data.message || \"An error occurred while downloading the model\"\n              );\n            } else if (data.type === \"success\") {\n              response.write(\n                `data: ${JSON.stringify({ type: \"success\", percentage: 100, message: \"Model downloaded successfully\" })}\\n\\n`\n              );\n              done = true;\n            } else if (data.type === \"progress\") {\n              const percentage =\n                data.total > 0\n                  ? Math.round((data.pulled / data.total) * 100)\n                  : 0;\n              response.write(\n                `data: ${JSON.stringify({ type: \"progress\", percentage, message: data.message })}\\n\\n`\n              );\n            }\n          }\n        }\n      } catch (e) {\n        console.error(e);\n        response.write(\n          `data: ${JSON.stringify({ type: \"error\", message: e.message })}\\n\\n`\n        );\n      } finally {\n        response.end();\n      }\n    }\n  );\n}\n\nmodule.exports = {\n  dockerModelRunnerUtilsEndpoints,\n};\n"
  },
  {
    "path": "server/endpoints/utils/lemonadeUtilsEndpoints.js",
    "content": "const { validatedRequest } = require(\"../../utils/middleware/validatedRequest\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../../utils/middleware/multiUserProtected\");\nconst { reqBody } = require(\"../../utils/http\");\nconst { safeJsonParse, decodeHtmlEntities } = require(\"../../utils/http\");\nconst {\n  parseLemonadeServerEndpoint,\n} = require(\"../../utils/AiProviders/lemonade\");\n\nfunction lemonadeUtilsEndpoints(app) {\n  if (!app) return;\n\n  app.post(\n    \"/utils/lemonade/download-model\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { modelId, basePath = \"\" } = reqBody(request);\n        const lemonadeUrl = new URL(\n          parseLemonadeServerEndpoint(\n            basePath ?? process.env.LEMONADE_LLM_BASE_PATH,\n            \"base\"\n          )\n        );\n        lemonadeUrl.pathname += \"api/v1/pull\";\n        response.writeHead(200, {\n          \"Content-Type\": \"text/event-stream\",\n          \"Cache-Control\": \"no-cache\",\n          Connection: \"keep-alive\",\n        });\n\n        const lemonadeResponse = await fetch(lemonadeUrl.toString(), {\n          method: \"POST\",\n          headers: {\n            \"Content-Type\": \"application/json\",\n          },\n          body: JSON.stringify({\n            model_name: String(modelId),\n            stream: true,\n          }),\n        });\n        if (!lemonadeResponse.ok)\n          throw new Error(\n            lemonadeResponse.statusText ||\n              \"An error occurred while downloading the model\"\n          );\n        const reader = lemonadeResponse.body.getReader();\n        let done = false;\n        let currentEvent = null;\n\n        while (!done) {\n          const { value, done: readerDone } = await reader.read();\n          if (readerDone) done = true;\n\n          const chunk = new TextDecoder(\"utf-8\").decode(value);\n          const lines = chunk.split(\"\\n\");\n          for (const line of lines) {\n            if (!line.trim()) continue;\n\n            if (line.startsWith(\"event:\")) {\n              currentEvent = line.replace(\"event:\", \"\").trim();\n              continue;\n            }\n\n            if (line.startsWith(\"data:\")) {\n              const jsonStr = line.replace(\"data:\", \"\").trim();\n              const decodedLine = decodeHtmlEntities(jsonStr);\n              const data = safeJsonParse(decodedLine);\n              if (!data) continue;\n\n              if (currentEvent === \"error\") {\n                throw new Error(\n                  data.message ||\n                    \"An error occurred while downloading the model\"\n                );\n              } else if (currentEvent === \"complete\") {\n                response.write(\n                  `data: ${JSON.stringify({ type: \"success\", percentage: 100, message: \"Model downloaded successfully\" })}\\n\\n`\n                );\n                done = true;\n              } else if (currentEvent === \"progress\") {\n                const percentage = data.percent ?? 0;\n                const message = data.file\n                  ? `Downloading ${data.file}`\n                  : \"Downloading model...\";\n                response.write(\n                  `data: ${JSON.stringify({ type: \"progress\", percentage, message })}\\n\\n`\n                );\n              }\n\n              currentEvent = null;\n            }\n          }\n        }\n      } catch (e) {\n        console.error(e);\n        response.write(\n          `data: ${JSON.stringify({ type: \"error\", message: e.message })}\\n\\n`\n        );\n      } finally {\n        response.end();\n      }\n    }\n  );\n\n  app.post(\n    \"/utils/lemonade/delete-model\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin])],\n    async (request, response) => {\n      try {\n        const { modelId, basePath = \"\" } = reqBody(request);\n        if (!modelId) {\n          return response.status(400).json({\n            success: false,\n            error: \"modelId is required\",\n          });\n        }\n\n        const lemonadeUrl = new URL(\n          parseLemonadeServerEndpoint(\n            basePath ?? process.env.LEMONADE_LLM_BASE_PATH,\n            \"base\"\n          )\n        );\n        lemonadeUrl.pathname += \"api/v1/delete\";\n\n        const lemonadeResponse = await fetch(lemonadeUrl.toString(), {\n          method: \"POST\",\n          headers: {\n            \"Content-Type\": \"application/json\",\n          },\n          body: JSON.stringify({\n            model_name: String(modelId),\n          }),\n        });\n\n        const data = await lemonadeResponse.json();\n        if (!lemonadeResponse.ok || data.status === \"error\") {\n          return response.status(lemonadeResponse.status || 500).json({\n            success: false,\n            error: data.message || \"An error occurred while deleting the model\",\n          });\n        }\n\n        return response.status(200).json({\n          success: true,\n          message: data.message || `Deleted model: ${modelId}`,\n        });\n      } catch (e) {\n        console.error(e);\n        return response.status(500).json({\n          success: false,\n          error: e.message || \"An error occurred while deleting the model\",\n        });\n      }\n    }\n  );\n}\n\nmodule.exports = {\n  lemonadeUtilsEndpoints,\n};\n"
  },
  {
    "path": "server/endpoints/utils.js",
    "content": "const { SystemSettings } = require(\"../models/systemSettings\");\n\nfunction utilEndpoints(app) {\n  if (!app) return;\n\n  app.get(\"/utils/metrics\", async (_, response) => {\n    try {\n      const metrics = {\n        online: true,\n        version: getGitVersion(),\n        mode: (await SystemSettings.isMultiUserMode())\n          ? \"multi-user\"\n          : \"single-user\",\n        vectorDB: process.env.VECTOR_DB || \"lancedb\",\n        storage: await getDiskStorage(),\n        appVersion: getDeploymentVersion(),\n      };\n      response.status(200).json(metrics);\n    } catch (e) {\n      console.error(e);\n      response.sendStatus(500).end();\n    }\n  });\n\n  const {\n    dockerModelRunnerUtilsEndpoints,\n  } = require(\"./utils/dockerModelRunnerUtils\");\n  dockerModelRunnerUtilsEndpoints(app);\n\n  const { lemonadeUtilsEndpoints } = require(\"./utils/lemonadeUtilsEndpoints\");\n  lemonadeUtilsEndpoints(app);\n}\n\nfunction getGitVersion() {\n  if (process.env.ANYTHING_LLM_RUNTIME === \"docker\") return \"--\";\n  try {\n    return require(\"child_process\")\n      .execSync(\"git rev-parse HEAD\")\n      .toString()\n      .trim();\n  } catch (e) {\n    console.error(\"getGitVersion\", e.message);\n    return \"--\";\n  }\n}\n\nfunction byteToGigaByte(n) {\n  return n / Math.pow(10, 9);\n}\n\nasync function getDiskStorage() {\n  try {\n    const checkDiskSpace = require(\"check-disk-space\").default;\n    const { free, size } = await checkDiskSpace(\"/\");\n    return {\n      current: Math.floor(byteToGigaByte(free)),\n      capacity: Math.floor(byteToGigaByte(size)),\n    };\n  } catch {\n    return {\n      current: null,\n      capacity: null,\n    };\n  }\n}\n\n/**\n * Returns the model tag based on the provider set in the environment.\n * This information is used to identify the parent model for the system\n * so that we can prioritize the correct model and types for future updates\n * as well as build features in AnythingLLM directly for a specific model or capabilities.\n *\n * Disable with  {@link https://github.com/Mintplex-Labs/anything-llm?tab=readme-ov-file#telemetry--privacy|Disable Telemetry}\n * @returns {string} The model tag.\n */\nfunction getModelTag() {\n  let model = null;\n  const provider = process.env.LLM_PROVIDER;\n\n  switch (provider) {\n    case \"openai\":\n      model = process.env.OPEN_MODEL_PREF;\n      break;\n    case \"anthropic\":\n      model = process.env.ANTHROPIC_MODEL_PREF;\n      break;\n    case \"lmstudio\":\n      model = process.env.LMSTUDIO_MODEL_PREF;\n      break;\n    case \"ollama\":\n      model = process.env.OLLAMA_MODEL_PREF;\n      break;\n    case \"groq\":\n      model = process.env.GROQ_MODEL_PREF;\n      break;\n    case \"togetherai\":\n      model = process.env.TOGETHER_AI_MODEL_PREF;\n      break;\n    case \"azure\":\n      model =\n        process.env.AZURE_OPENAI_MODEL_PREF || process.env.OPEN_MODEL_PREF;\n      break;\n    case \"koboldcpp\":\n      model = process.env.KOBOLD_CPP_MODEL_PREF;\n      break;\n    case \"localai\":\n      model = process.env.LOCAL_AI_MODEL_PREF;\n      break;\n    case \"openrouter\":\n      model = process.env.OPENROUTER_MODEL_PREF;\n      break;\n    case \"mistral\":\n      model = process.env.MISTRAL_MODEL_PREF;\n      break;\n    case \"generic-openai\":\n      model = process.env.GENERIC_OPEN_AI_MODEL_PREF;\n      break;\n    case \"perplexity\":\n      model = process.env.PERPLEXITY_MODEL_PREF;\n      break;\n    case \"textgenwebui\":\n      model = \"textgenwebui-default\";\n      break;\n    case \"bedrock\":\n      model = process.env.AWS_BEDROCK_LLM_MODEL_PREFERENCE;\n      break;\n    case \"fireworksai\":\n      model = process.env.FIREWORKS_AI_LLM_MODEL_PREF;\n      break;\n    case \"deepseek\":\n      model = process.env.DEEPSEEK_MODEL_PREF;\n      break;\n    case \"litellm\":\n      model = process.env.LITE_LLM_MODEL_PREF;\n      break;\n    case \"apipie\":\n      model = process.env.APIPIE_LLM_MODEL_PREF;\n      break;\n    case \"xai\":\n      model = process.env.XAI_LLM_MODEL_PREF;\n      break;\n    case \"novita\":\n      model = process.env.NOVITA_LLM_MODEL_PREF;\n      break;\n    case \"nvidia-nim\":\n      model = process.env.NVIDIA_NIM_LLM_MODEL_PREF;\n      break;\n    case \"ppio\":\n      model = process.env.PPIO_MODEL_PREF;\n      break;\n    case \"gemini\":\n      model = process.env.GEMINI_LLM_MODEL_PREF;\n      break;\n    case \"moonshotai\":\n      model = process.env.MOONSHOT_AI_MODEL_PREF;\n      break;\n    case \"zai\":\n      model = process.env.ZAI_MODEL_PREF;\n      break;\n    case \"giteeai\":\n      model = process.env.GITEE_AI_MODEL_PREF;\n      break;\n    case \"cohere\":\n      model = process.env.COHERE_MODEL_PREF;\n      break;\n    case \"docker-model-runner\":\n      model = process.env.DOCKER_MODEL_RUNNER_LLM_MODEL_PREF;\n      break;\n    case \"privatemode\":\n      model = process.env.PRIVATEMODE_LLM_MODEL_PREF;\n      break;\n    case \"sambanova\":\n      model = process.env.SAMBANOVA_LLM_MODEL_PREF;\n      break;\n    case \"lemonade\":\n      model = process.env.LEMONADE_LLM_MODEL_PREF;\n      break;\n    default:\n      model = \"--\";\n      break;\n  }\n  return model;\n}\n\n/**\n * Returns the deployment version.\n * - Dev: reads from package.json\n * - Prod: reads from ENV\n * expected format: major.minor.patch\n * @returns {string|null} The deployment version.\n */\nfunction getDeploymentVersion() {\n  if (process.env.NODE_ENV === \"development\")\n    return require(\"../../package.json\").version;\n  if (process.env.DEPLOYMENT_VERSION) return process.env.DEPLOYMENT_VERSION;\n  return null;\n}\n\n/**\n * Returns the user agent for the AnythingLLM deployment.\n * @returns {string} The user agent.\n */\nfunction getAnythingLLMUserAgent() {\n  const version = getDeploymentVersion() || \"unknown\";\n  return `AnythingLLM/${version}`;\n}\n\nmodule.exports = {\n  utilEndpoints,\n  getGitVersion,\n  getModelTag,\n  getAnythingLLMUserAgent,\n};\n"
  },
  {
    "path": "server/endpoints/webPush.js",
    "content": "const { reqBody } = require(\"../utils/http\");\nconst { validatedRequest } = require(\"../utils/middleware/validatedRequest\");\nconst { pushNotificationService } = require(\"../utils/PushNotifications\");\n\nfunction webPushEndpoints(app) {\n  if (!app) return;\n\n  app.post(\n    \"/web-push/subscribe\",\n    [validatedRequest],\n    async (request, response) => {\n      const subscription = reqBody(request);\n      await pushNotificationService.registerSubscription(\n        response.locals.user,\n        subscription\n      );\n      response.status(201).json({});\n    }\n  );\n\n  app.get(\"/web-push/pubkey\", [validatedRequest], (_request, response) => {\n    const publicKey = pushNotificationService.publicVapidKey;\n    response.status(200).json({ publicKey });\n  });\n}\n\nmodule.exports = { webPushEndpoints };\n"
  },
  {
    "path": "server/endpoints/workspaceThreads.js",
    "content": "const {\n  multiUserMode,\n  userFromSession,\n  reqBody,\n  safeJsonParse,\n} = require(\"../utils/http\");\nconst { validatedRequest } = require(\"../utils/middleware/validatedRequest\");\nconst { Telemetry } = require(\"../models/telemetry\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../utils/middleware/multiUserProtected\");\nconst { EventLogs } = require(\"../models/eventLogs\");\nconst { WorkspaceThread } = require(\"../models/workspaceThread\");\nconst {\n  validWorkspaceSlug,\n  validWorkspaceAndThreadSlug,\n} = require(\"../utils/middleware/validWorkspace\");\nconst { WorkspaceChats } = require(\"../models/workspaceChats\");\nconst { convertToChatHistory } = require(\"../utils/helpers/chat/responses\");\nconst { getModelTag } = require(\"./utils\");\n\nfunction workspaceThreadEndpoints(app) {\n  if (!app) return;\n\n  app.post(\n    \"/workspace/:slug/thread/new\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n        const { thread, message } = await WorkspaceThread.new(\n          workspace,\n          user?.id\n        );\n        await Telemetry.sendTelemetry(\n          \"workspace_thread_created\",\n          {\n            multiUserMode: multiUserMode(response),\n            LLMSelection: process.env.LLM_PROVIDER || \"openai\",\n            Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n            VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n            TTSSelection: process.env.TTS_PROVIDER || \"native\",\n            LLMModel: getModelTag(),\n          },\n          user?.id\n        );\n\n        await EventLogs.logEvent(\n          \"workspace_thread_created\",\n          {\n            workspaceName: workspace?.name || \"Unknown Workspace\",\n          },\n          user?.id\n        );\n        response.status(200).json({ thread, message });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/workspace/:slug/threads\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n        const threads = await WorkspaceThread.where({\n          workspace_id: workspace.id,\n          user_id: user?.id || null,\n        });\n        response.status(200).json({ threads });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/workspace/:slug/thread/:threadSlug\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.all]),\n      validWorkspaceAndThreadSlug,\n    ],\n    async (_, response) => {\n      try {\n        const thread = response.locals.thread;\n        await WorkspaceThread.delete({ id: thread.id });\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/workspace/:slug/thread-bulk-delete\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async (request, response) => {\n      try {\n        const { slugs = [] } = reqBody(request);\n        if (slugs.length === 0) return response.sendStatus(200).end();\n\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n        await WorkspaceThread.delete({\n          slug: { in: slugs },\n          user_id: user?.id ?? null,\n          workspace_id: workspace.id,\n        });\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/workspace/:slug/thread/:threadSlug/chats\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.all]),\n      validWorkspaceAndThreadSlug,\n    ],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n        const thread = response.locals.thread;\n        const history = await WorkspaceChats.where(\n          {\n            workspaceId: workspace.id,\n            user_id: user?.id || null,\n            thread_id: thread.id,\n            api_session_id: null, // Do not include API session chats.\n            include: true,\n          },\n          null,\n          { id: \"asc\" }\n        );\n\n        response.status(200).json({ history: convertToChatHistory(history) });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/thread/:threadSlug/update\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.all]),\n      validWorkspaceAndThreadSlug,\n    ],\n    async (request, response) => {\n      try {\n        const data = reqBody(request);\n        const currentThread = response.locals.thread;\n        const { thread, message } = await WorkspaceThread.update(\n          currentThread,\n          data\n        );\n        response.status(200).json({ thread, message });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/workspace/:slug/thread/:threadSlug/delete-edited-chats\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.all]),\n      validWorkspaceAndThreadSlug,\n    ],\n    async (request, response) => {\n      try {\n        const { startingId } = reqBody(request);\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n        const thread = response.locals.thread;\n\n        await WorkspaceChats.delete({\n          workspaceId: Number(workspace.id),\n          thread_id: Number(thread.id),\n          user_id: user?.id,\n          id: { gte: Number(startingId) },\n        });\n\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/thread/:threadSlug/update-chat\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.all]),\n      validWorkspaceAndThreadSlug,\n    ],\n    async (request, response) => {\n      try {\n        const { chatId, newText = null, role = \"assistant\" } = reqBody(request);\n        if (!newText || !String(newText).trim())\n          throw new Error(\"Cannot save empty edit\");\n\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n        const thread = response.locals.thread;\n        const existingChat = await WorkspaceChats.get({\n          workspaceId: workspace.id,\n          thread_id: thread.id,\n          user_id: user?.id,\n          id: Number(chatId),\n        });\n        if (!existingChat) throw new Error(\"Invalid chat.\");\n\n        if (role === \"user\") {\n          await WorkspaceChats._update(existingChat.id, {\n            prompt: String(newText),\n          });\n        } else {\n          const chatResponse = safeJsonParse(existingChat.response, null);\n          if (!chatResponse) throw new Error(\"Failed to parse chat response\");\n          await WorkspaceChats._update(existingChat.id, {\n            response: JSON.stringify({\n              ...chatResponse,\n              text: String(newText),\n            }),\n          });\n        }\n\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { workspaceThreadEndpoints };\n"
  },
  {
    "path": "server/endpoints/workspaces.js",
    "content": "const path = require(\"path\");\nconst fs = require(\"fs\");\nconst {\n  reqBody,\n  multiUserMode,\n  userFromSession,\n  safeJsonParse,\n} = require(\"../utils/http\");\nconst { normalizePath, isWithin } = require(\"../utils/files\");\nconst { Workspace } = require(\"../models/workspace\");\nconst { Document } = require(\"../models/documents\");\nconst { DocumentVectors } = require(\"../models/vectors\");\nconst { WorkspaceChats } = require(\"../models/workspaceChats\");\nconst { getVectorDbClass } = require(\"../utils/helpers\");\nconst { handleFileUpload, handlePfpUpload } = require(\"../utils/files/multer\");\nconst { validatedRequest } = require(\"../utils/middleware/validatedRequest\");\nconst { Telemetry } = require(\"../models/telemetry\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../utils/middleware/multiUserProtected\");\nconst { EventLogs } = require(\"../models/eventLogs\");\nconst {\n  WorkspaceSuggestedMessages,\n} = require(\"../models/workspacesSuggestedMessages\");\nconst { validWorkspaceSlug } = require(\"../utils/middleware/validWorkspace\");\nconst { convertToChatHistory } = require(\"../utils/helpers/chat/responses\");\nconst { CollectorApi } = require(\"../utils/collectorApi\");\nconst {\n  determineWorkspacePfpFilepath,\n  fetchPfp,\n} = require(\"../utils/files/pfp\");\nconst { getTTSProvider } = require(\"../utils/TextToSpeech\");\nconst { WorkspaceThread } = require(\"../models/workspaceThread\");\n\nconst truncate = require(\"truncate\");\nconst { purgeDocument } = require(\"../utils/files/purgeDocument\");\nconst { getModelTag } = require(\"./utils\");\nconst { searchWorkspaceAndThreads } = require(\"../utils/helpers/search\");\nconst { workspaceParsedFilesEndpoints } = require(\"./workspacesParsedFiles\");\n\nfunction workspaceEndpoints(app) {\n  if (!app) return;\n  const responseCache = new Map();\n\n  app.post(\n    \"/workspace/new\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const { name = null } = reqBody(request);\n        const { workspace, message } = await Workspace.new(name, user?.id);\n        await Telemetry.sendTelemetry(\n          \"workspace_created\",\n          {\n            multiUserMode: multiUserMode(response),\n            LLMSelection: process.env.LLM_PROVIDER || \"openai\",\n            Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n            VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n            TTSSelection: process.env.TTS_PROVIDER || \"native\",\n            LLMModel: getModelTag(),\n          },\n          user?.id\n        );\n\n        await EventLogs.logEvent(\n          \"workspace_created\",\n          {\n            workspaceName: workspace?.name || \"Unknown Workspace\",\n          },\n          user?.id\n        );\n        response.status(200).json({ workspace, message });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/update\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const { slug = null } = request.params;\n        const data = reqBody(request);\n        const currWorkspace = multiUserMode(response)\n          ? await Workspace.getWithUser(user, { slug })\n          : await Workspace.get({ slug });\n\n        if (!currWorkspace) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        await Workspace.trackChange(currWorkspace, data, user);\n        const { workspace, message } = await Workspace.update(\n          currWorkspace.id,\n          data\n        );\n        response.status(200).json({ workspace, message });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/upload\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin, ROLES.manager]),\n      handleFileUpload,\n    ],\n    async function (request, response) {\n      try {\n        const Collector = new CollectorApi();\n        const { originalname } = request.file;\n        const processingOnline = await Collector.online();\n\n        if (!processingOnline) {\n          response\n            .status(500)\n            .json({\n              success: false,\n              error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,\n            })\n            .end();\n          return;\n        }\n\n        const { success, reason } =\n          await Collector.processDocument(originalname);\n        if (!success) {\n          response.status(500).json({ success: false, error: reason }).end();\n          return;\n        }\n\n        Collector.log(\n          `Document ${originalname} uploaded processed and successfully. It is now available in documents.`\n        );\n        await Telemetry.sendTelemetry(\"document_uploaded\");\n        await EventLogs.logEvent(\n          \"document_uploaded\",\n          {\n            documentName: originalname,\n          },\n          response.locals?.user?.id\n        );\n        response.status(200).json({ success: true, error: null });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/upload-link\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const Collector = new CollectorApi();\n        const { link = \"\" } = reqBody(request);\n        const processingOnline = await Collector.online();\n\n        if (!processingOnline) {\n          response\n            .status(500)\n            .json({\n              success: false,\n              error: `Document processing API is not online. Link ${link} will not be processed automatically.`,\n            })\n            .end();\n          return;\n        }\n\n        const { success, reason } = await Collector.processLink(link);\n        if (!success) {\n          response.status(500).json({ success: false, error: reason }).end();\n          return;\n        }\n\n        Collector.log(\n          `Link ${link} uploaded processed and successfully. It is now available in documents.`\n        );\n        await Telemetry.sendTelemetry(\"link_uploaded\");\n        await EventLogs.logEvent(\n          \"link_uploaded\",\n          { link },\n          response.locals?.user?.id\n        );\n        response.status(200).json({ success: true, error: null });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/update-embeddings\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const { slug = null } = request.params;\n        const { adds = [], deletes = [] } = reqBody(request);\n        const currWorkspace = multiUserMode(response)\n          ? await Workspace.getWithUser(user, { slug })\n          : await Workspace.get({ slug });\n\n        if (!currWorkspace) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        await Document.removeDocuments(\n          currWorkspace,\n          deletes,\n          response.locals?.user?.id\n        );\n        const { failedToEmbed = [], errors = [] } = await Document.addDocuments(\n          currWorkspace,\n          adds,\n          response.locals?.user?.id\n        );\n        const updatedWorkspace = await Workspace.get({ id: currWorkspace.id });\n        response.status(200).json({\n          workspace: updatedWorkspace,\n          message:\n            failedToEmbed.length > 0\n              ? `${failedToEmbed.length} documents failed to add.\\n\\n${errors\n                  .map((msg) => `${msg}`)\n                  .join(\"\\n\\n\")}`\n              : null,\n        });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/workspace/:slug\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { slug = \"\" } = request.params;\n        const user = await userFromSession(request, response);\n        const VectorDb = getVectorDbClass();\n        const workspace = multiUserMode(response)\n          ? await Workspace.getWithUser(user, { slug })\n          : await Workspace.get({ slug });\n\n        if (!workspace) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        await WorkspaceChats.delete({ workspaceId: Number(workspace.id) });\n        await DocumentVectors.deleteForWorkspace(workspace.id);\n        await Document.delete({ workspaceId: Number(workspace.id) });\n        await Workspace.delete({ id: Number(workspace.id) });\n\n        await EventLogs.logEvent(\n          \"workspace_deleted\",\n          {\n            workspaceName: workspace?.name || \"Unknown Workspace\",\n          },\n          response.locals?.user?.id\n        );\n\n        try {\n          await VectorDb[\"delete-namespace\"]({ namespace: slug });\n        } catch (e) {\n          console.error(e.message);\n        }\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/workspace/:slug/reset-vector-db\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { slug = \"\" } = request.params;\n        const user = await userFromSession(request, response);\n        const VectorDb = getVectorDbClass();\n        const workspace = multiUserMode(response)\n          ? await Workspace.getWithUser(user, { slug })\n          : await Workspace.get({ slug });\n\n        if (!workspace) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        await DocumentVectors.deleteForWorkspace(workspace.id);\n        await Document.delete({ workspaceId: Number(workspace.id) });\n\n        await EventLogs.logEvent(\n          \"workspace_vectors_reset\",\n          {\n            workspaceName: workspace?.name || \"Unknown Workspace\",\n          },\n          response.locals?.user?.id\n        );\n\n        try {\n          await VectorDb[\"delete-namespace\"]({ namespace: slug });\n        } catch (e) {\n          console.error(e.message);\n        }\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/workspaces\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const workspaces = multiUserMode(response)\n          ? await Workspace.whereWithUser(user)\n          : await Workspace.where();\n\n        response.status(200).json({ workspaces });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/workspace/:slug\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async (request, response) => {\n      try {\n        const { slug } = request.params;\n        const user = await userFromSession(request, response);\n        const workspace = multiUserMode(response)\n          ? await Workspace.getWithUser(user, { slug })\n          : await Workspace.get({ slug });\n\n        response.status(200).json({ workspace });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/workspace/:slug/chats\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async (request, response) => {\n      try {\n        const { slug } = request.params;\n        const user = await userFromSession(request, response);\n        const workspace = multiUserMode(response)\n          ? await Workspace.getWithUser(user, { slug })\n          : await Workspace.get({ slug });\n\n        if (!workspace) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        const history = multiUserMode(response)\n          ? await WorkspaceChats.forWorkspaceByUser(workspace.id, user.id)\n          : await WorkspaceChats.forWorkspace(workspace.id);\n        response.status(200).json({ history: convertToChatHistory(history) });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/workspace/:slug/delete-chats\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async (request, response) => {\n      try {\n        const { chatIds = [] } = reqBody(request);\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n\n        if (!workspace || !Array.isArray(chatIds)) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        // This works for both workspace and threads.\n        // we simplify this by just looking at workspace<>user overlap\n        // since they are all on the same table.\n        await WorkspaceChats.delete({\n          id: { in: chatIds.map((id) => Number(id)) },\n          user_id: user?.id ?? null,\n          workspaceId: workspace.id,\n        });\n\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/workspace/:slug/delete-edited-chats\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async (request, response) => {\n      try {\n        const { startingId } = reqBody(request);\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n\n        await WorkspaceChats.delete({\n          workspaceId: workspace.id,\n          thread_id: null,\n          user_id: user?.id,\n          id: { gte: Number(startingId) },\n        });\n\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/update-chat\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async (request, response) => {\n      try {\n        const { chatId, newText = null, role = \"assistant\" } = reqBody(request);\n        if (!newText || !String(newText).trim())\n          throw new Error(\"Cannot save empty edit\");\n\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n        const existingChat = await WorkspaceChats.get({\n          workspaceId: workspace.id,\n          thread_id: null,\n          user_id: user?.id,\n          id: Number(chatId),\n        });\n        if (!existingChat) throw new Error(\"Invalid chat.\");\n\n        if (role === \"user\") {\n          await WorkspaceChats._update(existingChat.id, {\n            prompt: String(newText),\n          });\n        } else {\n          const chatResponse = safeJsonParse(existingChat.response, null);\n          if (!chatResponse) throw new Error(\"Failed to parse chat response\");\n          await WorkspaceChats._update(existingChat.id, {\n            response: JSON.stringify({\n              ...chatResponse,\n              text: String(newText),\n            }),\n          });\n        }\n\n        response.sendStatus(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/chat-feedback/:chatId\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async (request, response) => {\n      try {\n        const { chatId } = request.params;\n        const { feedback = null } = reqBody(request);\n        const user = await userFromSession(request, response);\n        const existingChat = await WorkspaceChats.get({\n          id: Number(chatId),\n          workspaceId: response.locals.workspace.id,\n          user_id: user?.id,\n        });\n\n        if (!existingChat) return response.status(404).json({ success: false });\n        await WorkspaceChats.updateFeedbackScore(chatId, feedback);\n        return response.status(200).json({ success: true });\n      } catch (error) {\n        console.error(\"Error updating chat feedback:\", error);\n        response.status(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/workspace/:slug/suggested-messages\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async function (request, response) {\n      try {\n        const { slug } = request.params;\n        const suggestedMessages =\n          await WorkspaceSuggestedMessages.getMessages(slug);\n        response.status(200).json({ success: true, suggestedMessages });\n      } catch (error) {\n        console.error(\"Error fetching suggested messages:\", error);\n        response\n          .status(500)\n          .json({ success: false, message: \"Internal server error\" });\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/suggested-messages\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async (request, response) => {\n      try {\n        const { messages = [] } = reqBody(request);\n        const { slug } = request.params;\n        if (!Array.isArray(messages)) {\n          return response.status(400).json({\n            success: false,\n            message: \"Invalid message format. Expected an array of messages.\",\n          });\n        }\n\n        await WorkspaceSuggestedMessages.saveAll(messages, slug);\n        return response.status(200).json({\n          success: true,\n          message: \"Suggested messages saved successfully.\",\n        });\n      } catch (error) {\n        console.error(\"Error processing the suggested messages:\", error);\n        response.status(500).json({\n          success: true,\n          message: \"Error saving the suggested messages.\",\n        });\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/update-pin\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin, ROLES.manager]),\n      validWorkspaceSlug,\n    ],\n    async (request, response) => {\n      try {\n        const { docPath, pinStatus = false } = reqBody(request);\n        const workspace = response.locals.workspace;\n\n        const document = await Document.get({\n          workspaceId: workspace.id,\n          docpath: docPath,\n        });\n        if (!document) return response.sendStatus(404).end();\n\n        await Document.update(document.id, { pinned: pinStatus });\n        return response.status(200).end();\n      } catch (error) {\n        console.error(\"Error processing the pin status update:\", error);\n        return response.status(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/workspace/:slug/tts/:chatId\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async function (request, response) {\n      try {\n        const { chatId } = request.params;\n        const workspace = response.locals.workspace;\n        const cacheKey = `${workspace.slug}:${chatId}`;\n        const wsChat = await WorkspaceChats.get({\n          id: Number(chatId),\n          workspaceId: workspace.id,\n        });\n\n        const cachedResponse = responseCache.get(cacheKey);\n        if (cachedResponse) {\n          response.writeHead(200, {\n            \"Content-Type\": cachedResponse.mime || \"audio/mpeg\",\n          });\n          response.end(cachedResponse.buffer);\n          return;\n        }\n\n        const text = safeJsonParse(wsChat.response, null)?.text;\n        if (!text) return response.sendStatus(204).end();\n\n        const TTSProvider = getTTSProvider();\n        const buffer = await TTSProvider.ttsBuffer(text);\n        if (buffer === null) return response.sendStatus(204).end();\n\n        responseCache.set(cacheKey, { buffer, mime: \"audio/mpeg\" });\n        response.writeHead(200, {\n          \"Content-Type\": \"audio/mpeg\",\n        });\n        response.end(buffer);\n        return;\n      } catch (error) {\n        console.error(\"Error processing the TTS request:\", error);\n        response.status(500).json({ message: \"TTS could not be completed\" });\n      }\n    }\n  );\n\n  app.get(\n    \"/workspace/:slug/pfp\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async function (request, response) {\n      try {\n        const { slug } = request.params;\n        const cachedResponse = responseCache.get(slug);\n\n        if (cachedResponse) {\n          response.writeHead(200, {\n            \"Content-Type\": cachedResponse.mime || \"image/png\",\n          });\n          response.end(cachedResponse.buffer);\n          return;\n        }\n\n        const pfpPath = await determineWorkspacePfpFilepath(slug);\n\n        if (!pfpPath) {\n          response.sendStatus(204).end();\n          return;\n        }\n\n        const { found, buffer, mime } = fetchPfp(pfpPath);\n        if (!found) {\n          response.sendStatus(204).end();\n          return;\n        }\n\n        responseCache.set(slug, { buffer, mime });\n\n        response.writeHead(200, {\n          \"Content-Type\": mime || \"image/png\",\n        });\n        response.end(buffer);\n        return;\n      } catch (error) {\n        console.error(\"Error processing the logo request:\", error);\n        response.status(500).json({ message: \"Internal server error\" });\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/upload-pfp\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin, ROLES.manager]),\n      handlePfpUpload,\n    ],\n    async function (request, response) {\n      try {\n        const { slug } = request.params;\n        const uploadedFileName = request.randomFileName;\n        if (!uploadedFileName) {\n          return response.status(400).json({ message: \"File upload failed.\" });\n        }\n\n        const workspaceRecord = await Workspace.get({\n          slug,\n        });\n\n        const oldPfpFilename = workspaceRecord.pfpFilename;\n        if (oldPfpFilename) {\n          const storagePath = path.join(__dirname, \"../storage/assets/pfp\");\n          const oldPfpPath = path.join(\n            storagePath,\n            normalizePath(workspaceRecord.pfpFilename)\n          );\n          if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))\n            throw new Error(\"Invalid path name\");\n          if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);\n        }\n\n        const { workspace, message } = await Workspace._update(\n          workspaceRecord.id,\n          {\n            pfpFilename: uploadedFileName,\n          }\n        );\n\n        return response.status(workspace ? 200 : 500).json({\n          message: workspace\n            ? \"Profile picture uploaded successfully.\"\n            : message,\n        });\n      } catch (error) {\n        console.error(\"Error processing the profile picture upload:\", error);\n        response.status(500).json({ message: \"Internal server error\" });\n      }\n    }\n  );\n\n  app.delete(\n    \"/workspace/:slug/remove-pfp\",\n    [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],\n    async function (request, response) {\n      try {\n        const { slug } = request.params;\n        const workspaceRecord = await Workspace.get({\n          slug,\n        });\n        const oldPfpFilename = workspaceRecord.pfpFilename;\n\n        if (oldPfpFilename) {\n          const storagePath = path.join(__dirname, \"../storage/assets/pfp\");\n          const oldPfpPath = path.join(\n            storagePath,\n            normalizePath(oldPfpFilename)\n          );\n          if (!isWithin(path.resolve(storagePath), path.resolve(oldPfpPath)))\n            throw new Error(\"Invalid path name\");\n          if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);\n        }\n\n        const { workspace, message } = await Workspace._update(\n          workspaceRecord.id,\n          {\n            pfpFilename: null,\n          }\n        );\n\n        // Clear the cache\n        responseCache.delete(slug);\n\n        return response.status(workspace ? 200 : 500).json({\n          message: workspace\n            ? \"Profile picture removed successfully.\"\n            : message,\n        });\n      } catch (error) {\n        console.error(\"Error processing the profile picture removal:\", error);\n        response.status(500).json({ message: \"Internal server error\" });\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/thread/fork\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async (request, response) => {\n      try {\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n        const { chatId, threadSlug } = reqBody(request);\n        if (!chatId)\n          return response.status(400).json({ message: \"chatId is required\" });\n\n        // Get threadId we are branching from if that request body is sent\n        // and is a valid thread slug.\n        const threadId = !!threadSlug\n          ? (\n              await WorkspaceThread.get({\n                slug: String(threadSlug),\n                workspace_id: workspace.id,\n              })\n            )?.id ?? null\n          : null;\n        const chatsToFork = await WorkspaceChats.where(\n          {\n            workspaceId: workspace.id,\n            user_id: user?.id,\n            include: true, // only duplicate visible chats\n            thread_id: threadId,\n            api_session_id: null, // Do not include API session chats.\n            id: { lte: Number(chatId) },\n          },\n          null,\n          { id: \"asc\" }\n        );\n\n        const { thread: newThread, message: threadError } =\n          await WorkspaceThread.new(workspace, user?.id);\n        if (threadError)\n          return response.status(500).json({ error: threadError });\n\n        let lastMessageText = \"\";\n        const chatsData = chatsToFork.map((chat) => {\n          const chatResponse = safeJsonParse(chat.response, {});\n          if (chatResponse?.text) lastMessageText = chatResponse.text;\n\n          return {\n            workspaceId: workspace.id,\n            prompt: chat.prompt,\n            response: JSON.stringify(chatResponse),\n            user_id: user?.id,\n            thread_id: newThread.id,\n          };\n        });\n        await WorkspaceChats.bulkCreate(chatsData);\n        await WorkspaceThread.update(newThread, {\n          name: !!lastMessageText\n            ? truncate(lastMessageText, 22)\n            : \"Forked Thread\",\n        });\n\n        await EventLogs.logEvent(\n          \"thread_forked\",\n          {\n            workspaceName: workspace?.name || \"Unknown Workspace\",\n            threadName: newThread.name,\n          },\n          user?.id\n        );\n        response.status(200).json({ newThreadSlug: newThread.slug });\n      } catch (e) {\n        console.error(e.message, e);\n        response.status(500).json({ message: \"Internal server error\" });\n      }\n    }\n  );\n\n  app.put(\n    \"/workspace/workspace-chats/:id\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async (request, response) => {\n      try {\n        const { id } = request.params;\n        const user = await userFromSession(request, response);\n        const validChat = await WorkspaceChats.get({\n          id: Number(id),\n          user_id: user?.id ?? null,\n        });\n        if (!validChat)\n          return response\n            .status(404)\n            .json({ success: false, error: \"Chat not found.\" });\n\n        await WorkspaceChats._update(validChat.id, { include: false });\n        response.json({ success: true, error: null });\n      } catch (e) {\n        console.error(e.message, e);\n        response.status(500).json({ success: false, error: \"Server error\" });\n      }\n    }\n  );\n\n  /** Handles the uploading and embedding in one-call by uploading via drag-and-drop in chat container. */\n  app.post(\n    \"/workspace/:slug/upload-and-embed\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin, ROLES.manager]),\n      handleFileUpload,\n    ],\n    async function (request, response) {\n      try {\n        const { slug = null } = request.params;\n        const user = await userFromSession(request, response);\n        const currWorkspace = multiUserMode(response)\n          ? await Workspace.getWithUser(user, { slug })\n          : await Workspace.get({ slug });\n\n        if (!currWorkspace) {\n          response.sendStatus(400).end();\n          return;\n        }\n\n        const Collector = new CollectorApi();\n        const { originalname } = request.file;\n        const processingOnline = await Collector.online();\n\n        if (!processingOnline) {\n          response\n            .status(500)\n            .json({\n              success: false,\n              error: `Document processing API is not online. Document ${originalname} will not be processed automatically.`,\n            })\n            .end();\n          return;\n        }\n\n        const { success, reason, documents } =\n          await Collector.processDocument(originalname);\n        if (!success || documents?.length === 0) {\n          response.status(500).json({ success: false, error: reason }).end();\n          return;\n        }\n\n        Collector.log(\n          `Document ${originalname} uploaded processed and successfully. It is now available in documents.`\n        );\n        await Telemetry.sendTelemetry(\"document_uploaded\");\n        await EventLogs.logEvent(\n          \"document_uploaded\",\n          {\n            documentName: originalname,\n          },\n          response.locals?.user?.id\n        );\n\n        const document = documents[0];\n        const { failedToEmbed = [], errors = [] } = await Document.addDocuments(\n          currWorkspace,\n          [document.location],\n          response.locals?.user?.id\n        );\n\n        if (failedToEmbed.length > 0)\n          return response\n            .status(200)\n            .json({ success: false, error: errors?.[0], document: null });\n\n        response.status(200).json({\n          success: true,\n          error: null,\n          document: { id: document.id, location: document.location },\n        });\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/workspace/:slug/remove-and-unembed\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin, ROLES.manager]),\n      handleFileUpload,\n    ],\n    async function (request, response) {\n      try {\n        const { slug = null } = request.params;\n        const body = reqBody(request);\n        const user = await userFromSession(request, response);\n        const currWorkspace = multiUserMode(response)\n          ? await Workspace.getWithUser(user, { slug })\n          : await Workspace.get({ slug });\n\n        if (!currWorkspace || !body.documentLocation)\n          return response.sendStatus(400).end();\n\n        // Will delete the document from the entire system + wil unembed it.\n        await purgeDocument(body.documentLocation);\n        response.status(200).end();\n      } catch (e) {\n        console.error(e.message, e);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/workspace/:slug/prompt-history\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async (_, response) => {\n      try {\n        response.status(200).json({\n          history: await Workspace.promptHistory({\n            workspaceId: response.locals.workspace.id,\n          }),\n        });\n      } catch (error) {\n        console.error(\"Error fetching prompt history:\", error);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/workspace/:slug/prompt-history\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin, ROLES.manager]),\n      validWorkspaceSlug,\n    ],\n    async (_, response) => {\n      try {\n        response.status(200).json({\n          success: await Workspace.deleteAllPromptHistory({\n            workspaceId: response.locals.workspace.id,\n          }),\n        });\n      } catch (error) {\n        console.error(\"Error clearing prompt history:\", error);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/workspace/prompt-history/:id\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.admin, ROLES.manager]),\n      validWorkspaceSlug,\n    ],\n    async (request, response) => {\n      try {\n        const { id } = request.params;\n        response.status(200).json({\n          success: await Workspace.deletePromptHistory({\n            workspaceId: response.locals.workspace.id,\n            id: Number(id),\n          }),\n        });\n      } catch (error) {\n        console.error(\"Error deleting prompt history:\", error);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  /**\n   * Searches for workspaces and threads by thread name or workspace name.\n   * Only returns assets owned by the user (if multi-user mode is enabled).\n   */\n  app.post(\n    \"/workspace/search\",\n    [validatedRequest, flexUserRoleValid([ROLES.all])],\n    async (request, response) => {\n      try {\n        const { searchTerm } = reqBody(request);\n        const searchResults = await searchWorkspaceAndThreads(\n          searchTerm,\n          response.locals?.user\n        );\n        response.status(200).json(searchResults);\n      } catch (error) {\n        console.error(\"Error searching for workspaces:\", error);\n        response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.get(\n    \"/workspace/:slug/is-agent-command-available\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async (_, response) => {\n      try {\n        response.status(200).json({\n          showAgentCommand: await Workspace.isAgentCommandAvailable(\n            response.locals.workspace\n          ),\n        });\n      } catch (error) {\n        console.error(\"Error checking if agent command is available:\", error);\n        response.status(500).json({ showAgentCommand: true });\n      }\n    }\n  );\n\n  // Parsed Files in separate endpoint just to keep the workspace endpoints clean\n  workspaceParsedFilesEndpoints(app);\n}\n\nmodule.exports = { workspaceEndpoints };\n"
  },
  {
    "path": "server/endpoints/workspacesParsedFiles.js",
    "content": "const { reqBody, multiUserMode, userFromSession } = require(\"../utils/http\");\nconst { handleFileUpload } = require(\"../utils/files/multer\");\nconst { validatedRequest } = require(\"../utils/middleware/validatedRequest\");\nconst { Telemetry } = require(\"../models/telemetry\");\nconst {\n  flexUserRoleValid,\n  ROLES,\n} = require(\"../utils/middleware/multiUserProtected\");\nconst { EventLogs } = require(\"../models/eventLogs\");\nconst { validWorkspaceSlug } = require(\"../utils/middleware/validWorkspace\");\nconst { CollectorApi } = require(\"../utils/collectorApi\");\nconst { WorkspaceThread } = require(\"../models/workspaceThread\");\nconst { WorkspaceParsedFiles } = require(\"../models/workspaceParsedFiles\");\n\nfunction workspaceParsedFilesEndpoints(app) {\n  if (!app) return;\n\n  app.get(\n    \"/workspace/:slug/parsed-files\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async (request, response) => {\n      try {\n        const threadSlug = request.query.threadSlug || null;\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n        const thread = threadSlug\n          ? await WorkspaceThread.get({ slug: String(threadSlug) })\n          : null;\n        const { files, contextWindow, currentContextTokenCount } =\n          await WorkspaceParsedFiles.getContextMetadataAndLimits(\n            workspace,\n            thread || null,\n            multiUserMode(response) ? user : null\n          );\n\n        return response\n          .status(200)\n          .json({ files, contextWindow, currentContextTokenCount });\n      } catch (e) {\n        console.error(e.message, e);\n        return response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.delete(\n    \"/workspace/:slug/delete-parsed-files\",\n    [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],\n    async function (request, response) {\n      try {\n        const { fileIds = [] } = reqBody(request);\n        if (!fileIds.length) return response.sendStatus(400).end();\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n        const success = await WorkspaceParsedFiles.delete({\n          id: {\n            in: fileIds.map((id) => parseInt(id)),\n          },\n          ...(user ? { userId: user.id } : {}),\n          workspaceId: workspace.id,\n        });\n        return response.status(success ? 200 : 403).end();\n      } catch (e) {\n        console.error(e.message, e);\n        return response.sendStatus(500).end();\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/embed-parsed-file/:fileId\",\n    [\n      validatedRequest,\n      // Embed is still an admin/manager only feature\n      flexUserRoleValid([ROLES.admin, ROLES.manager]),\n      validWorkspaceSlug,\n    ],\n    async function (request, response) {\n      const { fileId = null } = request.params;\n      try {\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n\n        if (!fileId) return response.sendStatus(400).end();\n        const { success, error, document } =\n          await WorkspaceParsedFiles.moveToDocumentsAndEmbed(\n            user,\n            fileId,\n            workspace\n          );\n\n        if (!success) {\n          return response.status(500).json({\n            success: false,\n            error: error || \"Failed to embed file\",\n          });\n        }\n\n        await Telemetry.sendTelemetry(\"document_embedded\");\n        await EventLogs.logEvent(\n          \"document_embedded\",\n          {\n            documentName: document?.name || \"unknown\",\n            workspaceId: workspace.id,\n          },\n          user?.id\n        );\n\n        return response.status(200).json({\n          success: true,\n          error: null,\n          document,\n        });\n      } catch (e) {\n        console.error(e.message, e);\n        return response.sendStatus(500).end();\n      } finally {\n        // eslint-disable-next-line\n        if (!fileId) return;\n        await WorkspaceParsedFiles.delete({ id: parseInt(fileId) });\n      }\n    }\n  );\n\n  app.post(\n    \"/workspace/:slug/parse\",\n    [\n      validatedRequest,\n      flexUserRoleValid([ROLES.all]),\n      handleFileUpload,\n      validWorkspaceSlug,\n    ],\n    async function (request, response) {\n      try {\n        const user = await userFromSession(request, response);\n        const workspace = response.locals.workspace;\n        const Collector = new CollectorApi();\n        const { originalname } = request.file;\n        const processingOnline = await Collector.online();\n\n        if (!processingOnline) {\n          return response.status(500).json({\n            success: false,\n            error: `Document processing API is not online. Document ${originalname} will not be parsed.`,\n          });\n        }\n\n        const { success, reason, documents } =\n          await Collector.parseDocument(originalname);\n        if (!success || !documents?.[0]) {\n          return response.status(500).json({\n            success: false,\n            error: reason || \"No document returned from collector\",\n          });\n        }\n\n        // Get thread ID if we have a slug\n        const { threadSlug = null } = reqBody(request);\n        const thread = threadSlug\n          ? await WorkspaceThread.get({\n              slug: String(threadSlug),\n              workspace_id: workspace.id,\n              user_id: user?.id || null,\n            })\n          : null;\n        const files = await Promise.all(\n          documents.map(async (doc) => {\n            const metadata = { ...doc };\n            // Strip out pageContent\n            delete metadata.pageContent;\n            const filename = `${originalname}-${doc.id}.json`;\n            const { file, error: dbError } = await WorkspaceParsedFiles.create({\n              filename,\n              workspaceId: workspace.id,\n              userId: user?.id || null,\n              threadId: thread?.id || null,\n              metadata: JSON.stringify(metadata),\n              tokenCountEstimate: doc.token_count_estimate || 0,\n            });\n\n            if (dbError) throw new Error(dbError);\n            return file;\n          })\n        );\n\n        Collector.log(`Document ${originalname} parsed successfully.`);\n        await EventLogs.logEvent(\n          \"document_uploaded_to_chat\",\n          {\n            documentName: originalname,\n            workspace: workspace.slug,\n            thread: thread?.name || null,\n          },\n          user?.id\n        );\n\n        return response.status(200).json({\n          success: true,\n          error: null,\n          files,\n        });\n      } catch (e) {\n        console.error(e.message, e);\n        return response.sendStatus(500).end();\n      }\n    }\n  );\n}\n\nmodule.exports = { workspaceParsedFilesEndpoints };\n"
  },
  {
    "path": "server/eslint.config.mjs",
    "content": "import js from \"@eslint/js\";\nimport globals from \"globals\";\nimport { defineConfig } from \"eslint/config\";\nimport pluginPrettier from \"eslint-plugin-prettier\";\nimport configPrettier from \"eslint-config-prettier\";\nimport unusedImports from \"eslint-plugin-unused-imports\";\n\nexport default defineConfig([\n  { ignores: [\"__tests__/**\", \"**/syncStaticLists.mjs\"] },\n  {\n    files: [\"**/*.{js,mjs,cjs}\"],\n    plugins: { js, prettier: pluginPrettier, \"unused-imports\": unusedImports },\n    extends: [\"js/recommended\"],\n    languageOptions: { globals: { ...globals.node, ...globals.browser } },\n    rules: {\n      ...configPrettier.rules,\n      \"prettier/prettier\": \"error\",\n      \"no-case-declarations\": \"off\",\n      \"no-prototype-builtins\": \"off\",\n      \"no-async-promise-executor\": \"off\",\n      \"no-extra-boolean-cast\": \"off\",\n      \"no-empty\": \"off\",\n      \"no-unused-private-class-members\": \"warn\",\n      \"no-unused-vars\": \"off\",\n      \"unused-imports/no-unused-imports\": \"error\",\n      \"unused-imports/no-unused-vars\": [\n        \"error\",\n        {\n          vars: \"all\",\n          varsIgnorePattern: \"^_\",\n          args: \"after-used\",\n          argsIgnorePattern: \"^_\",\n        },\n      ],\n    },\n  },\n  { files: [\"**/*.js\"], languageOptions: { sourceType: \"commonjs\" } },\n]);\n"
  },
  {
    "path": "server/index.js",
    "content": "process.env.NODE_ENV === \"development\"\n  ? require(\"dotenv\").config({ path: `.env.${process.env.NODE_ENV}` })\n  : require(\"dotenv\").config();\n\nrequire(\"./utils/logger\")();\nconst express = require(\"express\");\nconst bodyParser = require(\"body-parser\");\nconst cors = require(\"cors\");\nconst path = require(\"path\");\nconst { reqBody } = require(\"./utils/http\");\nconst { systemEndpoints } = require(\"./endpoints/system\");\nconst { workspaceEndpoints } = require(\"./endpoints/workspaces\");\nconst { chatEndpoints } = require(\"./endpoints/chat\");\nconst { embeddedEndpoints } = require(\"./endpoints/embed\");\nconst { embedManagementEndpoints } = require(\"./endpoints/embedManagement\");\nconst { getVectorDbClass } = require(\"./utils/helpers\");\nconst { adminEndpoints } = require(\"./endpoints/admin\");\nconst { inviteEndpoints } = require(\"./endpoints/invite\");\nconst { utilEndpoints } = require(\"./endpoints/utils\");\nconst { developerEndpoints } = require(\"./endpoints/api\");\nconst { extensionEndpoints } = require(\"./endpoints/extensions\");\nconst { bootHTTP, bootSSL } = require(\"./utils/boot\");\nconst { workspaceThreadEndpoints } = require(\"./endpoints/workspaceThreads\");\nconst { documentEndpoints } = require(\"./endpoints/document\");\nconst { agentWebsocket } = require(\"./endpoints/agentWebsocket\");\nconst { experimentalEndpoints } = require(\"./endpoints/experimental\");\nconst { browserExtensionEndpoints } = require(\"./endpoints/browserExtension\");\nconst { communityHubEndpoints } = require(\"./endpoints/communityHub\");\nconst { agentFlowEndpoints } = require(\"./endpoints/agentFlows\");\nconst { mcpServersEndpoints } = require(\"./endpoints/mcpServers\");\nconst { mobileEndpoints } = require(\"./endpoints/mobile\");\nconst { webPushEndpoints } = require(\"./endpoints/webPush\");\nconst { httpLogger } = require(\"./middleware/httpLogger\");\nconst app = express();\nconst apiRouter = express.Router();\nconst FILE_LIMIT = \"3GB\";\n\n// Only log HTTP requests in development mode and if the ENABLE_HTTP_LOGGER environment variable is set to true\nif (\n  process.env.NODE_ENV === \"development\" &&\n  !!process.env.ENABLE_HTTP_LOGGER\n) {\n  app.use(\n    httpLogger({\n      enableTimestamps: !!process.env.ENABLE_HTTP_LOGGER_TIMESTAMPS,\n    })\n  );\n}\napp.use(cors({ origin: true }));\napp.use(bodyParser.text({ limit: FILE_LIMIT }));\napp.use(bodyParser.json({ limit: FILE_LIMIT }));\napp.use(\n  bodyParser.urlencoded({\n    limit: FILE_LIMIT,\n    extended: true,\n  })\n);\n\nif (!!process.env.ENABLE_HTTPS) {\n  bootSSL(app, process.env.SERVER_PORT || 3001);\n} else {\n  require(\"@mintplex-labs/express-ws\").default(app); // load WebSockets in non-SSL mode.\n}\n\napp.use(\"/api\", apiRouter);\nsystemEndpoints(apiRouter);\nextensionEndpoints(apiRouter);\nworkspaceEndpoints(apiRouter);\nworkspaceThreadEndpoints(apiRouter);\nchatEndpoints(apiRouter);\nadminEndpoints(apiRouter);\ninviteEndpoints(apiRouter);\nembedManagementEndpoints(apiRouter);\nutilEndpoints(apiRouter);\ndocumentEndpoints(apiRouter);\nagentWebsocket(apiRouter);\nexperimentalEndpoints(apiRouter);\ndeveloperEndpoints(app, apiRouter);\ncommunityHubEndpoints(apiRouter);\nagentFlowEndpoints(apiRouter);\nmcpServersEndpoints(apiRouter);\nmobileEndpoints(apiRouter);\nwebPushEndpoints(apiRouter);\n// Externally facing embedder endpoints\nembeddedEndpoints(apiRouter);\n\n// Externally facing browser extension endpoints\nbrowserExtensionEndpoints(apiRouter);\n\nif (process.env.NODE_ENV !== \"development\") {\n  const { MetaGenerator } = require(\"./utils/boot/MetaGenerator\");\n  const IndexPage = new MetaGenerator();\n\n  app.use(\n    express.static(path.resolve(__dirname, \"public\"), {\n      extensions: [\"js\"],\n      setHeaders: (res) => {\n        // Disable I-framing of entire site UI\n        res.removeHeader(\"X-Powered-By\");\n        res.setHeader(\"X-Frame-Options\", \"DENY\");\n      },\n    })\n  );\n\n  app.get(\"/robots.txt\", function (_, response) {\n    response.type(\"text/plain\");\n    response.send(\"User-agent: *\\nDisallow: /\").end();\n  });\n\n  app.get(\"/manifest.json\", async function (_, response) {\n    IndexPage.generateManifest(response);\n    return;\n  });\n\n  app.use(\"/\", function (_, response) {\n    IndexPage.generate(response);\n    return;\n  });\n} else {\n  // Debug route for development connections to vectorDBs\n  apiRouter.post(\"/v/:command\", async (request, response) => {\n    try {\n      const VectorDb = getVectorDbClass();\n      const { command } = request.params;\n      if (!Object.getOwnPropertyNames(VectorDb).includes(command)) {\n        response.status(500).json({\n          message: \"invalid interface command\",\n          commands: Object.getOwnPropertyNames(VectorDb),\n        });\n        return;\n      }\n\n      try {\n        const body = reqBody(request);\n        const resBody = await VectorDb[command](body);\n        response.status(200).json({ ...resBody });\n      } catch (e) {\n        // console.error(e)\n        console.error(JSON.stringify(e));\n        response.status(500).json({ error: e.message });\n      }\n      return;\n    } catch (e) {\n      console.error(e.message, e);\n      response.sendStatus(500).end();\n    }\n  });\n}\n\napp.all(\"*\", function (_, response) {\n  response.sendStatus(404);\n});\n\n// In non-https mode we need to boot at the end since the server has not yet\n// started and is `.listen`ing.\nif (!process.env.ENABLE_HTTPS) bootHTTP(app, process.env.SERVER_PORT || 3001);\n"
  },
  {
    "path": "server/jobs/cleanup-orphan-documents.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { default: slugify } = require(\"slugify\");\nconst { log, conclude } = require(\"./helpers/index.js\");\nconst { WorkspaceParsedFiles } = require(\"../models/workspaceParsedFiles.js\");\nconst { directUploadsPath } = require(\"../utils/files\");\n\nasync function batchDeleteFiles(filesToDelete, batchSize = 500) {\n  let deletedCount = 0;\n  let failedCount = 0;\n\n  for (let i = 0; i < filesToDelete.length; i += batchSize) {\n    const batch = filesToDelete.slice(i, i + batchSize);\n\n    try {\n      await Promise.all(\n        batch.map((filePath) => {\n          if (!fs.existsSync(filePath)) return;\n          if (fs.lstatSync(filePath).isDirectory())\n            fs.rmSync(filePath, { recursive: true });\n          else fs.unlinkSync(filePath);\n        })\n      );\n      deletedCount += batch.length;\n\n      log(\n        `Deleted batch ${Math.floor(i / batchSize) + 1}: ${batch.length} files`\n      );\n    } catch {\n      // If batch fails, try individual files sync\n      for (const filePath of batch) {\n        try {\n          fs.unlinkSync(filePath);\n          deletedCount++;\n        } catch (fileErr) {\n          failedCount++;\n          log(`Failed to delete ${filePath}: ${fileErr.message}`);\n        }\n      }\n    }\n  }\n\n  return { deletedCount, failedCount };\n}\n\n(async () => {\n  try {\n    const filesToDelete = [];\n    const knownFiles = await WorkspaceParsedFiles.where({}, null, null, {\n      filename: true,\n    })\n      // Slugify the filename to match the direct uploads naming convention otherwise\n      // files with spaces will not result in a match and will be pruned when attached to a thread.\n      // This could then result in files showing \"Attached\" but the model not seeing them during chat.\n      .then((files) => new Set(files.map((f) => slugify(f.filename))));\n\n    if (!fs.existsSync(directUploadsPath))\n      return log(\"No direct uploads path found - exiting.\");\n    const filesInDirectUploadsPath = fs.readdirSync(directUploadsPath);\n    if (filesInDirectUploadsPath.length === 0) return;\n\n    for (let i = 0; i < filesInDirectUploadsPath.length; i++) {\n      const file = filesInDirectUploadsPath[i];\n      if (knownFiles.has(file)) continue;\n      filesToDelete.push(path.resolve(directUploadsPath, file));\n    }\n\n    if (filesToDelete.length === 0) return; // No orphaned files to delete\n    log(`Found ${filesToDelete.length} orphaned files to delete`);\n    const { deletedCount, failedCount } = await batchDeleteFiles(filesToDelete);\n    log(`Deleted ${deletedCount} orphaned files`);\n    if (failedCount > 0) log(`Failed to delete ${failedCount} files`);\n  } catch (e) {\n    console.error(e);\n    log(`errored with ${e.message}`);\n  } finally {\n    conclude();\n  }\n})();\n"
  },
  {
    "path": "server/jobs/helpers/index.js",
    "content": "const path = require(\"node:path\");\nconst fs = require(\"node:fs\");\nconst { parentPort } = require(\"node:worker_threads\");\nconst documentsPath =\n  process.env.NODE_ENV === \"development\"\n    ? path.resolve(__dirname, `../../storage/documents`)\n    : path.resolve(process.env.STORAGE_DIR, `documents`);\n\nfunction log(stringContent = \"\") {\n  if (parentPort)\n    parentPort.postMessage(`\\x1b[33m[${process.pid}]\\x1b[0m: ${stringContent}`); // running as worker\n  else\n    process.send(\n      `\\x1b[33m[${process.ppid}:${process.pid}]\\x1b[0m: ${stringContent}`\n    ); // running as child_process\n}\n\nfunction conclude() {\n  if (parentPort) parentPort.postMessage(\"done\");\n  else process.exit(0);\n}\n\nfunction updateSourceDocument(docPath = null, jsonContent = {}) {\n  const destinationFilePath = path.resolve(documentsPath, docPath);\n  fs.writeFileSync(destinationFilePath, JSON.stringify(jsonContent, null, 4), {\n    encoding: \"utf-8\",\n  });\n}\n\nmodule.exports = {\n  log,\n  conclude,\n  updateSourceDocument,\n};\n"
  },
  {
    "path": "server/jobs/sync-watched-documents.js",
    "content": "const { Document } = require(\"../models/documents.js\");\nconst { DocumentSyncQueue } = require(\"../models/documentSyncQueue.js\");\nconst { CollectorApi } = require(\"../utils/collectorApi\");\nconst { fileData } = require(\"../utils/files\");\nconst { log, conclude, updateSourceDocument } = require(\"./helpers/index.js\");\nconst { getVectorDbClass } = require(\"../utils/helpers/index.js\");\nconst { DocumentSyncRun } = require(\"../models/documentSyncRun.js\");\n\n(async () => {\n  try {\n    const queuesToProcess = await DocumentSyncQueue.staleDocumentQueues();\n    if (queuesToProcess.length === 0) {\n      log(\"No outstanding documents to sync. Exiting.\");\n      return;\n    }\n\n    const collector = new CollectorApi();\n    if (!(await collector.online())) {\n      log(\"Could not reach collector API. Exiting.\");\n      return;\n    }\n\n    log(\n      `${queuesToProcess.length} watched documents have been found to be stale and will be updated now.`\n    );\n    for (const queue of queuesToProcess) {\n      let newContent = null;\n      const document = queue.workspaceDoc;\n      const workspace = document.workspace;\n      const { metadata, type, source } =\n        Document.parseDocumentTypeAndSource(document);\n\n      if (!metadata || !DocumentSyncQueue.validFileTypes.includes(type)) {\n        // Document is either broken, invalid, or not supported so drop it from future queues.\n        log(\n          `Document ${document.filename} has no metadata, is broken, or invalid and has been removed from all future runs.`\n        );\n        await DocumentSyncQueue.unwatch(document);\n        continue;\n      }\n\n      if ([\"link\", \"youtube\"].includes(type)) {\n        const response = await collector.forwardExtensionRequest({\n          endpoint: \"/ext/resync-source-document\",\n          method: \"POST\",\n          body: JSON.stringify({\n            type,\n            options: { link: source },\n          }),\n        });\n        newContent = response?.content;\n      }\n\n      if ([\"confluence\", \"github\", \"gitlab\", \"drupalwiki\"].includes(type)) {\n        const response = await collector.forwardExtensionRequest({\n          endpoint: \"/ext/resync-source-document\",\n          method: \"POST\",\n          body: JSON.stringify({\n            type,\n            options: { chunkSource: metadata.chunkSource },\n          }),\n        });\n        newContent = response?.content;\n      }\n\n      if (!newContent) {\n        // Check if the last \"x\" runs were all failures (not exits!). If so - remove the job entirely since it is broken.\n        const failedRunCount = (\n          await DocumentSyncRun.where(\n            { queueId: queue.id },\n            DocumentSyncQueue.maxRepeatFailures,\n            { createdAt: \"desc\" }\n          )\n        ).filter(\n          (run) => run.status === DocumentSyncRun.statuses.failed\n        ).length;\n        if (failedRunCount >= DocumentSyncQueue.maxRepeatFailures) {\n          log(\n            `Document ${document.filename} has failed to refresh ${failedRunCount} times continuously and will now be removed from the watched document set.`\n          );\n          await DocumentSyncQueue.unwatch(document);\n          continue;\n        }\n\n        log(\n          `Failed to get a new content response from collector for source ${source}. Skipping, but will retry next worker interval. Attempt ${failedRunCount === 0 ? 1 : failedRunCount}/${DocumentSyncQueue.maxRepeatFailures}`\n        );\n        await DocumentSyncQueue.saveRun(\n          queue.id,\n          DocumentSyncRun.statuses.failed,\n          {\n            filename: document.filename,\n            workspacesModified: [],\n            reason: \"No content found.\",\n          }\n        );\n        continue;\n      }\n\n      const currentDocumentData = await fileData(document.docpath);\n      if (currentDocumentData.pageContent === newContent) {\n        const nextSync = DocumentSyncQueue.calcNextSync(queue);\n        log(\n          `Source ${source} is unchanged and will be skipped. Next sync will be ${nextSync.toLocaleString()}.`\n        );\n        await DocumentSyncQueue._update(queue.id, {\n          lastSyncedAt: new Date().toISOString(),\n          nextSyncAt: nextSync.toISOString(),\n        });\n        await DocumentSyncQueue.saveRun(\n          queue.id,\n          DocumentSyncRun.statuses.exited,\n          {\n            filename: document.filename,\n            workspacesModified: [],\n            reason: \"Content unchanged.\",\n          }\n        );\n        continue;\n      }\n\n      // update the defined document and workspace vectorDB with the latest information\n      // it will skip cache and create a new vectorCache file.\n      const vectorDatabase = getVectorDbClass();\n      await vectorDatabase.deleteDocumentFromNamespace(\n        workspace.slug,\n        document.docId\n      );\n      await vectorDatabase.addDocumentToNamespace(\n        workspace.slug,\n        {\n          ...currentDocumentData,\n          pageContent: newContent,\n          docId: document.docId,\n        },\n        document.docpath,\n        true\n      );\n      updateSourceDocument(document.docpath, {\n        ...currentDocumentData,\n        pageContent: newContent,\n        docId: document.docId,\n        published: new Date().toLocaleString(),\n        // Todo: Update word count and token_estimate?\n      });\n      log(\n        `Workspace \"${workspace.name}\" vectors of ${source} updated. Document and vector cache updated.`\n      );\n\n      // Now we can bloom the results to all matching documents in all other workspaces\n      const workspacesModified = [workspace.slug];\n      const moreReferences = await Document.where(\n        {\n          id: { not: document.id },\n          filename: document.filename,\n        },\n        null,\n        null,\n        { workspace: true }\n      );\n\n      if (moreReferences.length !== 0) {\n        log(\n          `${source} is referenced in ${moreReferences.length} other workspaces. Updating those workspaces as well...`\n        );\n        for (const additionalDocumentRef of moreReferences) {\n          const additionalWorkspace = additionalDocumentRef.workspace;\n          workspacesModified.push(additionalWorkspace.slug);\n\n          await vectorDatabase.deleteDocumentFromNamespace(\n            additionalWorkspace.slug,\n            additionalDocumentRef.docId\n          );\n          await vectorDatabase.addDocumentToNamespace(\n            additionalWorkspace.slug,\n            {\n              ...currentDocumentData,\n              pageContent: newContent,\n              docId: additionalDocumentRef.docId,\n            },\n            additionalDocumentRef.docpath\n          );\n          log(\n            `Workspace \"${additionalWorkspace.name}\" vectors for ${source} was also updated with the new content from cache.`\n          );\n        }\n      }\n\n      const nextRefresh = DocumentSyncQueue.calcNextSync(queue);\n      log(\n        `${source} has been refreshed in all workspaces it is currently referenced in. Next refresh will be ${nextRefresh.toLocaleString()}.`\n      );\n      await DocumentSyncQueue._update(queue.id, {\n        lastSyncedAt: new Date().toISOString(),\n        nextSyncAt: nextRefresh.toISOString(),\n      });\n      await DocumentSyncQueue.saveRun(\n        queue.id,\n        DocumentSyncRun.statuses.success,\n        { filename: document.filename, workspacesModified }\n      );\n    }\n  } catch (e) {\n    console.error(e);\n    log(`errored with ${e.message}`);\n  } finally {\n    conclude();\n  }\n})();\n"
  },
  {
    "path": "server/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"target\": \"ES2020\"\n  },\n  \"include\": [\n    \"./endpoints/**/*\",\n    \"./models/**/*\",\n    \"./utils/**/*\",\n    \"./swagger/**/*\",\n    \"index.js\"\n  ],\n  \"exclude\": [\"node_modules\", \"storage\"]\n}\n"
  },
  {
    "path": "server/middleware/httpLogger.js",
    "content": "const httpLogger =\n  ({ enableTimestamps = false }) =>\n  (req, res, next) => {\n    // Capture the original res.end to log response status\n    const originalEnd = res.end;\n\n    res.end = function (chunk, encoding) {\n      // Log the request method, status code, and path\n      const statusColor = res.statusCode >= 400 ? \"\\x1b[31m\" : \"\\x1b[32m\"; // Red for errors, green for success\n      console.log(\n        `\\x1b[32m[HTTP]\\x1b[0m ${statusColor}${res.statusCode}\\x1b[0m ${req.method} -> ${req.path} ${enableTimestamps ? `@ ${new Date().toLocaleTimeString(\"en-US\", { hour12: true })}` : \"\"}`.trim()\n      );\n\n      // Call the original end method\n      return originalEnd.call(this, chunk, encoding);\n    };\n\n    next();\n  };\n\nmodule.exports = {\n  httpLogger,\n};\n"
  },
  {
    "path": "server/models/apiKeys.js",
    "content": "const prisma = require(\"../utils/prisma\");\n\nconst ApiKey = {\n  tablename: \"api_keys\",\n  writable: [],\n\n  makeSecret: () => {\n    const uuidAPIKey = require(\"uuid-apikey\");\n    return uuidAPIKey.create().apiKey;\n  },\n\n  create: async function (createdByUserId = null) {\n    try {\n      const apiKey = await prisma.api_keys.create({\n        data: {\n          secret: this.makeSecret(),\n          createdBy: createdByUserId,\n        },\n      });\n\n      return { apiKey, error: null };\n    } catch (error) {\n      console.error(\"FAILED TO CREATE API KEY.\", error.message);\n      return { apiKey: null, error: error.message };\n    }\n  },\n\n  get: async function (clause = {}) {\n    try {\n      const apiKey = await prisma.api_keys.findFirst({ where: clause });\n      return apiKey;\n    } catch (error) {\n      console.error(\"FAILED TO GET API KEY.\", error.message);\n      return null;\n    }\n  },\n\n  count: async function (clause = {}) {\n    try {\n      const count = await prisma.api_keys.count({ where: clause });\n      return count;\n    } catch (error) {\n      console.error(\"FAILED TO COUNT API KEYS.\", error.message);\n      return 0;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.api_keys.deleteMany({ where: clause });\n      return true;\n    } catch (error) {\n      console.error(\"FAILED TO DELETE API KEY.\", error.message);\n      return false;\n    }\n  },\n\n  where: async function (clause = {}, limit) {\n    try {\n      const apiKeys = await prisma.api_keys.findMany({\n        where: clause,\n        take: limit,\n      });\n      return apiKeys;\n    } catch (error) {\n      console.error(\"FAILED TO GET API KEYS.\", error.message);\n      return [];\n    }\n  },\n\n  whereWithUser: async function (clause = {}, limit) {\n    try {\n      const { User } = require(\"./user\");\n      const apiKeys = await this.where(clause, limit);\n\n      for (const apiKey of apiKeys) {\n        if (!apiKey.createdBy) continue;\n        const user = await User.get({ id: apiKey.createdBy });\n        if (!user) continue;\n\n        apiKey.createdBy = {\n          id: user.id,\n          username: user.username,\n          role: user.role,\n        };\n      }\n\n      return apiKeys;\n    } catch (error) {\n      console.error(\"FAILED TO GET API KEYS WITH USER.\", error.message);\n      return [];\n    }\n  },\n};\n\nmodule.exports = { ApiKey };\n"
  },
  {
    "path": "server/models/browserExtensionApiKey.js",
    "content": "const prisma = require(\"../utils/prisma\");\nconst { SystemSettings } = require(\"./systemSettings\");\nconst { ROLES } = require(\"../utils/middleware/multiUserProtected\");\n\nconst BrowserExtensionApiKey = {\n  /**\n   * Creates a new secret for a browser extension API key.\n   * @returns {string} brx-*** API key to use with extension\n   */\n  makeSecret: () => {\n    const uuidAPIKey = require(\"uuid-apikey\");\n    return `brx-${uuidAPIKey.create().apiKey}`;\n  },\n\n  /**\n   * Creates a new api key for the browser Extension\n   * @param {number|null} userId - User id to associate creation of key with.\n   * @returns {Promise<{apiKey: import(\"@prisma/client\").browser_extension_api_keys|null, error:string|null}>}\n   */\n  create: async function (userId = null) {\n    try {\n      const apiKey = await prisma.browser_extension_api_keys.create({\n        data: {\n          key: this.makeSecret(),\n          user_id: userId,\n        },\n      });\n      return { apiKey, error: null };\n    } catch (error) {\n      console.error(\"Failed to create browser extension API key\", error);\n      return { apiKey: null, error: error.message };\n    }\n  },\n\n  /**\n   * Validated existing API key\n   * @param {string} key\n   * @returns {Promise<{apiKey: import(\"@prisma/client\").browser_extension_api_keys|boolean}>}\n   */\n  validate: async function (key) {\n    if (!key.startsWith(\"brx-\")) return false;\n    const apiKey = await prisma.browser_extension_api_keys.findUnique({\n      where: { key: key.toString() },\n      include: { user: true },\n    });\n    if (!apiKey) return false;\n\n    const multiUserMode = await SystemSettings.isMultiUserMode();\n    if (!multiUserMode) return apiKey; // In single-user mode, all keys are valid\n\n    // In multi-user mode, check if the key is associated with a user\n    return apiKey.user_id ? apiKey : false;\n  },\n\n  /**\n   * Fetches browser api key by params.\n   * @param {object} clause - Prisma props for search\n   * @returns {Promise<{apiKey: import(\"@prisma/client\").browser_extension_api_keys|boolean}>}\n   */\n  get: async function (clause = {}) {\n    try {\n      const apiKey = await prisma.browser_extension_api_keys.findFirst({\n        where: clause,\n      });\n      return apiKey;\n    } catch (error) {\n      console.error(\"FAILED TO GET BROWSER EXTENSION API KEY.\", error.message);\n      return null;\n    }\n  },\n\n  /**\n   * Deletes browser api key by db id.\n   * @param {number} id - database id of browser key\n   * @returns {Promise<{success: boolean, error:string|null}>}\n   */\n  delete: async function (id) {\n    try {\n      await prisma.browser_extension_api_keys.delete({\n        where: { id: parseInt(id) },\n      });\n      return { success: true, error: null };\n    } catch (error) {\n      console.error(\"Failed to delete browser extension API key\", error);\n      return { success: false, error: error.message };\n    }\n  },\n\n  /**\n   * Deletes all browser extension API keys for a user.\n   * Should be called when a user is deleted to revoke all their keys.\n   * @param {number} userId - The user ID whose keys should be deleted\n   * @returns {Promise<{success: boolean, error: string|null}>}\n   */\n  deleteAllForUser: async function (userId) {\n    try {\n      if (!userId) return { success: false, error: \"User ID is required\" };\n      await prisma.browser_extension_api_keys.deleteMany({\n        where: { user_id: parseInt(userId) },\n      });\n      return { success: true, error: null };\n    } catch (error) {\n      console.error(\n        \"Failed to delete browser extension API keys for user\",\n        error\n      );\n      return { success: false, error: error.message };\n    }\n  },\n\n  /**\n   * Gets browser keys by params\n   * @param {object} clause\n   * @param {number|null} limit\n   * @param {object|null} orderBy\n   * @returns {Promise<import(\"@prisma/client\").browser_extension_api_keys[]>}\n   */\n  where: async function (clause = {}, limit = null, orderBy = null) {\n    try {\n      const apiKeys = await prisma.browser_extension_api_keys.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n        include: { user: true },\n      });\n      return apiKeys;\n    } catch (error) {\n      console.error(\"FAILED TO GET BROWSER EXTENSION API KEYS.\", error.message);\n      return [];\n    }\n  },\n\n  /**\n   * Get browser API keys for user\n   * @param {import(\"@prisma/client\").users} user\n   * @param {object} clause\n   * @param {number|null} limit\n   * @param {object|null} orderBy\n   * @returns {Promise<import(\"@prisma/client\").browser_extension_api_keys[]>}\n   */\n  whereWithUser: async function (\n    user,\n    clause = {},\n    limit = null,\n    orderBy = null\n  ) {\n    // Admin can view and use any keys\n    if ([ROLES.admin].includes(user.role))\n      return await this.where(clause, limit, orderBy);\n\n    try {\n      const apiKeys = await prisma.browser_extension_api_keys.findMany({\n        where: {\n          ...clause,\n          user_id: user.id,\n        },\n        include: { user: true },\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return apiKeys;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  /**\n   * Updates owner of all DB ids to new admin.\n   * @param {number} userId\n   * @returns {Promise<void>}\n   */\n  migrateApiKeysToMultiUser: async function (userId) {\n    try {\n      await prisma.browser_extension_api_keys.updateMany({\n        where: {\n          user_id: null,\n        },\n        data: {\n          user_id: userId,\n        },\n      });\n      console.log(\"Successfully migrated API keys to multi-user mode\");\n    } catch (error) {\n      console.error(\"Error migrating API keys to multi-user mode:\", error);\n    }\n  },\n};\n\nmodule.exports = { BrowserExtensionApiKey };\n"
  },
  {
    "path": "server/models/cacheData.js",
    "content": "const prisma = require(\"../utils/prisma\");\n\nconst CacheData = {\n  new: async function (inputs = {}) {\n    try {\n      const cache = await prisma.cache_data.create({\n        data: inputs,\n      });\n      return { cache, message: null };\n    } catch (error) {\n      console.error(error.message);\n      return { cache: null, message: error.message };\n    }\n  },\n\n  get: async function (clause = {}, limit = null, orderBy = null) {\n    try {\n      const cache = await prisma.cache_data.findFirst({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return cache || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.cache_data.deleteMany({\n        where: clause,\n      });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  where: async function (clause = {}, limit = null, orderBy = null) {\n    try {\n      const caches = await prisma.cache_data.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return caches;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  count: async function (clause = {}) {\n    try {\n      const count = await prisma.cache_data.count({\n        where: clause,\n      });\n      return count;\n    } catch (error) {\n      console.error(error.message);\n      return 0;\n    }\n  },\n};\n\nmodule.exports = { CacheData };\n"
  },
  {
    "path": "server/models/communityHub.js",
    "content": "const ImportedPlugin = require(\"../utils/agents/imported\");\n\n/**\n * An interface to the AnythingLLM Community Hub external API.\n */\nconst CommunityHub = {\n  importPrefix: \"allm-community-id\",\n  apiBase:\n    process.env.NODE_ENV === \"development\"\n      ? \"http://127.0.0.1:5001/anythingllm-hub/us-central1/external/v1\"\n      : \"https://hub.external.anythingllm.com/v1\",\n  supportedStaticItemTypes: [\"system-prompt\", \"agent-flow\", \"slash-command\"],\n\n  /**\n   * Validate an import ID and return the entity type and ID.\n   * @param {string} importId - The import ID to validate.\n   * @returns {{entityType: string | null, entityId: string | null}}\n   */\n  validateImportId: function (importId) {\n    if (\n      !importId ||\n      !importId.startsWith(this.importPrefix) ||\n      importId.split(\":\").length !== 3\n    )\n      return { entityType: null, entityId: null };\n    const [_, entityType, entityId] = importId.split(\":\");\n    if (!entityType || !entityId) return { entityType: null, entityId: null };\n    return {\n      entityType: String(entityType).trim(),\n      entityId: String(entityId).trim(),\n    };\n  },\n\n  /**\n   * Fetch the explore items from the community hub that are publicly available.\n   * @returns {Promise<{agentSkills: {items: [], hasMore: boolean, totalCount: number}, systemPrompts: {items: [], hasMore: boolean, totalCount: number}, slashCommands: {items: [], hasMore: boolean, totalCount: number}}>}\n   */\n  fetchExploreItems: async function () {\n    return await fetch(`${this.apiBase}/explore`, {\n      method: \"GET\",\n    })\n      .then((response) => response.json())\n      .catch((error) => {\n        console.error(\"Error fetching explore items:\", error);\n        return {\n          agentSkills: {\n            items: [],\n            hasMore: false,\n            totalCount: 0,\n          },\n          systemPrompts: {\n            items: [],\n            hasMore: false,\n            totalCount: 0,\n          },\n          slashCommands: {\n            items: [],\n            hasMore: false,\n            totalCount: 0,\n          },\n        };\n      });\n  },\n\n  /**\n   * Fetch a bundle item from the community hub.\n   * Bundle items are entities that require a downloadURL to be fetched from the community hub.\n   * so we can unzip and import them to the AnythingLLM instance.\n   * @param {string} importId - The import ID of the item.\n   * @returns {Promise<{url: string | null, item: object | null, error: string | null}>}\n   */\n  getBundleItem: async function (importId) {\n    const { entityType, entityId } = this.validateImportId(importId);\n    if (!entityType || !entityId)\n      return { item: null, error: \"Invalid import ID\" };\n\n    const { SystemSettings } = require(\"./systemSettings\");\n    const { connectionKey } = await SystemSettings.hubSettings();\n    const { url, item, error } = await fetch(\n      `${this.apiBase}/${entityType}/${entityId}/pull`,\n      {\n        method: \"GET\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n          ...(connectionKey\n            ? { Authorization: `Bearer ${connectionKey}` }\n            : {}),\n        },\n      }\n    )\n      .then((response) => response.json())\n      .catch((error) => {\n        console.error(\n          `Error fetching bundle item for import ID ${importId}:`,\n          error\n        );\n        return { url: null, item: null, error: error.message };\n      });\n    return { url, item, error };\n  },\n\n  /**\n   * Apply an item to the AnythingLLM instance. Used for simple items like slash commands and system prompts.\n   * @param {object} item - The item to apply.\n   * @param {object} options - Additional options for applying the item.\n   * @param {object|null} options.currentUser - The current user object.\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  applyItem: async function (item, options = {}) {\n    if (!item) return { success: false, error: \"Item is required\" };\n\n    if (item.itemType === \"system-prompt\") {\n      if (!options?.workspaceSlug)\n        return { success: false, error: \"Workspace slug is required\" };\n\n      const { Workspace } = require(\"./workspace\");\n      const workspace = await Workspace.get({\n        slug: String(options.workspaceSlug),\n      });\n      if (!workspace) return { success: false, error: \"Workspace not found\" };\n      await Workspace.update(workspace.id, { openAiPrompt: item.prompt });\n      return { success: true, error: null };\n    }\n\n    if (item.itemType === \"slash-command\") {\n      const { SlashCommandPresets } = require(\"./slashCommandsPresets\");\n      await SlashCommandPresets.create(options?.currentUser?.id, {\n        command: SlashCommandPresets.formatCommand(String(item.command)),\n        prompt: String(item.prompt),\n        description: String(item.description),\n      });\n      return { success: true, error: null };\n    }\n\n    return {\n      success: false,\n      error: \"Unsupported item type. Nothing to apply.\",\n    };\n  },\n\n  /**\n   * Import a bundle item to the AnythingLLM instance by downloading the zip file and importing it.\n   * or whatever the item type requires.\n   * @param {{url: string, item: object}} params\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  importBundleItem: async function ({ url, item }) {\n    if (item.itemType === \"agent-skill\") {\n      const { success, error } =\n        await ImportedPlugin.importCommunityItemFromUrl(url, item);\n      return { success, error };\n    }\n\n    return {\n      success: false,\n      error: \"Unsupported item type. Nothing to import.\",\n    };\n  },\n\n  fetchUserItems: async function (connectionKey) {\n    if (!connectionKey) return { createdByMe: {}, teamItems: [] };\n\n    return await fetch(`${this.apiBase}/items`, {\n      method: \"GET\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `Bearer ${connectionKey}`,\n      },\n    })\n      .then((response) => response.json())\n      .catch((error) => {\n        console.error(\"Error fetching user items:\", error);\n        return { createdByMe: {}, teamItems: [] };\n      });\n  },\n\n  /**\n   * Create a new item in the community hub - Only supports STATIC items for now.\n   * @param {string} itemType - The type of item to create\n   * @param {object} data - The item data\n   * @param {string} connectionKey - The hub connection key\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  createStaticItem: async function (itemType, data, connectionKey) {\n    if (!connectionKey)\n      return { success: false, error: \"Connection key is required\" };\n    if (!this.supportedStaticItemTypes.includes(itemType))\n      return { success: false, error: \"Unsupported item type\" };\n\n    // If the item has special considerations or preprocessing, we can delegate that below before sending the request.\n    // eg: Agent flow files and such.\n\n    return await fetch(`${this.apiBase}/${itemType}/create`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `Bearer ${connectionKey}`,\n      },\n      body: JSON.stringify(data),\n    })\n      .then((response) => response.json())\n      .then((result) => {\n        if (!!result.error) throw new Error(result.error || \"Unknown error\");\n        return { success: true, error: null, itemId: result.item.id };\n      })\n      .catch((error) => {\n        console.error(`Error creating ${itemType}:`, error);\n        return { success: false, error: error.message };\n      });\n  },\n};\n\nmodule.exports = { CommunityHub };\n"
  },
  {
    "path": "server/models/documentSyncQueue.js",
    "content": "const { BackgroundService } = require(\"../utils/BackgroundWorkers\");\nconst prisma = require(\"../utils/prisma\");\nconst { SystemSettings } = require(\"./systemSettings\");\nconst { Telemetry } = require(\"./telemetry\");\n\n/**\n * @typedef {('link'|'youtube'|'confluence'|'github'|'gitlab')} validFileType\n */\n\nconst DocumentSyncQueue = {\n  featureKey: \"experimental_live_file_sync\",\n  // update the validFileTypes and .canWatch properties when adding elements here.\n  validFileTypes: [\n    \"link\",\n    \"youtube\",\n    \"confluence\",\n    \"github\",\n    \"gitlab\",\n    \"drupalwiki\",\n  ],\n  defaultStaleAfter: 604800000,\n  maxRepeatFailures: 5, // How many times a run can fail in a row before pruning.\n  writable: [],\n\n  bootWorkers: function () {\n    new BackgroundService().boot();\n  },\n\n  killWorkers: function () {\n    new BackgroundService().stop();\n  },\n\n  /** Check is the Document Sync/Watch feature is enabled and can be used. */\n  enabled: async function () {\n    return (\n      (await SystemSettings.get({ label: this.featureKey }))?.value ===\n      \"enabled\"\n    );\n  },\n\n  /**\n   * @param {import(\"@prisma/client\").document_sync_queues} queueRecord - queue record to calculate for\n   */\n  calcNextSync: function (queueRecord) {\n    return new Date(Number(new Date()) + queueRecord.staleAfterMs);\n  },\n\n  /**\n   * Check if the document can be watched based on the metadata fields\n   * @param {object} metadata - metadata to check\n   * @param {string} metadata.title - title of the document\n   * @param {string} metadata.chunkSource - chunk source of the document\n   * @returns {boolean} - true if the document can be watched, false otherwise\n   */\n  canWatch: function ({ title, chunkSource = null } = {}) {\n    if (!chunkSource) return false;\n\n    if (chunkSource.startsWith(\"link://\") && title.endsWith(\".html\"))\n      return true; // If is web-link material (prior to feature most chunkSources were links://)\n    if (chunkSource.startsWith(\"youtube://\")) return true; // If is a youtube link\n    if (chunkSource.startsWith(\"confluence://\")) return true; // If is a confluence document link\n    if (chunkSource.startsWith(\"github://\")) return true; // If is a GitHub file reference\n    if (chunkSource.startsWith(\"gitlab://\")) return true; // If is a GitLab file reference\n    if (chunkSource.startsWith(\"drupalwiki://\")) return true; // If is a DrupalWiki document link\n    return false;\n  },\n\n  /**\n   * Creates Queue record and updates document watch status to true on Document record\n   * @param {import(\"@prisma/client\").workspace_documents} document - document record to watch, must have `id`\n   */\n  watch: async function (document = null) {\n    if (!document) return false;\n    try {\n      const { Document } = require(\"./documents\");\n\n      // Get all documents that are watched and share the same unique filename. If this value is\n      // non-zero then we exit early so that we do not have duplicated watch queues for the same file\n      // across many workspaces.\n      const workspaceDocIds = (\n        await Document.where({ filename: document.filename, watched: true })\n      ).map((rec) => rec.id);\n      const hasRecords =\n        (await this.count({ workspaceDocId: { in: workspaceDocIds } })) > 0;\n      if (hasRecords)\n        throw new Error(\n          `Cannot watch this document again - it already has a queue set.`\n        );\n\n      const queue = await prisma.document_sync_queues.create({\n        data: {\n          workspaceDocId: document.id,\n          nextSyncAt: new Date(Number(new Date()) + this.defaultStaleAfter),\n        },\n      });\n      await Document._updateAll(\n        { filename: document.filename },\n        { watched: true }\n      );\n      return queue || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  /**\n   * Deletes Queue record and updates document watch status to false on Document record\n   * @param {import(\"@prisma/client\").workspace_documents} document - document record to unwatch, must have `id`\n   */\n  unwatch: async function (document = null) {\n    if (!document) return false;\n    try {\n      const { Document } = require(\"./documents\");\n\n      // We could have been given a document to unwatch which is a clone of one that is already being watched but by another workspaceDocument id.\n      // so in this instance we need to delete any queues related to this document by any WorkspaceDocumentId it is referenced by.\n      const workspaceDocIds = (\n        await Document.where({ filename: document.filename, watched: true })\n      ).map((rec) => rec.id);\n      await this.delete({ workspaceDocId: { in: workspaceDocIds } });\n      await Document._updateAll(\n        { filename: document.filename },\n        { watched: false }\n      );\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  _update: async function (id = null, data = {}) {\n    if (!id) throw new Error(\"No id provided for update\");\n\n    try {\n      await prisma.document_sync_queues.update({\n        where: { id },\n        data,\n      });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  get: async function (clause = {}) {\n    try {\n      const queue = await prisma.document_sync_queues.findFirst({\n        where: clause,\n      });\n      return queue || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  where: async function (\n    clause = {},\n    limit = null,\n    orderBy = null,\n    include = {}\n  ) {\n    try {\n      const results = await prisma.document_sync_queues.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n        ...(include !== null ? { include } : {}),\n      });\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  count: async function (clause = {}, limit = null) {\n    try {\n      const count = await prisma.document_sync_queues.count({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n      });\n      return count;\n    } catch (error) {\n      console.error(\"FAILED TO COUNT DOCUMENTS.\", error.message);\n      return 0;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.document_sync_queues.deleteMany({ where: clause });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  /**\n   * Gets the \"stale\" queues where the queue's nextSyncAt is less than the current time\n   * @returns {Promise<(\n   *  import(\"@prisma/client\").document_sync_queues &\n   * { workspaceDoc: import(\"@prisma/client\").workspace_documents &\n   *  { workspace: import(\"@prisma/client\").workspaces }\n   * })[]}>}\n   */\n  staleDocumentQueues: async function () {\n    const queues = await this.where(\n      {\n        nextSyncAt: {\n          lte: new Date().toISOString(),\n        },\n      },\n      null,\n      null,\n      {\n        workspaceDoc: {\n          include: {\n            workspace: true,\n          },\n        },\n      }\n    );\n    return queues;\n  },\n\n  saveRun: async function (queueId = null, status = null, result = {}) {\n    const { DocumentSyncRun } = require(\"./documentSyncRun\");\n    return DocumentSyncRun.save(queueId, status, result);\n  },\n\n  /**\n   * Updates document to be watched/unwatched & creates or deletes any queue records and updated Document record `watched` status\n   * @param {import(\"@prisma/client\").workspace_documents} documentRecord\n   * @param {boolean} watchStatus - indicate if queue record should be created or not.\n   * @returns\n   */\n  toggleWatchStatus: async function (documentRecord, watchStatus = false) {\n    if (!watchStatus) {\n      await Telemetry.sendTelemetry(\"document_unwatched\");\n      await this.unwatch(documentRecord);\n      return;\n    }\n\n    await this.watch(documentRecord);\n    await Telemetry.sendTelemetry(\"document_watched\");\n    return;\n  },\n};\n\nmodule.exports = { DocumentSyncQueue };\n"
  },
  {
    "path": "server/models/documentSyncRun.js",
    "content": "const prisma = require(\"../utils/prisma\");\nconst DocumentSyncRun = {\n  statuses: {\n    unknown: \"unknown\",\n    exited: \"exited\",\n    failed: \"failed\",\n    success: \"success\",\n  },\n\n  save: async function (queueId = null, status = null, result = {}) {\n    try {\n      if (!this.statuses.hasOwnProperty(status))\n        throw new Error(\n          `DocumentSyncRun status ${status} is not a valid status.`\n        );\n\n      const run = await prisma.document_sync_executions.create({\n        data: {\n          queueId: Number(queueId),\n          status: String(status),\n          result: JSON.stringify(result),\n        },\n      });\n      return run || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  get: async function (clause = {}) {\n    try {\n      const queue = await prisma.document_sync_executions.findFirst({\n        where: clause,\n      });\n      return queue || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  where: async function (\n    clause = {},\n    limit = null,\n    orderBy = null,\n    include = {}\n  ) {\n    try {\n      const results = await prisma.document_sync_executions.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n        ...(include !== null ? { include } : {}),\n      });\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  count: async function (clause = {}, limit = null, orderBy = {}) {\n    try {\n      const count = await prisma.document_sync_executions.count({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return count;\n    } catch (error) {\n      console.error(\"FAILED TO COUNT DOCUMENTS.\", error.message);\n      return 0;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.document_sync_executions.deleteMany({ where: clause });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n};\n\nmodule.exports = { DocumentSyncRun };\n"
  },
  {
    "path": "server/models/documents.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst { getVectorDbClass } = require(\"../utils/helpers\");\nconst prisma = require(\"../utils/prisma\");\nconst { Telemetry } = require(\"./telemetry\");\nconst { EventLogs } = require(\"./eventLogs\");\nconst { safeJsonParse } = require(\"../utils/http\");\nconst { getModelTag } = require(\"../endpoints/utils\");\n\nconst Document = {\n  writable: [\"pinned\", \"watched\", \"lastUpdatedAt\"],\n  /**\n   * @param {import(\"@prisma/client\").workspace_documents} document - Document PrismaRecord\n   * @returns {{\n   *  metadata: (null|object),\n   *  type: import(\"./documentSyncQueue.js\").validFileType,\n   *  source: string\n   * }}\n   */\n  parseDocumentTypeAndSource: function (document) {\n    const metadata = safeJsonParse(document.metadata, null);\n    if (!metadata) return { metadata: null, type: null, source: null };\n\n    // Parse the correct type of source and its original source path.\n    const idx = metadata.chunkSource.indexOf(\"://\");\n    const [type, source] = [\n      metadata.chunkSource.slice(0, idx),\n      metadata.chunkSource.slice(idx + 3),\n    ];\n    return { metadata, type, source: this._stripSource(source, type) };\n  },\n\n  forWorkspace: async function (workspaceId = null) {\n    if (!workspaceId) return [];\n    return await prisma.workspace_documents.findMany({\n      where: { workspaceId },\n    });\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.workspace_documents.deleteMany({ where: clause });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  get: async function (clause = {}) {\n    try {\n      const document = await prisma.workspace_documents.findFirst({\n        where: clause,\n      });\n      return document || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  where: async function (\n    clause = {},\n    limit = null,\n    orderBy = null,\n    include = null,\n    select = null\n  ) {\n    try {\n      const results = await prisma.workspace_documents.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n        ...(include !== null ? { include } : {}),\n        ...(select !== null ? { select: { ...select } } : {}),\n      });\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  addDocuments: async function (workspace, additions = [], userId = null) {\n    const VectorDb = getVectorDbClass();\n    if (additions.length === 0) return { failed: [], embedded: [] };\n    const { fileData } = require(\"../utils/files\");\n    const embedded = [];\n    const failedToEmbed = [];\n    const errors = new Set();\n\n    for (const path of additions) {\n      const data = await fileData(path);\n      if (!data) continue;\n\n      const docId = uuidv4();\n      const { pageContent: _pageContent, ...metadata } = data;\n      const newDoc = {\n        docId,\n        filename: path.split(\"/\")[1],\n        docpath: path,\n        workspaceId: workspace.id,\n        metadata: JSON.stringify(metadata),\n      };\n\n      const { vectorized, error } = await VectorDb.addDocumentToNamespace(\n        workspace.slug,\n        { ...data, docId },\n        path\n      );\n\n      if (!vectorized) {\n        console.error(\n          \"Failed to vectorize\",\n          metadata?.title || newDoc.filename\n        );\n        failedToEmbed.push(metadata?.title || newDoc.filename);\n        errors.add(error);\n        continue;\n      }\n\n      try {\n        await prisma.workspace_documents.create({ data: newDoc });\n        embedded.push(path);\n      } catch (error) {\n        console.error(error.message);\n      }\n    }\n\n    await Telemetry.sendTelemetry(\"documents_embedded_in_workspace\", {\n      LLMSelection: process.env.LLM_PROVIDER || \"openai\",\n      Embedder: process.env.EMBEDDING_ENGINE || \"inherit\",\n      VectorDbSelection: process.env.VECTOR_DB || \"lancedb\",\n      TTSSelection: process.env.TTS_PROVIDER || \"native\",\n      LLMModel: getModelTag(),\n    });\n    await EventLogs.logEvent(\n      \"workspace_documents_added\",\n      {\n        workspaceName: workspace?.name || \"Unknown Workspace\",\n        numberOfDocumentsAdded: additions.length,\n      },\n      userId\n    );\n    return { failedToEmbed, errors: Array.from(errors), embedded };\n  },\n\n  removeDocuments: async function (workspace, removals = [], userId = null) {\n    const VectorDb = getVectorDbClass();\n    if (removals.length === 0) return;\n\n    for (const path of removals) {\n      const document = await this.get({\n        docpath: path,\n        workspaceId: workspace.id,\n      });\n      if (!document) continue;\n      await VectorDb.deleteDocumentFromNamespace(\n        workspace.slug,\n        document.docId\n      );\n\n      try {\n        await prisma.workspace_documents.delete({\n          where: { id: document.id, workspaceId: workspace.id },\n        });\n        await prisma.document_vectors.deleteMany({\n          where: { docId: document.docId },\n        });\n      } catch (error) {\n        console.error(error.message);\n      }\n    }\n\n    await EventLogs.logEvent(\n      \"workspace_documents_removed\",\n      {\n        workspaceName: workspace?.name || \"Unknown Workspace\",\n        numberOfDocuments: removals.length,\n      },\n      userId\n    );\n    return true;\n  },\n\n  count: async function (clause = {}, limit = null) {\n    try {\n      const count = await prisma.workspace_documents.count({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n      });\n      return count;\n    } catch (error) {\n      console.error(\"FAILED TO COUNT DOCUMENTS.\", error.message);\n      return 0;\n    }\n  },\n  update: async function (id = null, data = {}) {\n    if (!id) throw new Error(\"No workspace document id provided for update\");\n\n    const validKeys = Object.keys(data).filter((key) =>\n      this.writable.includes(key)\n    );\n    if (validKeys.length === 0)\n      return { document: { id }, message: \"No valid fields to update!\" };\n\n    try {\n      const document = await prisma.workspace_documents.update({\n        where: { id },\n        data,\n      });\n      return { document, message: null };\n    } catch (error) {\n      console.error(error.message);\n      return { document: null, message: error.message };\n    }\n  },\n  _updateAll: async function (clause = {}, data = {}) {\n    try {\n      await prisma.workspace_documents.updateMany({\n        where: clause,\n        data,\n      });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n  content: async function (docId) {\n    if (!docId) throw new Error(\"No workspace docId provided!\");\n    const document = await this.get({ docId: String(docId) });\n    if (!document) throw new Error(`Could not find a document by id ${docId}`);\n\n    const { fileData } = require(\"../utils/files\");\n    const data = await fileData(document.docpath);\n    return { title: data.title, content: data.pageContent };\n  },\n  contentByDocPath: async function (docPath) {\n    const { fileData } = require(\"../utils/files\");\n    const data = await fileData(docPath);\n    return { title: data.title, content: data.pageContent };\n  },\n\n  // Some data sources have encoded params in them we don't want to log - so strip those details.\n  _stripSource: function (sourceString, type) {\n    if ([\"confluence\", \"github\"].includes(type)) {\n      const _src = new URL(sourceString);\n      _src.search = \"\"; // remove all search params that are encoded for resync.\n      return _src.toString();\n    }\n\n    return sourceString;\n  },\n\n  /**\n   * Functions for the backend API endpoints - not to be used by the frontend or elsewhere.\n   * @namespace api\n   */\n  api: {\n    /**\n     * Process a document upload from the API and upsert it into the database. This\n     * functionality should only be used by the backend /v1/documents/upload endpoints for post-upload embedding.\n     * @param {string} wsSlugs - The slugs of the workspaces to embed the document into, will be comma-separated list of workspace slugs\n     * @param {string} docLocation - The location/path of the document that was uploaded\n     * @returns {Promise<boolean>} - True if the document was uploaded successfully, false otherwise\n     */\n    uploadToWorkspace: async function (wsSlugs = \"\", docLocation = null) {\n      if (!docLocation)\n        return console.log(\n          \"No document location provided for embedding\",\n          docLocation\n        );\n\n      const slugs = wsSlugs\n        .split(\",\")\n        .map((slug) => String(slug)?.trim()?.toLowerCase());\n      if (slugs.length === 0)\n        return console.log(`No workspaces provided got: ${wsSlugs}`);\n\n      const { Workspace } = require(\"./workspace\");\n      const workspaces = await Workspace.where({ slug: { in: slugs } });\n      if (workspaces.length === 0)\n        return console.log(\"No valid workspaces found for slugs: \", slugs);\n\n      // Upsert the document into each workspace - do this sequentially\n      // because the document may be large and we don't want to overwhelm the embedder, plus on the first\n      // upsert we will then have the cache of the document - making n+1 embeds faster. If we parallelize this\n      // we will have to do a lot of extra work to ensure that the document is not embedded more than once.\n      for (const workspace of workspaces) {\n        const { failedToEmbed = [], errors = [] } = await Document.addDocuments(\n          workspace,\n          [docLocation]\n        );\n        if (failedToEmbed.length > 0)\n          return console.log(\n            `Failed to embed document into workspace ${workspace.slug}`,\n            errors\n          );\n        console.log(`Document embedded into workspace ${workspace.slug}...`);\n      }\n\n      return true;\n    },\n  },\n};\n\nmodule.exports = { Document };\n"
  },
  {
    "path": "server/models/embedChats.js",
    "content": "const { safeJsonParse } = require(\"../utils/http\");\nconst prisma = require(\"../utils/prisma\");\n\n/**\n * @typedef {Object} EmbedChat\n * @property {number} id\n * @property {number} embed_id\n * @property {string} prompt\n * @property {string} response\n * @property {string} connection_information\n * @property {string} session_id\n * @property {boolean} include\n */\n\nconst EmbedChats = {\n  new: async function ({\n    embedId,\n    prompt,\n    response = {},\n    connection_information = {},\n    sessionId,\n  }) {\n    try {\n      const chat = await prisma.embed_chats.create({\n        data: {\n          prompt,\n          embed_id: Number(embedId),\n          response: JSON.stringify(response),\n          connection_information: JSON.stringify(connection_information),\n          session_id: String(sessionId),\n        },\n      });\n      return { chat, message: null };\n    } catch (error) {\n      console.error(error.message);\n      return { chat: null, message: error.message };\n    }\n  },\n\n  /**\n   * Loops through each chat and filters out the sources from the response object.\n   * We do this when returning /history of an embed to the frontend to prevent inadvertent leaking\n   * of private sources the user may not have intended to share with users.\n   * @param {EmbedChat[]} chats\n   * @returns {EmbedChat[]} Returns a new array of chats with the sources filtered out of responses\n   */\n  filterSources: function (chats) {\n    return chats.map((chat) => {\n      const { response, ...rest } = chat;\n      const { sources: _sources, ...responseRest } = safeJsonParse(response);\n      return { ...rest, response: JSON.stringify(responseRest) };\n    });\n  },\n\n  /**\n   * Fetches chats for a given embed and session id.\n   * @param {number} embedId the id of the embed to fetch chats for\n   * @param {string} sessionId the id of the session to fetch chats for\n   * @param {number|null} limit the maximum number of chats to fetch\n   * @param {string|null} orderBy the order to fetch chats in\n   * @param {boolean} filterSources whether to filter out the sources from the response (default: false)\n   * @returns {Promise<EmbedChat[]>} Returns an array of chats for the given embed and session\n   */\n  forEmbedByUser: async function (\n    embedId = null,\n    sessionId = null,\n    limit = null,\n    orderBy = null,\n    filterSources = false\n  ) {\n    if (!embedId || !sessionId) return [];\n\n    try {\n      const chats = await prisma.embed_chats.findMany({\n        where: {\n          embed_id: Number(embedId),\n          session_id: String(sessionId),\n          include: true,\n        },\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : { orderBy: { id: \"asc\" } }),\n      });\n      return filterSources ? this.filterSources(chats) : chats;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  markHistoryInvalid: async function (embedId = null, sessionId = null) {\n    if (!embedId || !sessionId) return [];\n\n    try {\n      await prisma.embed_chats.updateMany({\n        where: {\n          embed_id: Number(embedId),\n          session_id: String(sessionId),\n        },\n        data: {\n          include: false,\n        },\n      });\n      return;\n    } catch (error) {\n      console.error(error.message);\n    }\n  },\n\n  get: async function (clause = {}, limit = null, orderBy = null) {\n    try {\n      const chat = await prisma.embed_chats.findFirst({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return chat || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.embed_chats.deleteMany({\n        where: clause,\n      });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  where: async function (\n    clause = {},\n    limit = null,\n    orderBy = null,\n    offset = null\n  ) {\n    try {\n      const chats = await prisma.embed_chats.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(offset !== null ? { skip: offset } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return chats;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  whereWithEmbedAndWorkspace: async function (\n    clause = {},\n    limit = null,\n    orderBy = null,\n    offset = null\n  ) {\n    try {\n      const chats = await prisma.embed_chats.findMany({\n        where: clause,\n        include: {\n          embed_config: {\n            select: {\n              workspace: {\n                select: {\n                  name: true,\n                },\n              },\n            },\n          },\n        },\n        ...(limit !== null ? { take: limit } : {}),\n        ...(offset !== null ? { skip: offset } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return chats;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  count: async function (clause = {}) {\n    try {\n      const count = await prisma.embed_chats.count({\n        where: clause,\n      });\n      return count;\n    } catch (error) {\n      console.error(error.message);\n      return 0;\n    }\n  },\n};\n\nmodule.exports = { EmbedChats };\n"
  },
  {
    "path": "server/models/embedConfig.js",
    "content": "const { v4 } = require(\"uuid\");\nconst prisma = require(\"../utils/prisma\");\nconst { VALID_CHAT_MODE } = require(\"../utils/chats/stream\");\n\nconst EmbedConfig = {\n  writable: [\n    // Used for generic updates so we can validate keys in request body\n    \"enabled\",\n    \"allowlist_domains\",\n    \"allow_model_override\",\n    \"allow_temperature_override\",\n    \"allow_prompt_override\",\n    \"max_chats_per_day\",\n    \"max_chats_per_session\",\n    \"chat_mode\",\n    \"workspace_id\",\n    \"message_limit\",\n  ],\n\n  new: async function (data, creatorId = null) {\n    try {\n      const embed = await prisma.embed_configs.create({\n        data: {\n          uuid: v4(),\n          enabled: true,\n          chat_mode: validatedCreationData(data?.chat_mode, \"chat_mode\"),\n          allowlist_domains: validatedCreationData(\n            data?.allowlist_domains,\n            \"allowlist_domains\"\n          ),\n          allow_model_override: validatedCreationData(\n            data?.allow_model_override,\n            \"allow_model_override\"\n          ),\n          allow_temperature_override: validatedCreationData(\n            data?.allow_temperature_override,\n            \"allow_temperature_override\"\n          ),\n          allow_prompt_override: validatedCreationData(\n            data?.allow_prompt_override,\n            \"allow_prompt_override\"\n          ),\n          max_chats_per_day: validatedCreationData(\n            data?.max_chats_per_day,\n            \"max_chats_per_day\"\n          ),\n          max_chats_per_session: validatedCreationData(\n            data?.max_chats_per_session,\n            \"max_chats_per_session\"\n          ),\n          message_limit: validatedCreationData(\n            data?.message_limit,\n            \"message_limit\"\n          ),\n          createdBy: creatorId != null ? Number(creatorId) : null,\n          workspace: {\n            connect: { id: Number(data.workspace_id) },\n          },\n        },\n      });\n      return { embed, message: null };\n    } catch (error) {\n      console.error(error.message);\n      return { embed: null, message: error.message };\n    }\n  },\n\n  update: async function (embedId = null, data = {}) {\n    if (!embedId) throw new Error(\"No embed id provided for update\");\n    const validKeys = Object.keys(data).filter((key) =>\n      this.writable.includes(key)\n    );\n    if (validKeys.length === 0)\n      return { embed: { id: embedId }, message: \"No valid fields to update!\" };\n\n    const updates = {};\n    validKeys.map((key) => {\n      updates[key] = validatedCreationData(data[key], key);\n    });\n\n    try {\n      await prisma.embed_configs.update({\n        where: { id: Number(embedId) },\n        data: updates,\n      });\n      return { success: true, error: null };\n    } catch (error) {\n      console.error(error.message);\n      return { success: false, error: error.message };\n    }\n  },\n\n  get: async function (clause = {}) {\n    try {\n      const embedConfig = await prisma.embed_configs.findFirst({\n        where: clause,\n      });\n\n      return embedConfig || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  getWithWorkspace: async function (clause = {}) {\n    try {\n      const embedConfig = await prisma.embed_configs.findFirst({\n        where: clause,\n        include: {\n          workspace: true,\n        },\n      });\n\n      return embedConfig || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.embed_configs.delete({\n        where: clause,\n      });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  where: async function (clause = {}, limit = null, orderBy = null) {\n    try {\n      const results = await prisma.embed_configs.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  whereWithWorkspace: async function (\n    clause = {},\n    limit = null,\n    orderBy = null\n  ) {\n    try {\n      const results = await prisma.embed_configs.findMany({\n        where: clause,\n        include: {\n          workspace: true,\n          _count: {\n            select: { embed_chats: true },\n          },\n        },\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  // Will return null if process should be skipped\n  // an empty array means the system will check. This\n  // prevents a bad parse from allowing all requests\n  parseAllowedHosts: function (embed) {\n    if (!embed.allowlist_domains) return null;\n\n    try {\n      return JSON.parse(embed.allowlist_domains);\n    } catch {\n      console.error(`Failed to parse allowlist_domains for Embed ${embed.id}!`);\n      return [];\n    }\n  },\n};\n\nconst BOOLEAN_KEYS = [\n  \"allow_model_override\",\n  \"allow_temperature_override\",\n  \"allow_prompt_override\",\n  \"enabled\",\n];\n\nconst NUMBER_KEYS = [\n  \"max_chats_per_day\",\n  \"max_chats_per_session\",\n  \"workspace_id\",\n  \"message_limit\",\n];\n\n// Helper to validate a data object strictly into the proper format\nfunction validatedCreationData(value, field) {\n  if (field === \"chat_mode\") {\n    if (!value || !VALID_CHAT_MODE.includes(value)) return \"query\";\n    return value;\n  }\n\n  if (field === \"allowlist_domains\") {\n    try {\n      if (!value) return null;\n      return JSON.stringify(\n        // Iterate and force all domains to URL object\n        // and stringify the result.\n        value\n          .split(\",\")\n          .map((input) => {\n            let url = input;\n            if (!url.includes(\"http://\") && !url.includes(\"https://\"))\n              url = `https://${url}`;\n            try {\n              new URL(url);\n              return url;\n            } catch {\n              return null;\n            }\n          })\n          .filter((u) => !!u)\n      );\n    } catch {\n      return null;\n    }\n  }\n\n  if (BOOLEAN_KEYS.includes(field)) {\n    return value === true || value === false ? value : false;\n  }\n\n  if (NUMBER_KEYS.includes(field)) {\n    return isNaN(value) || Number(value) <= 0 ? null : Number(value);\n  }\n\n  return null;\n}\n\nmodule.exports = { EmbedConfig };\n"
  },
  {
    "path": "server/models/eventLogs.js",
    "content": "const prisma = require(\"../utils/prisma\");\n\nconst EventLogs = {\n  logEvent: async function (event, metadata = {}, userId = null) {\n    try {\n      const eventLog = await prisma.event_logs.create({\n        data: {\n          event,\n          metadata: metadata ? JSON.stringify(metadata) : null,\n          userId: userId ? Number(userId) : null,\n          occurredAt: new Date(),\n        },\n      });\n      console.log(`\\x1b[32m[Event Logged]\\x1b[0m - ${event}`);\n      return { eventLog, message: null };\n    } catch (error) {\n      console.error(\n        `\\x1b[31m[Event Logging Failed]\\x1b[0m - ${event}`,\n        error.message\n      );\n      return { eventLog: null, message: error.message };\n    }\n  },\n\n  getByEvent: async function (event, limit = null, orderBy = null) {\n    try {\n      const logs = await prisma.event_logs.findMany({\n        where: { event },\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null\n          ? { orderBy }\n          : { orderBy: { occurredAt: \"desc\" } }),\n      });\n      return logs;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  getByUserId: async function (userId, limit = null, orderBy = null) {\n    try {\n      const logs = await prisma.event_logs.findMany({\n        where: { userId },\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null\n          ? { orderBy }\n          : { orderBy: { occurredAt: \"desc\" } }),\n      });\n      return logs;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  where: async function (\n    clause = {},\n    limit = null,\n    orderBy = null,\n    offset = null\n  ) {\n    try {\n      const logs = await prisma.event_logs.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(offset !== null ? { skip: offset } : {}),\n        ...(orderBy !== null\n          ? { orderBy }\n          : { orderBy: { occurredAt: \"desc\" } }),\n      });\n      return logs;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  whereWithData: async function (\n    clause = {},\n    limit = null,\n    offset = null,\n    orderBy = null\n  ) {\n    const { User } = require(\"./user\");\n\n    try {\n      const results = await this.where(clause, limit, orderBy, offset);\n\n      for (const res of results) {\n        const user = res.userId ? await User.get({ id: res.userId }) : null;\n        res.user = user\n          ? { username: user.username }\n          : { username: \"unknown user\" };\n      }\n\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  count: async function (clause = {}) {\n    try {\n      const count = await prisma.event_logs.count({\n        where: clause,\n      });\n      return count;\n    } catch (error) {\n      console.error(error.message);\n      return 0;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.event_logs.deleteMany({\n        where: clause,\n      });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n};\n\nmodule.exports = { EventLogs };\n"
  },
  {
    "path": "server/models/invite.js",
    "content": "const { safeJsonParse } = require(\"../utils/http\");\nconst prisma = require(\"../utils/prisma\");\n\nconst Invite = {\n  makeCode: () => {\n    const uuidAPIKey = require(\"uuid-apikey\");\n    return uuidAPIKey.create().apiKey;\n  },\n\n  create: async function ({ createdByUserId = 0, workspaceIds = [] }) {\n    try {\n      const invite = await prisma.invites.create({\n        data: {\n          code: this.makeCode(),\n          createdBy: createdByUserId,\n          workspaceIds: JSON.stringify(workspaceIds),\n        },\n      });\n      return { invite, error: null };\n    } catch (error) {\n      console.error(\"FAILED TO CREATE INVITE.\", error.message);\n      return { invite: null, error: error.message };\n    }\n  },\n\n  deactivate: async function (inviteId = null) {\n    try {\n      await prisma.invites.update({\n        where: { id: Number(inviteId) },\n        data: { status: \"disabled\" },\n      });\n      return { success: true, error: null };\n    } catch (error) {\n      console.error(error.message);\n      return { success: false, error: error.message };\n    }\n  },\n\n  markClaimed: async function (inviteId = null, user) {\n    try {\n      const invite = await prisma.invites.update({\n        where: { id: Number(inviteId) },\n        data: { status: \"claimed\", claimedBy: user.id },\n      });\n\n      try {\n        if (!!invite?.workspaceIds) {\n          const { Workspace } = require(\"./workspace\");\n          const { WorkspaceUser } = require(\"./workspaceUsers\");\n          const workspaceIds = (await Workspace.where({})).map(\n            (workspace) => workspace.id\n          );\n          const ids = safeJsonParse(invite.workspaceIds)\n            .map((id) => Number(id))\n            .filter((id) => workspaceIds.includes(id));\n          if (ids.length !== 0) await WorkspaceUser.createMany(user.id, ids);\n        }\n      } catch (e) {\n        console.error(\n          \"Could not add user to workspaces automatically\",\n          e.message\n        );\n      }\n\n      return { success: true, error: null };\n    } catch (error) {\n      console.error(error.message);\n      return { success: false, error: error.message };\n    }\n  },\n\n  get: async function (clause = {}) {\n    try {\n      const invite = await prisma.invites.findFirst({ where: clause });\n      return invite || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  count: async function (clause = {}) {\n    try {\n      const count = await prisma.invites.count({ where: clause });\n      return count;\n    } catch (error) {\n      console.error(error.message);\n      return 0;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.invites.deleteMany({ where: clause });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  where: async function (clause = {}, limit) {\n    try {\n      const invites = await prisma.invites.findMany({\n        where: clause,\n        take: limit || undefined,\n      });\n      return invites;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  whereWithUsers: async function (clause = {}, limit) {\n    const { User } = require(\"./user\");\n    try {\n      const invites = await this.where(clause, limit);\n      for (const invite of invites) {\n        if (invite.claimedBy) {\n          const acceptedUser = await User.get({ id: invite.claimedBy });\n          invite.claimedBy = {\n            id: acceptedUser?.id,\n            username: acceptedUser?.username,\n          };\n        }\n\n        if (invite.createdBy) {\n          const createdUser = await User.get({ id: invite.createdBy });\n          invite.createdBy = {\n            id: createdUser?.id,\n            username: createdUser?.username,\n          };\n        }\n      }\n      return invites;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n};\n\nmodule.exports = { Invite };\n"
  },
  {
    "path": "server/models/mobileDevice.js",
    "content": "const prisma = require(\"../utils/prisma\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst ip = require(\"ip\");\n\n/**\n * @typedef {Object} TemporaryMobileDeviceRequest\n * @property {number|null} userId - User id to associate creation of key with.\n * @property {number} createdAt - Timestamp of when the token was created.\n * @property {number} expiresAt - Timestamp of when the token expires.\n */\n\n/**\n * Temporary map to store mobile device requests\n * that are not yet approved. Generates a simple JWT\n * that expires and is tied to the user (if provided)\n * This token must be provided during /register event.\n * @type {Map<string, TemporaryMobileDeviceRequest>}\n */\nconst TemporaryMobileDeviceRequests = new Map();\n\nconst MobileDevice = {\n  platform: \"server\",\n  validDeviceOs: [\"android\"],\n  tablename: \"desktop_mobile_devices\",\n  writable: [\"approved\"],\n  validators: {\n    approved: (value) => {\n      if (typeof value !== \"boolean\") return \"Must be a boolean\";\n      return null;\n    },\n  },\n\n  /**\n   * Looks up and consumes a temporary token that was registered\n   * Will return null if the token is not found or expired.\n   * @param {string} token - The temporary token to lookup\n   * @returns {TemporaryMobileDeviceRequest|null} Temp token details\n   */\n  tempToken: (token = null) => {\n    try {\n      if (!token || !TemporaryMobileDeviceRequests.has(token)) return null;\n      const tokenData = TemporaryMobileDeviceRequests.get(token);\n      if (tokenData.expiresAt < Date.now()) return null;\n      return tokenData;\n    } catch {\n      return null;\n    } finally {\n      TemporaryMobileDeviceRequests.delete(token);\n    }\n  },\n\n  /**\n   * Registers a temporary token for a mobile device request\n   * This is just using a random token to identify the request\n   * @security Note: If we use a JWT the QR code that encodes it becomes extremely complex\n   * and noisy as QR codes have byte limits that could be exceeded with JWTs. Since this is\n   * a temporary token that is only used to register a device and is short lived we can use UUIDs.\n   * @param {import(\"@prisma/client\").users|null} user - User to get connection URL for in Multi-User Mode\n   * @returns {string} The temporary token\n   */\n  registerTempToken: function (user = null) {\n    let tokenData = {};\n    if (user) tokenData.userId = user.id;\n    else tokenData.userId = null;\n\n    // Set short lived expiry to this mapping\n    const createdAt = Date.now();\n    tokenData.createdAt = createdAt;\n    tokenData.expiresAt = createdAt + 3 * 60_000;\n\n    const tempToken = uuidv4().split(\"-\").slice(0, 3).join(\"\");\n    TemporaryMobileDeviceRequests.set(tempToken, tokenData);\n\n    // Run this on register since there is no BG task to do this.\n    this.cleanupExpiredTokens();\n    return tempToken;\n  },\n\n  /**\n   * Cleans up expired temporary registration tokens\n   * Should run quick since this mapping is wiped often\n   * and does not live past restarts.\n   */\n  cleanupExpiredTokens: function () {\n    const now = Date.now();\n    for (const [token, data] of TemporaryMobileDeviceRequests.entries()) {\n      if (data.expiresAt < now) TemporaryMobileDeviceRequests.delete(token);\n    }\n  },\n\n  /**\n   * Returns the connection URL for the mobile app to use to connect to the backend.\n   * Since you have to have a valid session to call /mobile/connect-info we can pre-register\n   * a temporary token for the user that is passed back to /mobile/register and can lookup\n   * who a device belongs to so we can scope it's access token.\n   * @param {import(\"@prisma/client\").users|null} user - User to get connection URL for in Multi-User Mode\n   * @returns {string}\n   */\n  connectionURL: function (user = null) {\n    let baseUrl = \"/api/mobile\";\n    if (process.env.NODE_ENV === \"production\") baseUrl = \"/api/mobile\";\n    else\n      baseUrl = `http://${ip.address()}:${process.env.SERVER_PORT || 3001}/api/mobile`;\n\n    const tempToken = this.registerTempToken(user);\n    baseUrl = `${baseUrl}?t=${tempToken}`;\n    return baseUrl;\n  },\n\n  /**\n   * Creates a new device for the mobile app\n   * @param {object} params - The params to create the device with.\n   * @param {string} params.deviceOs - Device os to associate creation of key with.\n   * @param {string} params.deviceName - Device name to associate creation of key with.\n   * @param {number|null} params.userId - User id to associate creation of key with.\n   * @returns {Promise<{device: import(\"@prisma/client\").desktop_mobile_devices|null, error:string|null}>}\n   */\n  create: async function ({ deviceOs, deviceName, userId = null }) {\n    try {\n      if (!deviceOs || !deviceName)\n        return { device: null, error: \"Device OS and name are required\" };\n      if (!this.validDeviceOs.includes(deviceOs))\n        return { device: null, error: `Invalid device OS - ${deviceOs}` };\n\n      const device = await prisma.desktop_mobile_devices.create({\n        data: {\n          deviceName: String(deviceName),\n          deviceOs: String(deviceOs).toLowerCase(),\n          token: uuidv4(),\n          userId: userId ? Number(userId) : null,\n        },\n      });\n      return { device, error: null };\n    } catch (error) {\n      console.error(\"Failed to create mobile device\", error);\n      return { device: null, error: error.message };\n    }\n  },\n\n  /**\n   * Validated existing API key\n   * @param {string} id - Device id (db id)\n   * @param {object} updates - Updates to apply to device\n   * @returns {Promise<{device: import(\"@prisma/client\").desktop_mobile_devices|null, error:string|null}>}\n   */\n  update: async function (id, updates = {}) {\n    const device = await this.get({ id: parseInt(id) });\n    if (!device) return { device: null, error: \"Device not found\" };\n\n    const validUpdates = {};\n    for (const [key, value] of Object.entries(updates)) {\n      if (!this.writable.includes(key)) continue;\n      const validation = this.validators[key](value);\n      if (validation !== null) return { device: null, error: validation };\n      validUpdates[key] = value;\n    }\n    // If no updates, return the device.\n    if (Object.keys(validUpdates).length === 0) return { device, error: null };\n\n    const updatedDevice = await prisma.desktop_mobile_devices.update({\n      where: { id: device.id },\n      data: validUpdates,\n    });\n    return { device: updatedDevice, error: null };\n  },\n\n  /**\n   * Fetches mobile device by params.\n   * @param {object} clause - Prisma props for search\n   * @returns {Promise<import(\"@prisma/client\").desktop_mobile_devices[]>}\n   */\n  get: async function (clause = {}, include = null) {\n    try {\n      const device = await prisma.desktop_mobile_devices.findFirst({\n        where: clause,\n        ...(include !== null ? { include } : {}),\n      });\n      return device;\n    } catch (error) {\n      console.error(\"FAILED TO GET MOBILE DEVICE.\", error);\n      return [];\n    }\n  },\n\n  /**\n   * Deletes mobile device by db id.\n   * @param {number} id - database id of mobile device\n   * @returns {Promise<{success: boolean, error:string|null}>}\n   */\n  delete: async function (id) {\n    try {\n      await prisma.desktop_mobile_devices.delete({\n        where: { id: parseInt(id) },\n      });\n      return { success: true, error: null };\n    } catch (error) {\n      console.error(\"Failed to delete mobile device\", error);\n      return { success: false, error: error.message };\n    }\n  },\n\n  /**\n   * Gets mobile devices by params\n   * @param {object} clause\n   * @param {number|null} limit\n   * @param {object|null} orderBy\n   * @returns {Promise<import(\"@prisma/client\").desktop_mobile_devices[]>}\n   */\n  where: async function (\n    clause = {},\n    limit = null,\n    orderBy = null,\n    include = null\n  ) {\n    try {\n      const devices = await prisma.desktop_mobile_devices.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n        ...(include !== null ? { include } : {}),\n      });\n      return devices;\n    } catch (error) {\n      console.error(\"FAILED TO GET MOBILE DEVICES.\", error.message);\n      return [];\n    }\n  },\n};\n\nmodule.exports = { MobileDevice };\n"
  },
  {
    "path": "server/models/passwordRecovery.js",
    "content": "const { v4 } = require(\"uuid\");\nconst prisma = require(\"../utils/prisma\");\nconst bcrypt = require(\"bcryptjs\");\n\nconst RecoveryCode = {\n  tablename: \"recovery_codes\",\n  writable: [],\n  create: async function (userId, code) {\n    try {\n      const codeHash = await bcrypt.hash(code, 10);\n      const recoveryCode = await prisma.recovery_codes.create({\n        data: { user_id: userId, code_hash: codeHash },\n      });\n      return { recoveryCode, error: null };\n    } catch (error) {\n      console.error(\"FAILED TO CREATE RECOVERY CODE.\", error.message);\n      return { recoveryCode: null, error: error.message };\n    }\n  },\n  createMany: async function (data) {\n    try {\n      const recoveryCodes = await prisma.$transaction(\n        data.map((recoveryCode) =>\n          prisma.recovery_codes.create({ data: recoveryCode })\n        )\n      );\n      return { recoveryCodes, error: null };\n    } catch (error) {\n      console.error(\"FAILED TO CREATE RECOVERY CODES.\", error.message);\n      return { recoveryCodes: null, error: error.message };\n    }\n  },\n  findFirst: async function (clause = {}) {\n    try {\n      const recoveryCode = await prisma.recovery_codes.findFirst({\n        where: clause,\n      });\n      return recoveryCode;\n    } catch (error) {\n      console.error(\"FAILED TO FIND RECOVERY CODE.\", error.message);\n      return null;\n    }\n  },\n  findMany: async function (clause = {}) {\n    try {\n      const recoveryCodes = await prisma.recovery_codes.findMany({\n        where: clause,\n      });\n      return recoveryCodes;\n    } catch (error) {\n      console.error(\"FAILED TO FIND RECOVERY CODES.\", error.message);\n      return null;\n    }\n  },\n  deleteMany: async function (clause = {}) {\n    try {\n      await prisma.recovery_codes.deleteMany({ where: clause });\n      return true;\n    } catch (error) {\n      console.error(\"FAILED TO DELETE RECOVERY CODES.\", error.message);\n      return false;\n    }\n  },\n  hashesForUser: async function (userId = null) {\n    if (!userId) return [];\n    return (await this.findMany({ user_id: userId })).map(\n      (recovery) => recovery.code_hash\n    );\n  },\n};\n\nconst PasswordResetToken = {\n  tablename: \"password_reset_tokens\",\n  resetExpiryMs: 600_000, // 10 minutes in ms;\n  writable: [],\n  calcExpiry: function () {\n    return new Date(Date.now() + this.resetExpiryMs);\n  },\n  create: async function (userId) {\n    try {\n      const passwordResetToken = await prisma.password_reset_tokens.create({\n        data: { user_id: userId, token: v4(), expiresAt: this.calcExpiry() },\n      });\n      return { passwordResetToken, error: null };\n    } catch (error) {\n      console.error(\"FAILED TO CREATE PASSWORD RESET TOKEN.\", error.message);\n      return { passwordResetToken: null, error: error.message };\n    }\n  },\n  findUnique: async function (clause = {}) {\n    try {\n      const passwordResetToken = await prisma.password_reset_tokens.findUnique({\n        where: clause,\n      });\n      return passwordResetToken;\n    } catch (error) {\n      console.error(\"FAILED TO FIND PASSWORD RESET TOKEN.\", error.message);\n      return null;\n    }\n  },\n  deleteMany: async function (clause = {}) {\n    try {\n      await prisma.password_reset_tokens.deleteMany({ where: clause });\n      return true;\n    } catch (error) {\n      console.error(\"FAILED TO DELETE PASSWORD RESET TOKEN.\", error.message);\n      return false;\n    }\n  },\n};\n\nmodule.exports = {\n  RecoveryCode,\n  PasswordResetToken,\n};\n"
  },
  {
    "path": "server/models/promptHistory.js",
    "content": "const prisma = require(\"../utils/prisma\");\n\nconst PromptHistory = {\n  new: async function ({ workspaceId, prompt, modifiedBy = null }) {\n    try {\n      const history = await prisma.prompt_history.create({\n        data: {\n          workspaceId: Number(workspaceId),\n          prompt: String(prompt),\n          modifiedBy: !!modifiedBy ? Number(modifiedBy) : null,\n        },\n      });\n      return { history, message: null };\n    } catch (error) {\n      console.error(error.message);\n      return { history: null, message: error.message };\n    }\n  },\n\n  /**\n   * Get the prompt history for a workspace.\n   * @param {number} workspaceId - The ID of the workspace to get prompt history for.\n   * @param {number|null} limit - The maximum number of history items to return.\n   * @param {string|null} orderBy - The field to order the history by.\n   * @returns {Promise<Array<{id: number, prompt: string, modifiedAt: Date, modifiedBy: number, user: {username: string}}>>} A promise that resolves to an array of prompt history objects.\n   */\n  forWorkspace: async function (\n    workspaceId = null,\n    limit = null,\n    orderBy = null\n  ) {\n    if (!workspaceId) return [];\n    try {\n      const history = await prisma.prompt_history.findMany({\n        where: { workspaceId: Number(workspaceId) },\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null\n          ? { orderBy }\n          : { orderBy: { modifiedAt: \"desc\" } }),\n        include: {\n          user: {\n            select: {\n              username: true,\n            },\n          },\n        },\n      });\n      return history;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  get: async function (clause = {}, limit = null, orderBy = null) {\n    try {\n      const history = await prisma.prompt_history.findFirst({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n        include: {\n          user: {\n            select: {\n              id: true,\n              username: true,\n              role: true,\n            },\n          },\n        },\n      });\n      return history || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.prompt_history.deleteMany({ where: clause });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  /**\n   * Utility method to handle prompt changes and create history entries\n   * @param {import('./workspace').Workspace} workspaceData - The workspace object (previous state)\n   * @param {{id: number, role: string}|null} user - The user making the change\n   * @returns {Promise<void>}\n   */\n  handlePromptChange: async function (workspaceData, user = null) {\n    try {\n      await this.new({\n        workspaceId: workspaceData.id,\n        prompt: workspaceData.openAiPrompt, // Store previous prompt as history\n        modifiedBy: user?.id,\n      });\n    } catch (error) {\n      console.error(\"Failed to create prompt history:\", error.message);\n    }\n  },\n};\n\nmodule.exports = { PromptHistory };\n"
  },
  {
    "path": "server/models/slashCommandsPresets.js",
    "content": "const { v4 } = require(\"uuid\");\nconst prisma = require(\"../utils/prisma\");\nconst CMD_REGEX = new RegExp(/[^a-zA-Z0-9_-]/g);\n\nconst SlashCommandPresets = {\n  formatCommand: function (command = \"\") {\n    if (!command || command.length < 2) return `/${v4().split(\"-\")[0]}`;\n\n    let adjustedCmd = command.toLowerCase(); // force lowercase\n    if (!adjustedCmd.startsWith(\"/\")) adjustedCmd = `/${adjustedCmd}`; // Fix if no preceding / is found.\n    return `/${adjustedCmd.slice(1).toLowerCase().replace(CMD_REGEX, \"-\")}`; // replace any invalid chars with '-'\n  },\n\n  get: async function (clause = {}) {\n    try {\n      const preset = await prisma.slash_command_presets.findFirst({\n        where: clause,\n      });\n      return preset || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  where: async function (clause = {}, limit) {\n    try {\n      const presets = await prisma.slash_command_presets.findMany({\n        where: clause,\n        take: limit || undefined,\n      });\n      return presets;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  // Command + userId must be unique combination.\n  create: async function (userId = null, presetData = {}) {\n    try {\n      const existingPreset = await this.get({\n        userId: userId ? Number(userId) : null,\n        command: String(presetData.command),\n      });\n\n      if (existingPreset) {\n        console.log(\n          \"SlashCommandPresets.create - preset already exists - will not create\"\n        );\n        return existingPreset;\n      }\n\n      const preset = await prisma.slash_command_presets.create({\n        data: {\n          ...presetData,\n          // This field (uid) is either the user_id or 0 (for non-multi-user mode).\n          // the UID field enforces the @@unique(userId, command) constraint since\n          // the real relational field (userId) cannot be non-null so this 'dummy' field gives us something\n          // to constrain against within the context of prisma and sqlite that works.\n          uid: userId ? Number(userId) : 0,\n          userId: userId ? Number(userId) : null,\n        },\n      });\n      return preset;\n    } catch (error) {\n      console.error(\"Failed to create preset\", error.message);\n      return null;\n    }\n  },\n\n  getUserPresets: async function (userId = null) {\n    try {\n      return (\n        await prisma.slash_command_presets.findMany({\n          where: { userId: !!userId ? Number(userId) : null },\n          orderBy: { createdAt: \"asc\" },\n        })\n      )?.map((preset) => ({\n        id: preset.id,\n        command: preset.command,\n        prompt: preset.prompt,\n        description: preset.description,\n      }));\n    } catch (error) {\n      console.error(\"Failed to get user presets\", error.message);\n      return [];\n    }\n  },\n\n  update: async function (presetId = null, presetData = {}) {\n    try {\n      const preset = await prisma.slash_command_presets.update({\n        where: { id: Number(presetId) },\n        data: presetData,\n      });\n      return preset;\n    } catch (error) {\n      console.error(\"Failed to update preset\", error.message);\n      return null;\n    }\n  },\n\n  delete: async function (presetId = null) {\n    try {\n      await prisma.slash_command_presets.delete({\n        where: { id: Number(presetId) },\n      });\n      return true;\n    } catch (error) {\n      console.error(\"Failed to delete preset\", error.message);\n      return false;\n    }\n  },\n};\n\nmodule.exports.SlashCommandPresets = SlashCommandPresets;\n"
  },
  {
    "path": "server/models/systemPromptVariables.js",
    "content": "const prisma = require(\"../utils/prisma\");\nconst moment = require(\"moment\");\n\n/**\n * @typedef {Object} SystemPromptVariable\n * @property {number} id\n * @property {string} key\n * @property {string|function} value\n * @property {string} description\n * @property {'system'|'user'|'workspace'|'static'} type\n * @property {number} userId\n * @property {boolean} multiUserRequired\n */\n\nconst SystemPromptVariables = {\n  VALID_TYPES: [\"user\", \"workspace\", \"system\", \"static\"],\n  DEFAULT_VARIABLES: [\n    {\n      key: \"time\",\n      value: () => moment().format(\"LTS\"),\n      description: \"Current time\",\n      type: \"system\",\n      multiUserRequired: false,\n    },\n    {\n      key: \"date\",\n      value: () => moment().format(\"LL\"),\n      description: \"Current date\",\n      type: \"system\",\n      multiUserRequired: false,\n    },\n    {\n      key: \"datetime\",\n      value: () => moment().format(\"LLLL\"),\n      description: \"Current date and time\",\n      type: \"system\",\n      multiUserRequired: false,\n    },\n    {\n      key: \"user.id\",\n      value: (userId = null) => {\n        if (!userId) return \"[User ID]\";\n        return userId;\n      },\n      description: \"Current user's ID\",\n      type: \"user\",\n      multiUserRequired: true,\n    },\n    {\n      key: \"user.name\",\n      value: async (userId = null) => {\n        if (!userId) return \"[User name]\";\n        try {\n          const user = await prisma.users.findUnique({\n            where: { id: Number(userId) },\n            select: { username: true },\n          });\n          return user?.username || \"[User name is empty or unknown]\";\n        } catch (error) {\n          console.error(\"Error fetching user name:\", error);\n          return \"[User name is empty or unknown]\";\n        }\n      },\n      description: \"Current user's username\",\n      type: \"user\",\n      multiUserRequired: true,\n    },\n    {\n      key: \"user.bio\",\n      value: async (userId = null) => {\n        if (!userId) return \"[User bio]\";\n        try {\n          const user = await prisma.users.findUnique({\n            where: { id: Number(userId) },\n            select: { bio: true },\n          });\n          return user?.bio || \"[User bio is empty]\";\n        } catch (error) {\n          console.error(\"Error fetching user bio:\", error);\n          return \"[User bio is empty]\";\n        }\n      },\n      description: \"Current user's bio field from their profile\",\n      type: \"user\",\n      multiUserRequired: true,\n    },\n    {\n      key: \"workspace.id\",\n      value: (workspaceId = null) => {\n        if (!workspaceId) return \"[Workspace ID]\";\n        return workspaceId;\n      },\n      description: \"Current workspace's ID\",\n      type: \"workspace\",\n      multiUserRequired: false,\n    },\n    {\n      key: \"workspace.name\",\n      value: async (workspaceId = null) => {\n        if (!workspaceId) return \"[Workspace name]\";\n        const workspace = await prisma.workspaces.findUnique({\n          where: { id: Number(workspaceId) },\n          select: { name: true },\n        });\n        return workspace?.name || \"[Workspace name is empty or unknown]\";\n      },\n      description: \"Current workspace's name\",\n      type: \"workspace\",\n      multiUserRequired: false,\n    },\n  ],\n\n  /**\n   * Gets a system prompt variable by its key\n   * @param {string} key\n   * @returns {Promise<SystemPromptVariable>}\n   */\n  get: async function (key = null) {\n    if (!key) return null;\n    const variable = await prisma.system_prompt_variables.findUnique({\n      where: { key: String(key) },\n    });\n    return variable;\n  },\n\n  /**\n   * Retrieves all system prompt variables with dynamic variables as well\n   * as user defined variables\n   * @param {number|null} userId - the current user ID (determines if in multi-user mode)\n   * @returns {Promise<SystemPromptVariable[]>}\n   */\n  getAll: async function (userId = null) {\n    // All user-defined system variables are available to everyone globally since only admins can create them.\n    const userDefinedSystemVariables =\n      await prisma.system_prompt_variables.findMany();\n    const formattedDbVars = userDefinedSystemVariables.map((v) => ({\n      id: v.id,\n      key: v.key,\n      value: v.value,\n      description: v.description,\n      type: v.type,\n      userId: v.userId,\n    }));\n\n    // If userId is not provided, filter the default variables to only include non-multiUserRequired variables\n    // since we wont be able to dynamically inject user-related content.\n    const defaultSystemVariables = !userId\n      ? this.DEFAULT_VARIABLES.filter((v) => !v.multiUserRequired)\n      : this.DEFAULT_VARIABLES;\n\n    return [...defaultSystemVariables, ...formattedDbVars];\n  },\n\n  /**\n   * Creates a new system prompt variable\n   * @param {{ key: string, value: string, description: string, type: string, userId: number }} data\n   * @returns {Promise<SystemPromptVariable>}\n   */\n  create: async function ({\n    key,\n    value,\n    description = null,\n    type = \"static\",\n    userId = null,\n  }) {\n    await this._checkVariableKey(key, true);\n    return await prisma.system_prompt_variables.create({\n      data: {\n        key: String(key),\n        value: String(value),\n        description: description ? String(description) : null,\n        type: type ? String(type) : \"static\",\n        userId: userId ? Number(userId) : null,\n      },\n    });\n  },\n\n  /**\n   * Updates a system prompt variable by its unique database ID\n   * @param {number} id\n   * @param {{ key: string, value: string, description: string }} data\n   * @returns {Promise<SystemPromptVariable>}\n   */\n  update: async function (id, { key, value, description = null }) {\n    if (!id || !key || !value) return null;\n    const existingRecord = await prisma.system_prompt_variables.findFirst({\n      where: { id: Number(id) },\n    });\n    if (!existingRecord) throw new Error(\"System prompt variable not found\");\n    await this._checkVariableKey(key, false);\n\n    return await prisma.system_prompt_variables.update({\n      where: { id: existingRecord.id },\n      data: {\n        key: String(key),\n        value: String(value),\n        description: description ? String(description) : null,\n      },\n    });\n  },\n\n  /**\n   * Deletes a system prompt variable by its unique database ID\n   * @param {number} id\n   * @returns {Promise<boolean>}\n   */\n  delete: async function (id = null) {\n    try {\n      await prisma.system_prompt_variables.delete({\n        where: { id: Number(id) },\n      });\n      return true;\n    } catch (error) {\n      console.error(\"Error deleting variable:\", error);\n      return false;\n    }\n  },\n\n  /**\n   * Injects variables into a string based on the user ID and workspace ID (if provided) and the variables available\n   * @param {string} str - the input string to expand variables into\n   * @param {number|null} userId - the user ID to use for dynamic variables\n   * @param {number|null} workspaceId - the workspace ID to use for workspace variables\n   * @returns {Promise<string>}\n   */\n  expandSystemPromptVariables: async function (\n    str,\n    userId = null,\n    workspaceId = null\n  ) {\n    if (!str) return str;\n\n    try {\n      const allVariables = await this.getAll(userId);\n      let result = str;\n\n      // Find all variable patterns in the string\n      const matches = str.match(/\\{([^}]+)\\}/g) || [];\n\n      // Process each match\n      for (const match of matches) {\n        const key = match.substring(1, match.length - 1); // Remove { and }\n\n        // Determine if the variable is a class-based variable (workspace.X or user.X)\n        const isWorkspaceOrUserVariable = [\"workspace.\", \"user.\"].some(\n          (prefix) => key.startsWith(prefix)\n        );\n\n        // Handle class-based variables with current workspace's or user's data\n        if (isWorkspaceOrUserVariable) {\n          let variableTypeDisplay;\n          if (key.startsWith(\"workspace.\")) variableTypeDisplay = \"Workspace\";\n          else if (key.startsWith(\"user.\")) variableTypeDisplay = \"User\";\n          else throw new Error(`Invalid class-based variable: ${key}`);\n\n          // Get the property name after the prefix\n          const prop = key.split(\".\")[1];\n          const variable = allVariables.find((v) => v.key === key);\n\n          // If the variable is a function, call it to get the current value\n          if (variable && typeof variable.value === \"function\") {\n            // If the variable is an async function, call it to get the current value\n            if (variable.value.constructor.name === \"AsyncFunction\") {\n              let value;\n              try {\n                if (variableTypeDisplay === \"Workspace\")\n                  value = await variable.value(workspaceId);\n                else if (variableTypeDisplay === \"User\")\n                  value = await variable.value(userId);\n                else throw new Error(`Invalid class-based variable: ${key}`);\n              } catch (error) {\n                console.error(\n                  `Error processing ${variableTypeDisplay} variable ${key}:`,\n                  error\n                );\n                value = `[${variableTypeDisplay} ${prop}]`;\n              }\n              result = result.replace(match, value);\n            } else {\n              let value;\n              try {\n                // Call the variable function with the appropriate workspace or user ID\n                if (variableTypeDisplay === \"Workspace\")\n                  value = variable.value(workspaceId);\n                else if (variableTypeDisplay === \"User\")\n                  value = variable.value(userId);\n                else throw new Error(`Invalid class-based variable: ${key}`);\n              } catch (error) {\n                console.error(\n                  `Error processing ${variableTypeDisplay} variable ${key}:`,\n                  error\n                );\n                value = `[${variableTypeDisplay} ${prop}]`;\n              }\n              result = result.replace(match, value);\n            }\n          } else {\n            // If the variable is not a function, replace the match with the variable value\n            result = result.replace(match, `[${variableTypeDisplay} ${prop}]`);\n          }\n          continue;\n        }\n\n        // Handle regular variables (static types)\n        const variable = allVariables.find((v) => v.key === key);\n        if (!variable) continue;\n\n        // For dynamic and system variables, call the function to get the current value\n        if (\n          [\"system\"].includes(variable.type) &&\n          typeof variable.value === \"function\"\n        ) {\n          try {\n            if (variable.value.constructor.name === \"AsyncFunction\") {\n              const value = await variable.value(userId);\n              result = result.replace(match, value);\n            } else {\n              const value = variable.value();\n              result = result.replace(match, value);\n            }\n          } catch (error) {\n            console.error(`Error processing dynamic variable ${key}:`, error);\n            result = result.replace(match, match);\n          }\n        } else {\n          result = result.replace(match, variable.value || match);\n        }\n      }\n      return result;\n    } catch (error) {\n      console.error(\"Error in expandSystemPromptVariables:\", error);\n      return str;\n    }\n  },\n\n  /**\n   * Internal function to check if a variable key is valid\n   * @param {string} key\n   * @param {boolean} checkExisting\n   * @returns {Promise<boolean>}\n   */\n  _checkVariableKey: async function (key = null, checkExisting = true) {\n    if (!key) throw new Error(\"Key is required\");\n    if (typeof key !== \"string\") throw new Error(\"Key must be a string\");\n    if (!/^[a-zA-Z0-9_]+$/.test(key))\n      throw new Error(\"Key must contain only letters, numbers and underscores\");\n    if (key.length > 255)\n      throw new Error(\"Key must be less than 255 characters\");\n    if (key.length < 3) throw new Error(\"Key must be at least 3 characters\");\n    if (key.startsWith(\"user.\"))\n      throw new Error(\"Key cannot start with 'user.'\");\n    if (key.startsWith(\"system.\"))\n      throw new Error(\"Key cannot start with 'system.'\");\n    if (checkExisting && (await this.get(key)) !== null)\n      throw new Error(\"System prompt variable with this key already exists\");\n\n    return true;\n  },\n};\n\nmodule.exports = { SystemPromptVariables };\n"
  },
  {
    "path": "server/models/systemSettings.js",
    "content": "process.env.NODE_ENV === \"development\"\n  ? require(\"dotenv\").config({ path: `.env.${process.env.NODE_ENV}` })\n  : require(\"dotenv\").config();\n\nconst { default: slugify } = require(\"slugify\");\nconst { isValidUrl, safeJsonParse } = require(\"../utils/http\");\nconst prisma = require(\"../utils/prisma\");\nconst { MetaGenerator } = require(\"../utils/boot/MetaGenerator\");\nconst { PGVector } = require(\"../utils/vectorDbProviders/pgvector\");\nconst { NativeEmbedder } = require(\"../utils/EmbeddingEngines/native\");\nconst { getBaseLLMProviderModel } = require(\"../utils/helpers\");\nconst {\n  ConnectionStringParser,\n} = require(\"../utils/agents/aibitat/plugins/sql-agent/SQLConnectors/utils\");\n\nfunction isNullOrNaN(value) {\n  if (value === null) return true;\n  return isNaN(value);\n}\n\nconst SystemSettings = {\n  /** A default system prompt that is used when no other system prompt is set or available to the function caller. */\n  saneDefaultSystemPrompt:\n    \"Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.\",\n  protectedFields: [\"multi_user_mode\", \"hub_api_key\", \"onboarding_complete\"],\n  publicFields: [\n    \"footer_data\",\n    \"support_email\",\n    \"text_splitter_chunk_size\",\n    \"text_splitter_chunk_overlap\",\n    \"max_embed_chunk_size\",\n    \"agent_search_provider\",\n    \"agent_sql_connections\",\n    \"default_agent_skills\",\n    \"disabled_agent_skills\",\n    \"imported_agent_skills\",\n    \"custom_app_name\",\n    \"feature_flags\",\n    \"meta_page_title\",\n    \"meta_page_favicon\",\n  ],\n  supportedFields: [\n    \"logo_filename\",\n    \"telemetry_id\",\n    \"footer_data\",\n    \"support_email\",\n\n    \"text_splitter_chunk_size\",\n    \"text_splitter_chunk_overlap\",\n    \"agent_search_provider\",\n    \"default_agent_skills\",\n    \"disabled_agent_skills\",\n    \"agent_sql_connections\",\n    \"custom_app_name\",\n    \"default_system_prompt\",\n\n    // Meta page customization\n    \"meta_page_title\",\n    \"meta_page_favicon\",\n\n    // beta feature flags\n    \"experimental_live_file_sync\",\n\n    // Hub settings\n    \"hub_api_key\",\n  ],\n  validations: {\n    footer_data: (updates) => {\n      try {\n        const array = JSON.parse(updates)\n          .filter((setting) => isValidUrl(setting.url))\n          .slice(0, 3); // max of 3 items in footer.\n        return JSON.stringify(array);\n      } catch {\n        console.error(`Failed to run validation function on footer_data`);\n        return JSON.stringify([]);\n      }\n    },\n    text_splitter_chunk_size: (update) => {\n      try {\n        if (isNullOrNaN(update)) throw new Error(\"Value is not a number.\");\n        if (Number(update) <= 0) throw new Error(\"Value must be non-zero.\");\n        const { purgeEntireVectorCache } = require(\"../utils/files\");\n        purgeEntireVectorCache();\n        return Number(update);\n      } catch (e) {\n        console.error(\n          `Failed to run validation function on text_splitter_chunk_size`,\n          e.message\n        );\n        return 1000;\n      }\n    },\n    text_splitter_chunk_overlap: (update) => {\n      try {\n        if (isNullOrNaN(update)) throw new Error(\"Value is not a number\");\n        if (Number(update) < 0) throw new Error(\"Value cannot be less than 0.\");\n        const { purgeEntireVectorCache } = require(\"../utils/files\");\n        purgeEntireVectorCache();\n        return Number(update);\n      } catch (e) {\n        console.error(\n          `Failed to run validation function on text_splitter_chunk_overlap`,\n          e.message\n        );\n        return 20;\n      }\n    },\n    agent_search_provider: (update) => {\n      try {\n        if (update === \"none\") return null;\n        if (\n          ![\n            \"google-search-engine\",\n            \"serpapi\",\n            \"searchapi\",\n            \"serper-dot-dev\",\n            \"bing-search\",\n            \"serply-engine\",\n            \"searxng-engine\",\n            \"tavily-search\",\n            \"duckduckgo-engine\",\n            \"exa-search\",\n            \"perplexity-search\",\n          ].includes(update)\n        )\n          throw new Error(\"Invalid SERP provider.\");\n        return String(update);\n      } catch (e) {\n        console.error(\n          `Failed to run validation function on agent_search_provider`,\n          e.message\n        );\n        return null;\n      }\n    },\n    default_agent_skills: (updates) => {\n      try {\n        const skills = updates.split(\",\").filter((skill) => !!skill);\n        return JSON.stringify(skills);\n      } catch {\n        console.error(`Could not validate agent skills.`);\n        return JSON.stringify([]);\n      }\n    },\n    disabled_agent_skills: (updates) => {\n      try {\n        const skills = updates.split(\",\").filter((skill) => !!skill);\n        return JSON.stringify(skills);\n      } catch {\n        console.error(`Could not validate disabled agent skills.`);\n        return JSON.stringify([]);\n      }\n    },\n    agent_sql_connections: async (updates) => {\n      const existingConnections = safeJsonParse(\n        (await SystemSettings.get({ label: \"agent_sql_connections\" }))?.value,\n        []\n      );\n      try {\n        const updatedConnections = mergeConnections(\n          existingConnections,\n          safeJsonParse(updates, [])\n        );\n        return JSON.stringify(updatedConnections);\n      } catch {\n        console.error(`Failed to merge connections`);\n        return JSON.stringify(existingConnections ?? []);\n      }\n    },\n    experimental_live_file_sync: (update) => {\n      if (typeof update === \"boolean\")\n        return update === true ? \"enabled\" : \"disabled\";\n      if (![\"enabled\", \"disabled\"].includes(update)) return \"disabled\";\n      return String(update);\n    },\n    meta_page_title: (newTitle) => {\n      try {\n        if (typeof newTitle !== \"string\" || !newTitle) return null;\n        return String(newTitle);\n      } catch {\n        return null;\n      } finally {\n        new MetaGenerator().clearConfig();\n      }\n    },\n    meta_page_favicon: (faviconUrl) => {\n      if (!faviconUrl) return null;\n      try {\n        const url = new URL(faviconUrl);\n        return url.toString();\n      } catch {\n        return null;\n      } finally {\n        new MetaGenerator().clearConfig();\n      }\n    },\n    hub_api_key: (apiKey) => {\n      if (!apiKey) return null;\n      return String(apiKey);\n    },\n    default_system_prompt: (prompt) => {\n      if (typeof prompt !== \"string\" || !prompt) return null;\n      if (prompt.trim() === SystemSettings.saneDefaultSystemPrompt)\n        return SystemSettings.saneDefaultSystemPrompt;\n      return String(prompt.trim());\n    },\n  },\n  currentSettings: async function () {\n    const { hasVectorCachedFiles } = require(\"../utils/files\");\n    const {\n      ToolReranker,\n    } = require(\"../utils/agents/aibitat/utils/toolReranker\");\n    const AIbitat = require(\"../utils/agents/aibitat\");\n\n    const llmProvider = process.env.LLM_PROVIDER;\n    const vectorDB = process.env.VECTOR_DB;\n    const embeddingEngine = process.env.EMBEDDING_ENGINE ?? \"native\";\n    return {\n      // --------------------------------------------------------\n      // General Settings\n      // --------------------------------------------------------\n      RequiresAuth: !!process.env.AUTH_TOKEN,\n      AuthToken: !!process.env.AUTH_TOKEN,\n      JWTSecret: !!process.env.JWT_SECRET,\n      StorageDir: process.env.STORAGE_DIR,\n      MultiUserMode: await this.isMultiUserMode(),\n      DisableTelemetry: process.env.DISABLE_TELEMETRY || \"false\",\n\n      // --------------------------------------------------------\n      // Embedder Provider Selection Settings & Configs\n      // --------------------------------------------------------\n      EmbeddingEngine: embeddingEngine,\n      HasExistingEmbeddings: await this.hasEmbeddings(), // check if they have any currently embedded documents active in workspaces.\n      HasCachedEmbeddings: hasVectorCachedFiles(), // check if they any currently cached embedded docs.\n      EmbeddingBasePath: process.env.EMBEDDING_BASE_PATH,\n      EmbeddingModelPref:\n        embeddingEngine === \"native\"\n          ? NativeEmbedder._getEmbeddingModel()\n          : process.env.EMBEDDING_MODEL_PREF,\n      EmbeddingOutputDimensions:\n        process.env.EMBEDDING_OUTPUT_DIMENSIONS || null,\n      EmbeddingModelMaxChunkLength:\n        process.env.EMBEDDING_MODEL_MAX_CHUNK_LENGTH,\n      OllamaEmbeddingBatchSize: process.env.OLLAMA_EMBEDDING_BATCH_SIZE || 1,\n      VoyageAiApiKey: !!process.env.VOYAGEAI_API_KEY,\n      GenericOpenAiEmbeddingApiKey:\n        !!process.env.GENERIC_OPEN_AI_EMBEDDING_API_KEY,\n      GenericOpenAiEmbeddingMaxConcurrentChunks:\n        process.env.GENERIC_OPEN_AI_EMBEDDING_MAX_CONCURRENT_CHUNKS || 500,\n      GeminiEmbeddingApiKey: !!process.env.GEMINI_EMBEDDING_API_KEY,\n\n      // --------------------------------------------------------\n      // VectorDB Provider Selection Settings & Configs\n      // --------------------------------------------------------\n      VectorDB: vectorDB,\n      ...this.vectorDBPreferenceKeys(),\n\n      // --------------------------------------------------------\n      // LLM Provider Selection Settings & Configs\n      // --------------------------------------------------------\n      LLMProvider: llmProvider,\n      LLMModel: getBaseLLMProviderModel({ provider: llmProvider }) || null,\n      ...this.llmPreferenceKeys(),\n\n      // --------------------------------------------------------\n      // Whisper (Audio transcription) Selection Settings & Configs\n      // - Currently the only 3rd party is OpenAI, so is OPEN_AI_KEY is set\n      // - then it can be shared.\n      // --------------------------------------------------------\n      WhisperProvider: process.env.WHISPER_PROVIDER || \"local\",\n      WhisperModelPref:\n        process.env.WHISPER_MODEL_PREF || \"Xenova/whisper-small\",\n\n      // --------------------------------------------------------\n      // TTS/STT  Selection Settings & Configs\n      // - Currently the only 3rd party is OpenAI or the native browser-built in\n      // --------------------------------------------------------\n      TextToSpeechProvider: process.env.TTS_PROVIDER || \"native\",\n      TTSOpenAIKey: !!process.env.TTS_OPEN_AI_KEY,\n      TTSOpenAIVoiceModel: process.env.TTS_OPEN_AI_VOICE_MODEL,\n\n      // Eleven Labs TTS\n      TTSElevenLabsKey: !!process.env.TTS_ELEVEN_LABS_KEY,\n      TTSElevenLabsVoiceModel: process.env.TTS_ELEVEN_LABS_VOICE_MODEL,\n      // Piper TTS\n      TTSPiperTTSVoiceModel:\n        process.env.TTS_PIPER_VOICE_MODEL ?? \"en_US-hfc_female-medium\",\n      // OpenAI Generic TTS\n      TTSOpenAICompatibleKey: !!process.env.TTS_OPEN_AI_COMPATIBLE_KEY,\n      TTSOpenAICompatibleModel: process.env.TTS_OPEN_AI_COMPATIBLE_MODEL,\n      TTSOpenAICompatibleVoiceModel:\n        process.env.TTS_OPEN_AI_COMPATIBLE_VOICE_MODEL,\n      TTSOpenAICompatibleEndpoint: process.env.TTS_OPEN_AI_COMPATIBLE_ENDPOINT,\n\n      // --------------------------------------------------------\n      // Agent Settings & Configs\n      // --------------------------------------------------------\n      AgentSerpApiKey: !!process.env.AGENT_SERPAPI_API_KEY || null,\n      AgentSerpApiEngine: process.env.AGENT_SERPAPI_ENGINE || \"google\",\n      AgentSearchApiKey: !!process.env.AGENT_SEARCHAPI_API_KEY || null,\n      AgentSearchApiEngine: process.env.AGENT_SEARCHAPI_ENGINE || \"google\",\n      AgentSerperApiKey: !!process.env.AGENT_SERPER_DEV_KEY || null,\n      AgentBingSearchApiKey: !!process.env.AGENT_BING_SEARCH_API_KEY || null,\n      AgentSerplyApiKey: !!process.env.AGENT_SERPLY_API_KEY || null,\n      AgentSearXNGApiUrl: process.env.AGENT_SEARXNG_API_URL || null,\n      AgentTavilyApiKey: !!process.env.AGENT_TAVILY_API_KEY || null,\n      AgentExaApiKey: !!process.env.AGENT_EXA_API_KEY || null,\n      AgentPerplexityApiKey: !!process.env.AGENT_PERPLEXITY_API_KEY || null,\n\n      // --------------------------------------------------------\n      // Compliance Settings\n      // --------------------------------------------------------\n      // Disable View Chat History for the whole instance.\n      DisableViewChatHistory:\n        \"DISABLE_VIEW_CHAT_HISTORY\" in process.env || false,\n\n      // --------------------------------------------------------\n      // Simple SSO Settings\n      // --------------------------------------------------------\n      SimpleSSOEnabled: \"SIMPLE_SSO_ENABLED\" in process.env || false,\n      SimpleSSONoLogin: \"SIMPLE_SSO_NO_LOGIN\" in process.env || false,\n      SimpleSSONoLoginRedirect: this.simpleSSO.noLoginRedirect(),\n\n      // --------------------------------------------------------\n      // Agent Skill Settings\n      // --------------------------------------------------------\n      AgentSkillMaxToolCalls: AIbitat.defaultMaxToolCalls(),\n      AgentSkillRerankerEnabled: ToolReranker.isEnabled(),\n      AgentSkillRerankerTopN: ToolReranker.getTopN(),\n    };\n  },\n\n  get: async function (clause = {}) {\n    try {\n      const setting = await prisma.system_settings.findFirst({ where: clause });\n      return setting || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  getValueOrFallback: async function (clause = {}, fallback = null) {\n    try {\n      return (await this.get(clause))?.value ?? fallback;\n    } catch (error) {\n      console.error(error.message);\n      return fallback;\n    }\n  },\n\n  where: async function (clause = {}, limit) {\n    try {\n      const settings = await prisma.system_settings.findMany({\n        where: clause,\n        take: limit || undefined,\n      });\n      return settings;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  // Can take generic keys and will pre-filter invalid keys\n  // from the set before sending to the explicit update function\n  // that will then enforce validations as well.\n  updateSettings: async function (updates = {}) {\n    const validFields = Object.keys(updates).filter((key) =>\n      this.supportedFields.includes(key)\n    );\n\n    Object.entries(updates).forEach(([key]) => {\n      if (validFields.includes(key)) return;\n      delete updates[key];\n    });\n\n    return this._updateSettings(updates);\n  },\n\n  // Explicit update of settings + key validations.\n  // Only use this method when directly setting a key value\n  // that takes no user input for the keys being modified.\n  _updateSettings: async function (updates = {}) {\n    try {\n      const updatePromises = [];\n      for (const key of Object.keys(updates)) {\n        let validatedValue = updates[key];\n        if (this.validations.hasOwnProperty(key)) {\n          if (this.validations[key].constructor.name === \"AsyncFunction\") {\n            validatedValue = await this.validations[key](updates[key]);\n          } else {\n            validatedValue = this.validations[key](updates[key]);\n          }\n        }\n\n        updatePromises.push(\n          prisma.system_settings.upsert({\n            where: { label: key },\n            update: {\n              value: validatedValue === null ? null : String(validatedValue),\n            },\n            create: {\n              label: key,\n              value: validatedValue === null ? null : String(validatedValue),\n            },\n          })\n        );\n      }\n\n      await Promise.all(updatePromises);\n      return { success: true, error: null };\n    } catch (error) {\n      console.error(\"FAILED TO UPDATE SYSTEM SETTINGS\", error.message);\n      return { success: false, error: error.message };\n    }\n  },\n\n  isMultiUserMode: async function () {\n    try {\n      const setting = await this.get({ label: \"multi_user_mode\" });\n      return setting?.value === \"true\";\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  isOnboardingComplete: async function () {\n    try {\n      const setting = await this.get({ label: \"onboarding_complete\" });\n      return setting?.value === \"true\";\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  markOnboardingComplete: async function () {\n    try {\n      await this._updateSettings({ onboarding_complete: true });\n      const { Telemetry } = require(\"./telemetry\");\n      await Telemetry.sendTelemetry(\"onboarding_complete\");\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  currentLogoFilename: async function () {\n    try {\n      const setting = await this.get({ label: \"logo_filename\" });\n      return setting?.value || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  hasEmbeddings: async function () {\n    try {\n      const { Document } = require(\"./documents\");\n      const count = await Document.count({}, 1);\n      return count > 0;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  vectorDBPreferenceKeys: function () {\n    return {\n      // Pinecone DB Keys\n      PineConeKey: !!process.env.PINECONE_API_KEY,\n      PineConeIndex: process.env.PINECONE_INDEX,\n\n      // Chroma DB Keys\n      ChromaEndpoint: process.env.CHROMA_ENDPOINT,\n      ChromaApiHeader: process.env.CHROMA_API_HEADER,\n      ChromaApiKey: !!process.env.CHROMA_API_KEY,\n\n      // ChromaCloud DB Keys\n      ChromaCloudApiKey: !!process.env.CHROMACLOUD_API_KEY,\n      ChromaCloudTenant: process.env.CHROMACLOUD_TENANT,\n      ChromaCloudDatabase: process.env.CHROMACLOUD_DATABASE,\n\n      // Weaviate DB Keys\n      WeaviateEndpoint: process.env.WEAVIATE_ENDPOINT,\n      WeaviateApiKey: !!process.env.WEAVIATE_API_KEY,\n\n      // QDrant DB Keys\n      QdrantEndpoint: process.env.QDRANT_ENDPOINT,\n      QdrantApiKey: !!process.env.QDRANT_API_KEY,\n\n      // Milvus DB Keys\n      MilvusAddress: process.env.MILVUS_ADDRESS,\n      MilvusUsername: process.env.MILVUS_USERNAME,\n      MilvusPassword: !!process.env.MILVUS_PASSWORD,\n\n      // Zilliz DB Keys\n      ZillizEndpoint: process.env.ZILLIZ_ENDPOINT,\n      ZillizApiToken: !!process.env.ZILLIZ_API_TOKEN,\n\n      // AstraDB Keys\n      AstraDBApplicationToken: !!process?.env?.ASTRA_DB_APPLICATION_TOKEN,\n      AstraDBEndpoint: process?.env?.ASTRA_DB_ENDPOINT,\n\n      // PGVector Keys\n      PGVectorConnectionString: !!PGVector.connectionString() || false,\n      PGVectorTableName: PGVector.tableName(),\n    };\n  },\n\n  llmPreferenceKeys: function () {\n    return {\n      // OpenAI Keys\n      OpenAiKey: !!process.env.OPEN_AI_KEY,\n      OpenAiModelPref: process.env.OPEN_MODEL_PREF || \"gpt-4o\",\n\n      // Azure + OpenAI Keys\n      AzureOpenAiEndpoint: process.env.AZURE_OPENAI_ENDPOINT,\n      AzureOpenAiKey: !!process.env.AZURE_OPENAI_KEY,\n      AzureOpenAiModelPref:\n        process.env.AZURE_OPENAI_MODEL_PREF || process.env.OPEN_MODEL_PREF,\n      AzureOpenAiEmbeddingModelPref: process.env.EMBEDDING_MODEL_PREF,\n      AzureOpenAiTokenLimit: process.env.AZURE_OPENAI_TOKEN_LIMIT || 4096,\n      AzureOpenAiModelType: process.env.AZURE_OPENAI_MODEL_TYPE || \"default\",\n\n      // Anthropic Keys\n      AnthropicApiKey: !!process.env.ANTHROPIC_API_KEY,\n      AnthropicModelPref: process.env.ANTHROPIC_MODEL_PREF || \"claude-2\",\n      AnthropicCacheControl: process.env.ANTHROPIC_CACHE_CONTROL || \"none\",\n\n      // Gemini Keys\n      GeminiLLMApiKey: !!process.env.GEMINI_API_KEY,\n      GeminiLLMModelPref:\n        process.env.GEMINI_LLM_MODEL_PREF || \"gemini-2.0-flash-lite\",\n      GeminiSafetySetting:\n        process.env.GEMINI_SAFETY_SETTING || \"BLOCK_MEDIUM_AND_ABOVE\",\n\n      // LMStudio Keys\n      LMStudioBasePath: process.env.LMSTUDIO_BASE_PATH,\n      LMStudioTokenLimit: process.env.LMSTUDIO_MODEL_TOKEN_LIMIT || null,\n      LMStudioModelPref: process.env.LMSTUDIO_MODEL_PREF,\n      LMStudioAuthToken: !!process.env.LMSTUDIO_AUTH_TOKEN,\n\n      // LocalAI Keys\n      LocalAiApiKey: !!process.env.LOCAL_AI_API_KEY,\n      LocalAiBasePath: process.env.LOCAL_AI_BASE_PATH,\n      LocalAiModelPref: process.env.LOCAL_AI_MODEL_PREF,\n      LocalAiTokenLimit: process.env.LOCAL_AI_MODEL_TOKEN_LIMIT,\n\n      // Ollama LLM Keys\n      OllamaLLMAuthToken: !!process.env.OLLAMA_AUTH_TOKEN,\n      OllamaLLMBasePath: process.env.OLLAMA_BASE_PATH,\n      OllamaLLMModelPref: process.env.OLLAMA_MODEL_PREF,\n      OllamaLLMTokenLimit: process.env.OLLAMA_MODEL_TOKEN_LIMIT || null,\n      OllamaLLMKeepAliveSeconds: process.env.OLLAMA_KEEP_ALIVE_TIMEOUT ?? 300,\n\n      // Novita LLM Keys\n      NovitaLLMApiKey: !!process.env.NOVITA_LLM_API_KEY,\n      NovitaLLMModelPref: process.env.NOVITA_LLM_MODEL_PREF,\n      NovitaLLMTimeout: process.env.NOVITA_LLM_TIMEOUT_MS,\n\n      // TogetherAI Keys\n      TogetherAiApiKey: !!process.env.TOGETHER_AI_API_KEY,\n      TogetherAiModelPref: process.env.TOGETHER_AI_MODEL_PREF,\n\n      // Fireworks AI API Keys\n      FireworksAiLLMApiKey: !!process.env.FIREWORKS_AI_LLM_API_KEY,\n      FireworksAiLLMModelPref: process.env.FIREWORKS_AI_LLM_MODEL_PREF,\n\n      // Perplexity AI Keys\n      PerplexityApiKey: !!process.env.PERPLEXITY_API_KEY,\n      PerplexityModelPref: process.env.PERPLEXITY_MODEL_PREF,\n\n      // OpenRouter Keys\n      OpenRouterApiKey: !!process.env.OPENROUTER_API_KEY,\n      OpenRouterModelPref: process.env.OPENROUTER_MODEL_PREF,\n      OpenRouterTimeout: process.env.OPENROUTER_TIMEOUT_MS,\n\n      // Mistral AI (API) Keys\n      MistralApiKey: !!process.env.MISTRAL_API_KEY,\n      MistralModelPref: process.env.MISTRAL_MODEL_PREF,\n\n      // Groq AI API Keys\n      GroqApiKey: !!process.env.GROQ_API_KEY,\n      GroqModelPref: process.env.GROQ_MODEL_PREF,\n\n      // HuggingFace Dedicated Inference\n      HuggingFaceLLMEndpoint: process.env.HUGGING_FACE_LLM_ENDPOINT,\n      HuggingFaceLLMAccessToken: !!process.env.HUGGING_FACE_LLM_API_KEY,\n      HuggingFaceLLMTokenLimit: process.env.HUGGING_FACE_LLM_TOKEN_LIMIT,\n\n      // KoboldCPP Keys\n      KoboldCPPModelPref: process.env.KOBOLD_CPP_MODEL_PREF,\n      KoboldCPPBasePath: process.env.KOBOLD_CPP_BASE_PATH,\n      KoboldCPPTokenLimit: process.env.KOBOLD_CPP_MODEL_TOKEN_LIMIT,\n      KoboldCPPMaxTokens: process.env.KOBOLD_CPP_MAX_TOKENS,\n\n      // Text Generation Web UI Keys\n      TextGenWebUIBasePath: process.env.TEXT_GEN_WEB_UI_BASE_PATH,\n      TextGenWebUITokenLimit: process.env.TEXT_GEN_WEB_UI_MODEL_TOKEN_LIMIT,\n      TextGenWebUIAPIKey: !!process.env.TEXT_GEN_WEB_UI_API_KEY,\n\n      // LiteLLM Keys\n      LiteLLMModelPref: process.env.LITE_LLM_MODEL_PREF,\n      LiteLLMTokenLimit: process.env.LITE_LLM_MODEL_TOKEN_LIMIT,\n      LiteLLMBasePath: process.env.LITE_LLM_BASE_PATH,\n      LiteLLMApiKey: !!process.env.LITE_LLM_API_KEY,\n\n      // Moonshot AI Keys\n      MoonshotAiApiKey: !!process.env.MOONSHOT_AI_API_KEY,\n      MoonshotAiModelPref:\n        process.env.MOONSHOT_AI_MODEL_PREF || \"moonshot-v1-32k\",\n\n      // Generic OpenAI Keys\n      GenericOpenAiBasePath: process.env.GENERIC_OPEN_AI_BASE_PATH,\n      GenericOpenAiModelPref: process.env.GENERIC_OPEN_AI_MODEL_PREF,\n      GenericOpenAiTokenLimit: process.env.GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT,\n      GenericOpenAiKey: !!process.env.GENERIC_OPEN_AI_API_KEY,\n      GenericOpenAiMaxTokens: process.env.GENERIC_OPEN_AI_MAX_TOKENS,\n\n      // Foundry Keys\n      FoundryBasePath: process.env.FOUNDRY_BASE_PATH,\n      FoundryModelPref: process.env.FOUNDRY_MODEL_PREF,\n      FoundryModelTokenLimit: process.env.FOUNDRY_MODEL_TOKEN_LIMIT,\n\n      AwsBedrockLLMConnectionMethod:\n        process.env.AWS_BEDROCK_LLM_CONNECTION_METHOD || \"iam\",\n      AwsBedrockLLMAccessKeyId: !!process.env.AWS_BEDROCK_LLM_ACCESS_KEY_ID,\n      AwsBedrockLLMAccessKey: !!process.env.AWS_BEDROCK_LLM_ACCESS_KEY,\n      AwsBedrockLLMSessionToken: !!process.env.AWS_BEDROCK_LLM_SESSION_TOKEN,\n      AwsBedrockLLMAPIKey: !!process.env.AWS_BEDROCK_LLM_API_KEY,\n      AwsBedrockLLMRegion: process.env.AWS_BEDROCK_LLM_REGION,\n      AwsBedrockLLMModel: process.env.AWS_BEDROCK_LLM_MODEL_PREFERENCE,\n      AwsBedrockLLMTokenLimit:\n        process.env.AWS_BEDROCK_LLM_MODEL_TOKEN_LIMIT || 8192,\n      AwsBedrockLLMMaxOutputTokens:\n        process.env.AWS_BEDROCK_LLM_MAX_OUTPUT_TOKENS || 4096,\n\n      // Cohere API Keys\n      CohereApiKey: !!process.env.COHERE_API_KEY,\n      CohereModelPref: process.env.COHERE_MODEL_PREF,\n\n      // DeepSeek API Keys\n      DeepSeekApiKey: !!process.env.DEEPSEEK_API_KEY,\n      DeepSeekModelPref: process.env.DEEPSEEK_MODEL_PREF,\n\n      // APIPie LLM API Keys\n      ApipieLLMApiKey: !!process.env.APIPIE_LLM_API_KEY,\n      ApipieLLMModelPref: process.env.APIPIE_LLM_MODEL_PREF,\n\n      // xAI LLM API Keys\n      XAIApiKey: !!process.env.XAI_LLM_API_KEY,\n      XAIModelPref: process.env.XAI_LLM_MODEL_PREF,\n\n      // NVIDIA NIM Keys\n      NvidiaNimLLMBasePath: process.env.NVIDIA_NIM_LLM_BASE_PATH,\n      NvidiaNimLLMModelPref: process.env.NVIDIA_NIM_LLM_MODEL_PREF,\n      NvidiaNimLLMTokenLimit: process.env.NVIDIA_NIM_LLM_MODEL_TOKEN_LIMIT,\n\n      // PPIO API keys\n      PPIOApiKey: !!process.env.PPIO_API_KEY,\n      PPIOModelPref: process.env.PPIO_MODEL_PREF,\n\n      // Dell Pro AI Studio Keys\n      DellProAiStudioBasePath: process.env.DPAIS_LLM_BASE_PATH,\n      DellProAiStudioModelPref: process.env.DPAIS_LLM_MODEL_PREF,\n      DellProAiStudioTokenLimit:\n        process.env.DPAIS_LLM_MODEL_TOKEN_LIMIT ?? 4096,\n\n      // CometAPI LLM Keys\n      CometApiLLMApiKey: !!process.env.COMETAPI_LLM_API_KEY,\n      CometApiLLMModelPref: process.env.COMETAPI_LLM_MODEL_PREF,\n      CometApiLLMTimeout: process.env.COMETAPI_LLM_TIMEOUT_MS,\n\n      // Z.AI Keys\n      ZAiApiKey: !!process.env.ZAI_API_KEY,\n      ZAiModelPref: process.env.ZAI_MODEL_PREF,\n\n      // GiteeAI API Keys\n      GiteeAIApiKey: !!process.env.GITEE_AI_API_KEY,\n      GiteeAIModelPref: process.env.GITEE_AI_MODEL_PREF,\n      GiteeAITokenLimit: process.env.GITEE_AI_MODEL_TOKEN_LIMIT || 8192,\n\n      // Docker Model Runner Keys\n      DockerModelRunnerBasePath: process.env.DOCKER_MODEL_RUNNER_BASE_PATH,\n      DockerModelRunnerModelPref:\n        process.env.DOCKER_MODEL_RUNNER_LLM_MODEL_PREF,\n      DockerModelRunnerModelTokenLimit:\n        process.env.DOCKER_MODEL_RUNNER_LLM_MODEL_TOKEN_LIMIT || 8192,\n\n      // Privatemode Keys\n      PrivateModeBasePath: process.env.PRIVATEMODE_LLM_BASE_PATH,\n      PrivateModeModelPref: process.env.PRIVATEMODE_LLM_MODEL_PREF,\n\n      // SambaNova Keys\n      SambaNovaLLMApiKey: !!process.env.SAMBANOVA_LLM_API_KEY,\n      SambaNovaLLMModelPref: process.env.SAMBANOVA_LLM_MODEL_PREF,\n\n      // Lemonade Keys\n      LemonadeLLMBasePath: process.env.LEMONADE_LLM_BASE_PATH,\n      LemonadeLLMModelPref: process.env.LEMONADE_LLM_MODEL_PREF,\n      LemonadeLLMModelTokenLimit:\n        process.env.LEMONADE_LLM_MODEL_TOKEN_LIMIT || 8192,\n    };\n  },\n\n  agent_sql_connections: async function () {\n    const setting = await SystemSettings.get({\n      label: \"agent_sql_connections\",\n    });\n    if (!setting) return [];\n\n    const connections = safeJsonParse(setting.value, []).map((conn) => {\n      let scheme = conn.engine;\n      if (scheme === \"sql-server\") scheme = \"mssql\";\n      if (scheme === \"postgresql\") scheme = \"postgres\";\n      const parser = new ConnectionStringParser({ scheme });\n\n      const parsed = parser.parse(conn.connectionString);\n      return {\n        ...conn,\n        username: parsed.username,\n        password: parsed.password,\n        host: parsed.hosts?.[0]?.host,\n        port: parsed.hosts?.[0]?.port,\n        database: parsed.endpoint,\n        scheme: parsed.scheme,\n      };\n    });\n\n    return connections;\n  },\n  getFeatureFlags: async function () {\n    return {\n      experimental_live_file_sync:\n        (await SystemSettings.get({ label: \"experimental_live_file_sync\" }))\n          ?.value === \"enabled\",\n    };\n  },\n\n  /**\n   * Get user configured Community Hub Settings\n   * Connection key is used to authenticate with the Community Hub API\n   * for your account.\n   * @returns {Promise<{connectionKey: string}>}\n   */\n  hubSettings: async function () {\n    try {\n      const hubKey = await this.get({ label: \"hub_api_key\" });\n      return { connectionKey: hubKey?.value || null };\n    } catch (error) {\n      console.error(error.message);\n      return { connectionKey: null };\n    }\n  },\n\n  simpleSSO: {\n    /**\n     * Gets the no login redirect URL. If the conditions below are not met, this will return null.\n     * - If simple SSO is not enabled.\n     * - If simple SSO login page is not disabled.\n     * - If the no login redirect is not a valid URL or is not set.\n     * @returns {string | null}\n     */\n    noLoginRedirect: () => {\n      if (!(\"SIMPLE_SSO_ENABLED\" in process.env)) return null; // if simple SSO is not enabled, return null\n      if (!(\"SIMPLE_SSO_NO_LOGIN\" in process.env)) return null; // if the no login config is not set, return null\n      if (!(\"SIMPLE_SSO_NO_LOGIN_REDIRECT\" in process.env)) return null; // if the no login redirect is not set, return null\n\n      try {\n        let url = new URL(process.env.SIMPLE_SSO_NO_LOGIN_REDIRECT);\n        return url.toString();\n      } catch {}\n\n      // if the no login redirect is not a valid URL or is not set, return null\n      return null;\n    },\n  },\n};\n\n/**\n * Merges SQL connection updates from the frontend with existing backend connections.\n * Processes three types of actions: \"remove\", \"update\", and \"add\".\n *\n * @param {Array<Object>} existingConnections - Current connections stored in the database\n * @param {Array<Object>} updates - Connection updates from frontend, each with an action property\n * @returns {Array<Object>} - The merged connections array\n */\nfunction mergeConnections(existingConnections = [], updates = []) {\n  const connectionsMap = new Map(\n    existingConnections.map((conn) => [conn.database_id, conn])\n  );\n\n  for (const update of updates) {\n    const {\n      action,\n      database_id,\n      originalDatabaseId,\n      connectionString,\n      engine,\n      schema,\n    } = update;\n\n    switch (action) {\n      case \"remove\": {\n        connectionsMap.delete(database_id);\n        break;\n      }\n      case \"update\": {\n        if (!connectionString) continue;\n        const newId = slugify(database_id);\n\n        // Verify original connection exists\n        if (!connectionsMap.has(originalDatabaseId)) {\n          console.warn(\n            `[mergeConnections] Update skipped: Original connection \"${originalDatabaseId}\" not found`\n          );\n          break;\n        }\n\n        // Check for name conflict (excluding the one being updated)\n        if (newId !== originalDatabaseId && connectionsMap.has(newId)) {\n          console.warn(\n            `[mergeConnections] Update skipped: New name \"${newId}\" conflicts with existing connection`\n          );\n          break;\n        }\n\n        // Remove old and add updated connection\n        connectionsMap.delete(originalDatabaseId);\n        connectionsMap.set(newId, {\n          engine,\n          database_id: newId,\n          connectionString,\n          ...(schema && { schema }),\n        });\n        break;\n      }\n\n      case \"add\": {\n        if (!connectionString) continue;\n        const slugifiedId = slugify(database_id);\n\n        // Skip if already exists\n        if (connectionsMap.has(slugifiedId)) {\n          console.warn(\n            `[mergeConnections] Add skipped: Connection \"${slugifiedId}\" already exists`\n          );\n          break;\n        }\n\n        connectionsMap.set(slugifiedId, {\n          engine,\n          database_id: slugifiedId,\n          connectionString,\n          ...(schema && { schema }),\n        });\n        break;\n      }\n\n      default: {\n        throw new Error(\"SQL connection update contains an invalid action.\");\n      }\n    }\n  }\n\n  return Array.from(connectionsMap.values());\n}\n\nmodule.exports.SystemSettings = SystemSettings;\n"
  },
  {
    "path": "server/models/telemetry.js",
    "content": "const { v4 } = require(\"uuid\");\nconst { SystemSettings } = require(\"./systemSettings\");\n\n// Map of events and last sent time to check if the event is on cooldown\n// This will be cleared on server restart - but that is fine since it is mostly to just\n// prevent spamming the logs.\nconst TelemetryCooldown = new Map();\n\nconst Telemetry = {\n  // Write-only key. It can't read events or any of your other data, so it's safe to use in public apps.\n  pubkey: \"phc_9qu7QLpV8L84P3vFmEiZxL020t2EqIubP7HHHxrSsqS\",\n  stubDevelopmentEvents: true, // [DO NOT TOUCH] Core team only.\n  label: \"telemetry_id\",\n  /*\n  Key value pairs of events that should be debounced to prevent spamming the logs.\n  This should be used for events that could be triggered in rapid succession that are not useful to atomically log.\n  The value is the number of seconds to debounce the event\n  */\n  debounced: {\n    sent_chat: 1800,\n    agent_chat_sent: 1800,\n    agent_chat_started: 1800,\n    agent_tool_call: 1800,\n\n    // Document mgmt events\n    document_uploaded: 30,\n    documents_embedded_in_workspace: 30,\n    link_uploaded: 30,\n    raw_document_uploaded: 30,\n    document_parsed: 30,\n  },\n\n  id: async function () {\n    const result = await SystemSettings.get({ label: this.label });\n    return result?.value || null;\n  },\n\n  connect: async function () {\n    const client = this.client();\n    const distinctId = await this.findOrCreateId();\n    return { client, distinctId };\n  },\n\n  isDev: function () {\n    return process.env.NODE_ENV === \"development\" && this.stubDevelopmentEvents;\n  },\n\n  client: function () {\n    if (process.env.DISABLE_TELEMETRY === \"true\" || this.isDev()) return null;\n    const { PostHog } = require(\"posthog-node\");\n    return new PostHog(this.pubkey);\n  },\n\n  runtime: function () {\n    if (process.env.ANYTHING_LLM_RUNTIME === \"docker\") return \"docker\";\n    if (process.env.NODE_ENV === \"production\") return \"production\";\n    return \"other\";\n  },\n\n  /**\n   * Checks if the event is on cooldown\n   * @param {string} event - The event to check\n   * @returns {boolean} - True if the event is on cooldown, false otherwise\n   */\n  isOnCooldown: function (event) {\n    // If the event is not debounced, return false\n    if (!this.debounced[event]) return false;\n\n    // If the event is not in the cooldown map, return false\n    const lastSent = TelemetryCooldown.get(event);\n    if (!lastSent) return false;\n\n    // If the event is in the cooldown map, check if it has expired\n    const now = Date.now();\n    const cooldown = this.debounced[event] * 1000;\n    return now - lastSent < cooldown;\n  },\n\n  /**\n   * Marks the event as on cooldown - will check if the event is debounced first\n   * @param {string} event - The event to mark\n   */\n  markOnCooldown: function (event) {\n    if (!this.debounced[event]) return;\n    TelemetryCooldown.set(event, Date.now());\n  },\n\n  sendTelemetry: async function (\n    event,\n    eventProperties = {},\n    subUserId = null,\n    silent = false\n  ) {\n    try {\n      const { client, distinctId: systemId } = await this.connect();\n      if (!client) return;\n      const distinctId = !!subUserId ? `${systemId}::${subUserId}` : systemId;\n      const properties = { ...eventProperties, runtime: this.runtime() };\n\n      // If the event is on cooldown, return\n      if (this.isOnCooldown(event)) return;\n\n      // Silence some events to keep logs from being too messy in production\n      // eg: Tool calls from agents spamming the logs.\n      if (!silent) {\n        console.log(`\\x1b[32m[TELEMETRY SENT]\\x1b[0m`, {\n          event,\n          distinctId,\n          properties,\n        });\n      }\n\n      client.capture({\n        event,\n        distinctId,\n        properties,\n      });\n    } catch {\n      return;\n    } finally {\n      // Mark the event as on cooldown if needed\n      this.markOnCooldown(event);\n    }\n  },\n\n  flush: async function () {\n    const client = this.client();\n    if (!client) return;\n    await client.shutdownAsync();\n  },\n\n  setUid: async function () {\n    const newId = v4();\n    await SystemSettings._updateSettings({ [this.label]: newId });\n    return newId;\n  },\n\n  findOrCreateId: async function () {\n    let currentId = await this.id();\n    if (currentId) return currentId;\n\n    currentId = await this.setUid();\n    return currentId;\n  },\n};\n\nmodule.exports = { Telemetry };\n"
  },
  {
    "path": "server/models/temporaryAuthToken.js",
    "content": "const { makeJWT } = require(\"../utils/http\");\nconst prisma = require(\"../utils/prisma\");\n\n/**\n * Temporary auth tokens are used for simple SSO.\n * They simply enable the ability for a time-based token to be used in the query of the /sso/login URL\n * to login as a user without the need of a username and password. These tokens are single-use and expire.\n */\nconst TemporaryAuthToken = {\n  expiry: 1000 * 60 * 6, // 1 hour\n  tablename: \"temporary_auth_tokens\",\n  writable: [],\n\n  makeTempToken: () => {\n    const uuidAPIKey = require(\"uuid-apikey\");\n    return `allm-tat-${uuidAPIKey.create().apiKey}`;\n  },\n\n  /**\n   * Issues a temporary auth token for a user via its ID.\n   * @param {number} userId\n   * @returns {Promise<{token: string|null, error: string | null}>}\n   */\n  issue: async function (userId = null) {\n    if (!userId)\n      throw new Error(\"User ID is required to issue a temporary auth token.\");\n    await this.invalidateUserTokens(userId);\n\n    try {\n      const token = this.makeTempToken();\n      const expiresAt = new Date(Date.now() + this.expiry);\n      await prisma.temporary_auth_tokens.create({\n        data: {\n          token,\n          expiresAt,\n          userId: Number(userId),\n        },\n      });\n\n      return { token, error: null };\n    } catch (error) {\n      console.error(\"FAILED TO CREATE TEMPORARY AUTH TOKEN.\", error.message);\n      return { token: null, error: error.message };\n    }\n  },\n\n  /**\n   * Invalidates (deletes) all temporary auth tokens for a user via their ID.\n   * @param {number} userId\n   * @returns {Promise<boolean>}\n   */\n  invalidateUserTokens: async function (userId) {\n    if (!userId)\n      throw new Error(\n        \"User ID is required to invalidate temporary auth tokens.\"\n      );\n    await prisma.temporary_auth_tokens.deleteMany({\n      where: { userId: Number(userId) },\n    });\n    return true;\n  },\n\n  /**\n   * Validates a temporary auth token and returns the session token\n   * to be set in the browser localStorage for authentication.\n   * @param {string} publicToken - the token to validate against\n   * @returns {Promise<{sessionToken: string|null, token: import(\"@prisma/client\").temporary_auth_tokens & {user: import(\"@prisma/client\").users} | null, error: string | null}>}\n   */\n  validate: async function (publicToken = \"\") {\n    /** @type {import(\"@prisma/client\").temporary_auth_tokens & {user: import(\"@prisma/client\").users} | undefined | null} **/\n    let token;\n\n    try {\n      if (!publicToken)\n        throw new Error(\n          \"Public token is required to validate a temporary auth token.\"\n        );\n      token = await prisma.temporary_auth_tokens.findUnique({\n        where: { token: String(publicToken) },\n        include: { user: true },\n      });\n      if (!token) throw new Error(\"Invalid token.\");\n      if (token.expiresAt < new Date()) throw new Error(\"Token expired.\");\n      if (token.user.suspended) throw new Error(\"User account suspended.\");\n\n      // Create a new session token for the user valid for 30 days\n      const sessionToken = makeJWT(\n        { id: token.user.id, username: token.user.username },\n        process.env.JWT_EXPIRY\n      );\n\n      return { sessionToken, token, error: null };\n    } catch (error) {\n      console.error(\"FAILED TO VALIDATE TEMPORARY AUTH TOKEN.\", error.message);\n      return { sessionToken: null, token: null, error: error.message };\n    } finally {\n      // Delete the token after it has been used under all circumstances if it was retrieved\n      if (token)\n        await prisma.temporary_auth_tokens.delete({ where: { id: token.id } });\n    }\n  },\n};\n\nmodule.exports = { TemporaryAuthToken };\n"
  },
  {
    "path": "server/models/user.js",
    "content": "const { Prisma } = require(\"@prisma/client\");\nconst prisma = require(\"../utils/prisma\");\nconst { EventLogs } = require(\"./eventLogs\");\n\n/**\n * @typedef {Object} User\n * @property {number} id\n * @property {string} username\n * @property {string} password\n * @property {string} pfpFilename\n * @property {string} role\n * @property {boolean} suspended\n * @property {number|null} dailyMessageLimit\n */\n\nconst User = {\n  usernameRegex: new RegExp(/^[a-z][a-z0-9._@-]*$/),\n  writable: [\n    // Used for generic updates so we can validate keys in request body\n    \"username\",\n    \"password\",\n    \"pfpFilename\",\n    \"role\",\n    \"suspended\",\n    \"dailyMessageLimit\",\n    \"bio\",\n  ],\n  validations: {\n    /**\n     * Unix-style username regex:\n     * - Must start with a lowercase letter\n     * - Can contain lowercase letters, digits, underscores, hyphens, @ signs, and periods\n     * - 2-32 characters long\n     */\n    username: (newValue = \"\") => {\n      try {\n        const username = String(newValue);\n        if (username.length > 32)\n          throw new Error(\"Username cannot be longer than 32 characters\");\n        if (username.length < 2)\n          throw new Error(\"Username must be at least 2 characters\");\n        if (!User.usernameRegex.test(username))\n          throw new Error(\n            \"Username must start with a lowercase letter and only contain lowercase letters, numbers, underscores, hyphens, and periods\"\n          );\n        return username;\n      } catch (e) {\n        throw new Error(e.message);\n      }\n    },\n    role: (role = \"default\") => {\n      const VALID_ROLES = [\"default\", \"admin\", \"manager\"];\n      if (!VALID_ROLES.includes(role)) {\n        throw new Error(\n          `Invalid role. Allowed roles are: ${VALID_ROLES.join(\", \")}`\n        );\n      }\n      return String(role);\n    },\n    dailyMessageLimit: (dailyMessageLimit = null) => {\n      if (dailyMessageLimit === null) return null;\n      const limit = Number(dailyMessageLimit);\n      if (isNaN(limit) || limit < 1) {\n        throw new Error(\n          \"Daily message limit must be null or a number greater than or equal to 1\"\n        );\n      }\n      return limit;\n    },\n    bio: (bio = \"\") => {\n      if (!bio || typeof bio !== \"string\") return \"\";\n      if (bio.length > 1000)\n        throw new Error(\"Bio cannot be longer than 1,000 characters\");\n      return String(bio);\n    },\n  },\n  // validations for the above writable fields.\n  castColumnValue: function (key, value) {\n    switch (key) {\n      case \"suspended\":\n        return Number(Boolean(value));\n      case \"dailyMessageLimit\":\n        return value === null ? null : Number(value);\n      default:\n        return String(value);\n    }\n  },\n\n  filterFields: function (user = {}) {\n    const {\n      password: _password,\n      web_push_subscription_config: _web_push_subscription_config,\n      ...rest\n    } = user;\n    return { ...rest };\n  },\n  _identifyErrorAndFormatMessage: function (error) {\n    if (error instanceof Prisma.PrismaClientKnownRequestError) {\n      // P2002 is the unique constraint violation error code\n      if (error.code === \"P2002\") {\n        const target = error.meta?.target;\n        return `A user with that ${target?.join(\", \")} already exists`;\n      }\n    }\n    return error.message;\n  },\n\n  create: async function ({\n    username,\n    password,\n    role = \"default\",\n    dailyMessageLimit = null,\n    bio = \"\",\n  }) {\n    const passwordCheck = this.checkPasswordComplexity(password);\n    if (!passwordCheck.checkedOK) {\n      return { user: null, error: passwordCheck.error };\n    }\n\n    try {\n      // Validate username format (validation function handles all checks)\n      const validatedUsername = this.validations.username(username);\n\n      const bcrypt = require(\"bcryptjs\");\n      const hashedPassword = bcrypt.hashSync(password, 10);\n      const user = await prisma.users.create({\n        data: {\n          username: validatedUsername,\n          password: hashedPassword,\n          role: this.validations.role(role),\n          bio: this.validations.bio(bio),\n          dailyMessageLimit:\n            this.validations.dailyMessageLimit(dailyMessageLimit),\n        },\n      });\n      return { user: this.filterFields(user), error: null };\n    } catch (error) {\n      console.error(\"FAILED TO CREATE USER.\", error.message);\n      return { user: null, error: this._identifyErrorAndFormatMessage(error) };\n    }\n  },\n  // Log the changes to a user object, but omit sensitive fields\n  // that are not meant to be logged.\n  loggedChanges: function (updates, prev = {}) {\n    const changes = {};\n    const sensitiveFields = [\"password\"];\n\n    Object.keys(updates).forEach((key) => {\n      if (!sensitiveFields.includes(key) && updates[key] !== prev[key]) {\n        changes[key] = `${prev[key]} => ${updates[key]}`;\n      }\n    });\n\n    return changes;\n  },\n\n  update: async function (userId, updates = {}) {\n    try {\n      if (!userId) throw new Error(\"No user id provided for update\");\n      const currentUser = await prisma.users.findUnique({\n        where: { id: parseInt(userId) },\n      });\n      if (!currentUser) return { success: false, error: \"User not found\" };\n\n      // We previously had more lenient username validation, but now with more strict validation\n      // we dont want to break existing users by changing non-username fields.\n      // If they are not explictly changing the username, do not attempt to validate it.\n      if (updates.hasOwnProperty(\"username\")) {\n        if (updates.username === currentUser.username) delete updates.username;\n      }\n\n      // Removes non-writable fields for generic updates\n      // and force-casts to the proper type;\n      Object.entries(updates).forEach(([key, value]) => {\n        if (this.writable.includes(key)) {\n          if (this.validations.hasOwnProperty(key)) {\n            updates[key] = this.validations[key](\n              this.castColumnValue(key, value)\n            );\n          } else {\n            updates[key] = this.castColumnValue(key, value);\n          }\n          return;\n        }\n        delete updates[key];\n      });\n\n      if (Object.keys(updates).length === 0)\n        return { success: false, error: \"No valid updates applied.\" };\n\n      // Handle password specific updates\n      if (updates.hasOwnProperty(\"password\")) {\n        const passwordCheck = this.checkPasswordComplexity(updates.password);\n        if (!passwordCheck.checkedOK) {\n          return { success: false, error: passwordCheck.error };\n        }\n        const bcrypt = require(\"bcryptjs\");\n        updates.password = bcrypt.hashSync(updates.password, 10);\n      }\n\n      const user = await prisma.users.update({\n        where: { id: parseInt(userId) },\n        data: updates,\n      });\n\n      await EventLogs.logEvent(\n        \"user_updated\",\n        {\n          username: user.username,\n          changes: this.loggedChanges(updates, currentUser),\n        },\n        userId\n      );\n      return { success: true, error: null };\n    } catch (error) {\n      console.error(\"FAILED TO UPDATE USER.\", error.message);\n      return {\n        success: false,\n        error: this._identifyErrorAndFormatMessage(error),\n      };\n    }\n  },\n\n  /**\n   * Explicit direct update of user object.\n   * Only use this method when directly setting a key value\n   * that takes no user input for the keys being modified.\n   * @param {number} id - The id of the user to update.\n   * @param {Object} data - The data to update the user with.\n   * @returns {Promise<Object>} The updated user object.\n   */\n  _update: async function (id = null, data = {}) {\n    if (!id) throw new Error(\"No user id provided for update\");\n\n    try {\n      const user = await prisma.users.update({\n        where: { id },\n        data,\n      });\n      return { user, message: null };\n    } catch (error) {\n      console.error(error.message);\n      return { user: null, message: error.message };\n    }\n  },\n\n  /**\n   * Get all users that match the given clause without filtering the fields.\n   * Internal use only - do not use this method for user-input flows\n   * @param {Object} clause - The clause to filter the users by.\n   * @param {number|null} limit - The maximum number of users to return.\n   * @returns {Promise<Array<User>>} The users that match the given clause.\n   */\n  _where: async function (clause = {}, limit = null) {\n    try {\n      const users = await prisma.users.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n      });\n      return users;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  /**\n   * Returns a user object based on the clause provided.\n   * @param {Object} clause - The clause to use to find the user.\n   * @returns {Promise<import(\"@prisma/client\").users|null>} The user object or null if not found.\n   */\n  get: async function (clause = {}) {\n    try {\n      const user = await prisma.users.findFirst({ where: clause });\n      return user ? this.filterFields({ ...user }) : null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n  // Returns user object with all fields\n  _get: async function (clause = {}) {\n    try {\n      const user = await prisma.users.findFirst({ where: clause });\n      return user ? { ...user } : null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  count: async function (clause = {}) {\n    try {\n      const count = await prisma.users.count({ where: clause });\n      return count;\n    } catch (error) {\n      console.error(error.message);\n      return 0;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.users.deleteMany({ where: clause });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  where: async function (clause = {}, limit = null) {\n    try {\n      const users = await prisma.users.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n      });\n      return users.map((usr) => this.filterFields(usr));\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  checkPasswordComplexity: function (passwordInput = \"\") {\n    const passwordComplexity = require(\"joi-password-complexity\");\n    // Can be set via ENV variable on boot. No frontend config at this time.\n    // Docs: https://www.npmjs.com/package/joi-password-complexity\n    const complexityOptions = {\n      min: process.env.PASSWORDMINCHAR || 8,\n      max: process.env.PASSWORDMAXCHAR || 250,\n      lowerCase: process.env.PASSWORDLOWERCASE || 0,\n      upperCase: process.env.PASSWORDUPPERCASE || 0,\n      numeric: process.env.PASSWORDNUMERIC || 0,\n      symbol: process.env.PASSWORDSYMBOL || 0,\n      // reqCount should be equal to how many conditions you are testing for (1-4)\n      requirementCount: process.env.PASSWORDREQUIREMENTS || 0,\n    };\n\n    const complexityCheck = passwordComplexity(\n      complexityOptions,\n      \"password\"\n    ).validate(passwordInput);\n    if (complexityCheck.hasOwnProperty(\"error\")) {\n      let myError = \"\";\n      let prepend = \"\";\n      for (let i = 0; i < complexityCheck.error.details.length; i++) {\n        myError += prepend + complexityCheck.error.details[i].message;\n        prepend = \", \";\n      }\n      return { checkedOK: false, error: myError };\n    }\n\n    return { checkedOK: true, error: \"No error.\" };\n  },\n\n  /**\n   * Check if a user can send a chat based on their daily message limit.\n   * This limit is system wide and not per workspace and only applies to\n   * multi-user mode AND non-admin users.\n   * @param {User} user The user object record.\n   * @returns {Promise<boolean>} True if the user can send a chat, false otherwise.\n   */\n  canSendChat: async function (user) {\n    const { ROLES } = require(\"../utils/middleware/multiUserProtected\");\n    if (!user || user.dailyMessageLimit === null || user.role === ROLES.admin)\n      return true;\n\n    const { WorkspaceChats } = require(\"./workspaceChats\");\n    const currentChatCount = await WorkspaceChats.count({\n      user_id: user.id,\n      createdAt: {\n        gte: new Date(new Date() - 24 * 60 * 60 * 1000), // 24 hours\n      },\n    });\n\n    return currentChatCount < user.dailyMessageLimit;\n  },\n};\n\nmodule.exports = { User };\n"
  },
  {
    "path": "server/models/vectors.js",
    "content": "const prisma = require(\"../utils/prisma\");\nconst { Document } = require(\"./documents\");\n\nconst DocumentVectors = {\n  bulkInsert: async function (vectorRecords = []) {\n    if (vectorRecords.length === 0) return;\n\n    try {\n      const inserts = [];\n      vectorRecords.forEach((record) => {\n        inserts.push(\n          prisma.document_vectors.create({\n            data: {\n              docId: record.docId,\n              vectorId: record.vectorId,\n            },\n          })\n        );\n      });\n      await prisma.$transaction(inserts);\n      return { documentsInserted: inserts.length };\n    } catch (error) {\n      console.error(\"Bulk insert failed\", error);\n      return { documentsInserted: 0 };\n    }\n  },\n\n  where: async function (clause = {}, limit) {\n    try {\n      const results = await prisma.document_vectors.findMany({\n        where: clause,\n        take: limit || undefined,\n      });\n      return results;\n    } catch (error) {\n      console.error(\"Where query failed\", error);\n      return [];\n    }\n  },\n\n  deleteForWorkspace: async function (workspaceId) {\n    const documents = await Document.forWorkspace(workspaceId);\n    const docIds = [...new Set(documents.map((doc) => doc.docId))];\n\n    try {\n      await prisma.document_vectors.deleteMany({\n        where: { docId: { in: docIds } },\n      });\n      return true;\n    } catch (error) {\n      console.error(\"Delete for workspace failed\", error);\n      return false;\n    }\n  },\n\n  deleteIds: async function (ids = []) {\n    try {\n      await prisma.document_vectors.deleteMany({\n        where: { id: { in: ids } },\n      });\n      return true;\n    } catch (error) {\n      console.error(\"Delete IDs failed\", error);\n      return false;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.document_vectors.deleteMany({ where: clause });\n      return true;\n    } catch (error) {\n      console.error(\"Delete failed\", error);\n      return false;\n    }\n  },\n};\n\nmodule.exports = { DocumentVectors };\n"
  },
  {
    "path": "server/models/workspace.js",
    "content": "const prisma = require(\"../utils/prisma\");\nconst slugifyModule = require(\"slugify\");\nconst { Document } = require(\"./documents\");\nconst { WorkspaceUser } = require(\"./workspaceUsers\");\nconst { ROLES } = require(\"../utils/middleware/multiUserProtected\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst { User } = require(\"./user\");\nconst { PromptHistory } = require(\"./promptHistory\");\nconst { SystemSettings } = require(\"./systemSettings\");\n\nfunction isNullOrNaN(value) {\n  if (value === null) return true;\n  return isNaN(value);\n}\n\n/**\n * @typedef {Object} Workspace\n * @property {number} id - The ID of the workspace\n * @property {string} name - The name of the workspace\n * @property {string} slug - The slug of the workspace\n * @property {string} openAiPrompt - The OpenAI prompt of the workspace\n * @property {string} openAiTemp - The OpenAI temperature of the workspace\n * @property {number} openAiHistory - The OpenAI history of the workspace\n * @property {number} similarityThreshold - The similarity threshold of the workspace\n * @property {string} chatProvider - The chat provider of the workspace\n * @property {string} chatModel - The chat model of the workspace\n * @property {number} topN - The top N of the workspace\n * @property {string} chatMode - The chat mode of the workspace\n * @property {string} agentProvider - The agent provider of the workspace\n * @property {string} agentModel - The agent model of the workspace\n * @property {string} queryRefusalResponse - The query refusal response of the workspace\n * @property {string} vectorSearchMode - The vector search mode of the workspace\n */\n\nconst Workspace = {\n  VALID_CHAT_MODES: [\"chat\", \"query\", \"automatic\"],\n  defaultPrompt: SystemSettings.saneDefaultSystemPrompt,\n\n  // Used for generic updates so we can validate keys in request body\n  // commented fields are not writable, but are available on the db object\n  writable: [\n    \"name\",\n    // \"slug\",\n    // \"vectorTag\",\n    \"openAiTemp\",\n    \"openAiHistory\",\n    \"lastUpdatedAt\",\n    \"openAiPrompt\",\n    \"similarityThreshold\",\n    \"chatProvider\",\n    \"chatModel\",\n    \"topN\",\n    \"chatMode\",\n    // \"pfpFilename\",\n    \"agentProvider\",\n    \"agentModel\",\n    \"queryRefusalResponse\",\n    \"vectorSearchMode\",\n  ],\n\n  validations: {\n    name: (value) => {\n      // If the name is not provided or is not a string then we will use a default name.\n      // as the name field is not nullable in the db schema or has a default value.\n      if (!value || typeof value !== \"string\") return \"My Workspace\";\n      return String(value).slice(0, 255);\n    },\n    openAiTemp: (value) => {\n      if (value === null || value === undefined) return null;\n      const temp = parseFloat(value);\n      if (isNullOrNaN(temp) || temp < 0) return null;\n      return temp;\n    },\n    openAiHistory: (value) => {\n      if (value === null || value === undefined) return 20;\n      const history = parseInt(value);\n      if (isNullOrNaN(history)) return 20;\n      if (history < 0) return 0;\n      return history;\n    },\n    similarityThreshold: (value) => {\n      if (value === null || value === undefined) return 0.25;\n      const threshold = parseFloat(value);\n      if (isNullOrNaN(threshold)) return 0.25;\n      if (threshold < 0) return 0.0;\n      if (threshold > 1) return 1.0;\n      return threshold;\n    },\n    topN: (value) => {\n      if (value === null || value === undefined) return 4;\n      const n = parseInt(value);\n      if (isNullOrNaN(n)) return 4;\n      if (n < 1) return 1;\n      return n;\n    },\n    chatMode: (value) => {\n      if (!value || !Workspace.VALID_CHAT_MODES.includes(value)) return \"chat\";\n      return value;\n    },\n    chatProvider: (value) => {\n      if (!value || typeof value !== \"string\" || value === \"none\") return null;\n      return String(value);\n    },\n    chatModel: (value) => {\n      if (!value || typeof value !== \"string\") return null;\n      return String(value);\n    },\n    agentProvider: (value) => {\n      if (!value || typeof value !== \"string\" || value === \"none\") return null;\n      return String(value);\n    },\n    agentModel: (value) => {\n      if (!value || typeof value !== \"string\") return null;\n      return String(value);\n    },\n    queryRefusalResponse: (value) => {\n      if (!value || typeof value !== \"string\") return null;\n      return String(value);\n    },\n    openAiPrompt: (value) => {\n      if (!value || typeof value !== \"string\") return null;\n      return String(value);\n    },\n    vectorSearchMode: (value) => {\n      if (\n        !value ||\n        typeof value !== \"string\" ||\n        ![\"default\", \"rerank\"].includes(value)\n      )\n        return \"default\";\n      return value;\n    },\n  },\n\n  /**\n   * The default Slugify module requires some additional mapping to prevent downstream issues\n   * with some vector db providers and instead of building a normalization method for every provider\n   * we can capture this on the table level to not have to worry about it.\n   * @param  {...any} args - slugify args for npm package.\n   * @returns {string}\n   */\n  slugify: function (...args) {\n    slugifyModule.extend({\n      \"+\": \" plus \",\n      \"!\": \" bang \",\n      \"@\": \" at \",\n      \"*\": \" splat \",\n      \".\": \" dot \",\n      \":\": \"\",\n      \"~\": \"\",\n      \"(\": \"\",\n      \")\": \"\",\n      \"'\": \"\",\n      '\"': \"\",\n      \"|\": \"\",\n    });\n    return slugifyModule(...args);\n  },\n\n  /**\n   * Validate the fields for a workspace update.\n   * @param {Object} updates - The updates to validate - should be writable fields\n   * @returns {Object} The validated updates. Only valid fields are returned.\n   */\n  validateFields: function (updates = {}) {\n    const validatedFields = {};\n    for (const [key, value] of Object.entries(updates)) {\n      if (!this.writable.includes(key)) continue;\n      if (this.validations[key]) {\n        validatedFields[key] = this.validations[key](value);\n      } else {\n        // If there is no validation for the field then we will just pass it through.\n        validatedFields[key] = value;\n      }\n    }\n    return validatedFields;\n  },\n\n  /**\n   * Create a new workspace.\n   * @param {string} name - The name of the workspace.\n   * @param {number} creatorId - The ID of the user creating the workspace.\n   * @param {Object} additionalFields - Additional fields to apply to the workspace - will be validated.\n   * @returns {Promise<{workspace: Object | null, message: string | null}>} A promise that resolves to an object containing the created workspace and an error message if applicable.\n   */\n  new: async function (name = null, creatorId = null, additionalFields = {}) {\n    if (!name) return { workspace: null, message: \"name cannot be null\" };\n    var slug = this.slugify(name, { lower: true });\n    slug = slug || uuidv4();\n\n    const existingBySlug = await this.get({ slug });\n    if (existingBySlug !== null) {\n      const slugSeed = Math.floor(10000000 + Math.random() * 90000000);\n      slug = this.slugify(`${name}-${slugSeed}`, { lower: true });\n    }\n\n    // Get the default system prompt\n    const defaultSystemPrompt = await SystemSettings.get({\n      label: \"default_system_prompt\",\n    });\n    if (!!defaultSystemPrompt?.value)\n      additionalFields.openAiPrompt = defaultSystemPrompt.value;\n    else additionalFields.openAiPrompt = this.defaultPrompt;\n\n    try {\n      const workspace = await prisma.workspaces.create({\n        data: {\n          name: this.validations.name(name),\n          chatMode: \"chat\", // default to chat mode for now\n          ...this.validateFields(additionalFields),\n          slug,\n        },\n      });\n\n      // If created with a user then we need to create the relationship as well.\n      // If creating with an admin User it wont change anything because admins can\n      // view all workspaces anyway.\n      if (!!creatorId) await WorkspaceUser.create(creatorId, workspace.id);\n      return { workspace, message: null };\n    } catch (error) {\n      console.error(error.message);\n      return { workspace: null, message: error.message };\n    }\n  },\n\n  /**\n   * Update the settings for a workspace. Applies validations to the updates provided.\n   * @param {number} id - The ID of the workspace to update.\n   * @param {Object} updates - The data to update.\n   * @returns {Promise<{workspace: Object | null, message: string | null}>} A promise that resolves to an object containing the updated workspace and an error message if applicable.\n   */\n  update: async function (id = null, updates = {}) {\n    if (!id) throw new Error(\"No workspace id provided for update\");\n\n    const validatedUpdates = this.validateFields(updates);\n    if (Object.keys(validatedUpdates).length === 0)\n      return { workspace: { id }, message: \"No valid fields to update!\" };\n\n    // If the user unset the chatProvider we will need\n    // to then clear the chatModel as well to prevent confusion during\n    // LLM loading.\n    if (validatedUpdates?.chatProvider === \"default\") {\n      validatedUpdates.chatProvider = null;\n      validatedUpdates.chatModel = null;\n    }\n\n    return this._update(id, validatedUpdates);\n  },\n\n  /**\n   * Direct update of workspace settings without any validation.\n   * @param {number} id - The ID of the workspace to update.\n   * @param {Object} data - The data to update.\n   * @returns {Promise<{workspace: Object | null, message: string | null}>} A promise that resolves to an object containing the updated workspace and an error message if applicable.\n   */\n  _update: async function (id = null, data = {}) {\n    if (!id) throw new Error(\"No workspace id provided for update\");\n\n    try {\n      const workspace = await prisma.workspaces.update({\n        where: { id },\n        data,\n      });\n      return { workspace, message: null };\n    } catch (error) {\n      console.error(error.message);\n      return { workspace: null, message: error.message };\n    }\n  },\n\n  getWithUser: async function (user = null, clause = {}) {\n    if ([ROLES.admin, ROLES.manager].includes(user.role))\n      return this.get(clause);\n\n    try {\n      const workspace = await prisma.workspaces.findFirst({\n        where: {\n          ...clause,\n          workspace_users: {\n            some: {\n              user_id: user?.id,\n            },\n          },\n        },\n        include: {\n          workspace_users: true,\n          documents: true,\n        },\n      });\n\n      if (!workspace) return null;\n\n      return {\n        ...workspace,\n        documents: await Document.forWorkspace(workspace.id),\n        contextWindow: this._getContextWindow(workspace),\n        currentContextTokenCount: await this._getCurrentContextTokenCount(\n          workspace.id\n        ),\n      };\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  /**\n   * Get the total token count of all parsed files in a workspace/thread\n   * @param {number} workspaceId - The ID of the workspace\n   * @param {number|null} threadId - Optional thread ID to filter by\n   * @returns {Promise<number>} Total token count of all files\n   * @private\n   */\n  async _getCurrentContextTokenCount(workspaceId, threadId = null) {\n    const { WorkspaceParsedFiles } = require(\"./workspaceParsedFiles\");\n    return await WorkspaceParsedFiles.totalTokenCount({\n      workspaceId: Number(workspaceId),\n      threadId: threadId ? Number(threadId) : null,\n    });\n  },\n\n  /**\n   * Get the context window size for a workspace based on its provider and model settings.\n   * If the workspace has no provider/model set, falls back to system defaults.\n   * @param {Workspace} workspace - The workspace to get context window for\n   * @returns {number|null} The context window size in tokens (defaults to null if no provider/model found)\n   * @private\n   */\n  _getContextWindow: function (workspace) {\n    const {\n      getLLMProviderClass,\n      getBaseLLMProviderModel,\n    } = require(\"../utils/helpers\");\n    const provider = workspace.chatProvider || process.env.LLM_PROVIDER || null;\n    const LLMProvider = getLLMProviderClass({ provider });\n    const model =\n      workspace.chatModel || getBaseLLMProviderModel({ provider }) || null;\n\n    if (!provider || !model) return null;\n    return LLMProvider?.promptWindowLimit?.(model) || null;\n  },\n\n  get: async function (clause = {}) {\n    try {\n      const workspace = await prisma.workspaces.findFirst({\n        where: clause,\n        include: {\n          documents: true,\n        },\n      });\n\n      if (!workspace) return null;\n      return {\n        ...workspace,\n        contextWindow: this._getContextWindow(workspace),\n        currentContextTokenCount: await this._getCurrentContextTokenCount(\n          workspace.id\n        ),\n      };\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.workspaces.delete({\n        where: clause,\n      });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  where: async function (clause = {}, limit = null, orderBy = null) {\n    try {\n      const results = await prisma.workspaces.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  whereWithUser: async function (\n    user,\n    clause = {},\n    limit = null,\n    orderBy = null\n  ) {\n    if ([ROLES.admin, ROLES.manager].includes(user.role))\n      return await this.where(clause, limit, orderBy);\n\n    try {\n      const workspaces = await prisma.workspaces.findMany({\n        where: {\n          ...clause,\n          workspace_users: {\n            some: {\n              user_id: user.id,\n            },\n          },\n        },\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return workspaces;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  whereWithUsers: async function (clause = {}, limit = null, orderBy = null) {\n    try {\n      const workspaces = await this.where(clause, limit, orderBy);\n      for (const workspace of workspaces) {\n        const userIds = (\n          await WorkspaceUser.where({ workspace_id: Number(workspace.id) })\n        ).map((rel) => rel.user_id);\n        workspace.userIds = userIds;\n      }\n      return workspaces;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  /**\n   * Get all users for a workspace.\n   * @param {number} workspaceId - The ID of the workspace to get users for.\n   * @returns {Promise<Array<{userId: number, username: string, role: string}>>} A promise that resolves to an array of user objects.\n   */\n  workspaceUsers: async function (workspaceId) {\n    try {\n      const users = (\n        await WorkspaceUser.where({ workspace_id: Number(workspaceId) })\n      ).map((rel) => rel);\n\n      const usersById = await User.where({\n        id: { in: users.map((user) => user.user_id) },\n      });\n\n      const userInfo = usersById.map((user) => {\n        const workspaceUser = users.find((u) => u.user_id === user.id);\n        return {\n          userId: user.id,\n          username: user.username,\n          role: user.role,\n          lastUpdatedAt: workspaceUser.lastUpdatedAt,\n        };\n      });\n\n      return userInfo;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  /**\n   * Update the users for a workspace. Will remove all existing users and replace them with the new list.\n   * @param {number} workspaceId - The ID of the workspace to update.\n   * @param {number[]} userIds - An array of user IDs to add to the workspace.\n   * @returns {Promise<{success: boolean, error: string | null}>} A promise that resolves to an object containing the success status and an error message if applicable.\n   */\n  updateUsers: async function (workspaceId, userIds = []) {\n    try {\n      await WorkspaceUser.delete({ workspace_id: Number(workspaceId) });\n      await WorkspaceUser.createManyUsers(userIds, workspaceId);\n      return { success: true, error: null };\n    } catch (error) {\n      console.error(error.message);\n      return { success: false, error: error.message };\n    }\n  },\n\n  trackChange: async function (prevData, newData, user) {\n    try {\n      await this._trackWorkspacePromptChange(prevData, newData, user);\n      return;\n    } catch (error) {\n      console.error(\"Error tracking workspace change:\", error.message);\n      return;\n    }\n  },\n\n  /**\n   * We are tracking this change to determine the need to a prompt library or\n   * prompt assistant feature. If this is something you would like to see - tell us on GitHub!\n   * We now track the prompt change in the PromptHistory model.\n   * which is a sub-model of the Workspace model.\n   * @param {Workspace} prevData - The previous data of the workspace.\n   * @param {Workspace} newData - The new data of the workspace.\n   * @param {{id: number, role: string}|null} user - The user who made the change.\n   * @returns {Promise<void>}\n   */\n  _trackWorkspacePromptChange: async function (prevData, newData, user = null) {\n    if (\n      !!newData?.openAiPrompt && // new prompt is set\n      !!prevData?.openAiPrompt && // previous prompt was not null (default)\n      prevData?.openAiPrompt !== this.defaultPrompt && // previous prompt was not default\n      newData?.openAiPrompt !== prevData?.openAiPrompt // previous and new prompt are not the same\n    )\n      await PromptHistory.handlePromptChange(prevData, user); // log the change to the prompt history\n\n    const { Telemetry } = require(\"./telemetry\");\n    const { EventLogs } = require(\"./eventLogs\");\n    if (\n      !newData?.openAiPrompt || // no prompt change\n      newData?.openAiPrompt === this.defaultPrompt || // new prompt is default prompt\n      newData?.openAiPrompt === prevData?.openAiPrompt // same prompt\n    )\n      return;\n\n    await Telemetry.sendTelemetry(\"workspace_prompt_changed\");\n    await EventLogs.logEvent(\n      \"workspace_prompt_changed\",\n      {\n        workspaceName: prevData?.name,\n        prevSystemPrompt: prevData?.openAiPrompt || this.defaultPrompt,\n        newSystemPrompt: newData?.openAiPrompt,\n      },\n      user?.id\n    );\n    return;\n  },\n\n  // Direct DB queries for API use only.\n  /**\n   * Generic prisma FindMany query for workspaces collections\n   * @param {import(\"../node_modules/.prisma/client/index.d.ts\").Prisma.TypeMap['model']['workspaces']['operations']['findMany']['args']} prismaQuery\n   * @returns\n   */\n  _findMany: async function (prismaQuery = {}) {\n    try {\n      const results = await prisma.workspaces.findMany(prismaQuery);\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  /**\n   * Generic prisma query for .get of workspaces collections\n   * @param {import(\"../node_modules/.prisma/client/index.d.ts\").Prisma.TypeMap['model']['workspaces']['operations']['findFirst']['args']} prismaQuery\n   * @returns\n   */\n  _findFirst: async function (prismaQuery = {}) {\n    try {\n      const results = await prisma.workspaces.findFirst(prismaQuery);\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  /**\n   * Get the prompt history for a workspace.\n   * @param {Object} options - The options to get prompt history for.\n   * @param {number} options.workspaceId - The ID of the workspace to get prompt history for.\n   * @returns {Promise<Array<{id: number, prompt: string, modifiedAt: Date, modifiedBy: number, user: {id: number, username: string, role: string}}>>} A promise that resolves to an array of prompt history objects.\n   */\n  promptHistory: async function ({ workspaceId }) {\n    try {\n      const results = await PromptHistory.forWorkspace(workspaceId);\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  /**\n   * Delete the prompt history for a workspace.\n   * @param {Object} options - The options to delete the prompt history for.\n   * @param {number} options.workspaceId - The ID of the workspace to delete prompt history for.\n   * @returns {Promise<boolean>} A promise that resolves to a boolean indicating the success of the operation.\n   */\n  deleteAllPromptHistory: async function ({ workspaceId }) {\n    try {\n      return await PromptHistory.delete({ workspaceId });\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  /**\n   * Delete the prompt history for a workspace.\n   * @param {Object} options - The options to delete the prompt history for.\n   * @param {number} options.workspaceId - The ID of the workspace to delete prompt history for.\n   * @param {number} options.id - The ID of the prompt history to delete.\n   * @returns {Promise<boolean>} A promise that resolves to a boolean indicating the success of the operation.\n   */\n  deletePromptHistory: async function ({ workspaceId, id }) {\n    try {\n      return await PromptHistory.delete({ id, workspaceId });\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  /**\n   * Checks if the workspace's chat provider/model waterfall supports native tool calling.\n   * @param {Workspace} workspace - The workspace object to check\n   * @returns {Promise<boolean>}\n   */\n  supportsNativeToolCalling: async function (workspace = {}) {\n    if (!workspace) return false;\n    const { getBaseLLMProviderModel } = require(\"../utils/helpers\");\n    const AIbitat = require(\"../utils/agents/aibitat\");\n    const provider =\n      workspace?.agentProvider ??\n      workspace?.chatProvider ??\n      process.env.LLM_PROVIDER;\n    const model =\n      workspace?.agentModel ??\n      workspace?.chatModel ??\n      getBaseLLMProviderModel({ provider });\n    const agentConfig = { provider, model };\n    const agentProvider = new AIbitat(agentConfig).getProviderForConfig(\n      agentConfig\n    );\n    const nativeToolCalling = await agentProvider.supportsNativeToolCalling?.();\n    return nativeToolCalling;\n  },\n\n  /**\n   * Checks if the agent command is available for a workspace\n   * by checking if the workspace's agent provider supports native tool calling.\n   * - If the workspaces chat provider/model supports native tool calling, then the agent command is NOT available\n   * as it will be assumed the model is capable of handling tool calls.\n   * Otherwise, the agent command is available and the user must opt-in to \"@agent\" to use tool calls.\n   * @param {Workspace} workspace - The workspace object to check\n   * @returns {Promise<boolean>}\n   */\n  isAgentCommandAvailable: async function (workspace) {\n    if (workspace.chatMode !== \"automatic\") return true;\n    const nativeToolCalling = await this.supportsNativeToolCalling(workspace);\n    return nativeToolCalling === false;\n  },\n};\n\nmodule.exports = { Workspace };\n"
  },
  {
    "path": "server/models/workspaceAgentInvocation.js",
    "content": "const prisma = require(\"../utils/prisma\");\nconst { v4: uuidv4 } = require(\"uuid\");\n\nconst WorkspaceAgentInvocation = {\n  // returns array of strings with their @ handle.\n  // must start with @agent for now.\n  parseAgents: function (promptString) {\n    if (!promptString.startsWith(\"@agent\")) return [];\n    return promptString.split(/\\s+/).filter((v) => v.startsWith(\"@\"));\n  },\n\n  close: async function (uuid) {\n    if (!uuid) return;\n    try {\n      await prisma.workspace_agent_invocations.update({\n        where: { uuid: String(uuid) },\n        data: { closed: true },\n      });\n    } catch {}\n  },\n\n  new: async function ({ prompt, workspace, user = null, thread = null }) {\n    try {\n      const invocation = await prisma.workspace_agent_invocations.create({\n        data: {\n          uuid: uuidv4(),\n          workspace_id: workspace.id,\n          prompt: String(prompt),\n          user_id: user?.id,\n          thread_id: thread?.id,\n        },\n      });\n\n      return { invocation, message: null };\n    } catch (error) {\n      console.error(error.message);\n      return { invocation: null, message: error.message };\n    }\n  },\n\n  get: async function (clause = {}) {\n    try {\n      const invocation = await prisma.workspace_agent_invocations.findFirst({\n        where: clause,\n      });\n\n      return invocation || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  getWithWorkspace: async function (clause = {}) {\n    try {\n      const invocation = await prisma.workspace_agent_invocations.findFirst({\n        where: clause,\n        include: {\n          workspace: true,\n        },\n      });\n\n      return invocation || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.workspace_agent_invocations.delete({\n        where: clause,\n      });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  where: async function (clause = {}, limit = null, orderBy = null) {\n    try {\n      const results = await prisma.workspace_agent_invocations.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n};\n\nmodule.exports = { WorkspaceAgentInvocation };\n"
  },
  {
    "path": "server/models/workspaceChats.js",
    "content": "const prisma = require(\"../utils/prisma\");\nconst { safeJSONStringify } = require(\"../utils/helpers/chat/responses\");\n\nconst WorkspaceChats = {\n  new: async function ({\n    workspaceId,\n    prompt,\n    response = {},\n    user = null,\n    threadId = null,\n    include = true,\n    apiSessionId = null,\n  }) {\n    try {\n      const chat = await prisma.workspace_chats.create({\n        data: {\n          workspaceId,\n          prompt,\n          response: safeJSONStringify(response),\n          user_id: user?.id || null,\n          thread_id: threadId,\n          api_session_id: apiSessionId,\n          include,\n        },\n      });\n      return { chat, message: null };\n    } catch (error) {\n      console.error(error.message);\n      return { chat: null, message: error.message };\n    }\n  },\n\n  forWorkspaceByUser: async function (\n    workspaceId = null,\n    userId = null,\n    limit = null,\n    orderBy = null\n  ) {\n    if (!workspaceId || !userId) return [];\n    try {\n      const chats = await prisma.workspace_chats.findMany({\n        where: {\n          workspaceId,\n          user_id: userId,\n          thread_id: null, // this function is now only used for the default thread on workspaces and users\n          api_session_id: null, // do not include api-session chats in the frontend for anyone.\n          include: true,\n        },\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : { orderBy: { id: \"asc\" } }),\n      });\n      return chats;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  forWorkspaceByApiSessionId: async function (\n    workspaceId = null,\n    apiSessionId = null,\n    limit = null,\n    orderBy = null\n  ) {\n    if (!workspaceId || !apiSessionId) return [];\n    try {\n      const chats = await prisma.workspace_chats.findMany({\n        where: {\n          workspaceId,\n          user_id: null,\n          api_session_id: String(apiSessionId),\n          thread_id: null,\n        },\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : { orderBy: { id: \"asc\" } }),\n      });\n      return chats;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  forWorkspace: async function (\n    workspaceId = null,\n    limit = null,\n    orderBy = null\n  ) {\n    if (!workspaceId) return [];\n    try {\n      const chats = await prisma.workspace_chats.findMany({\n        where: {\n          workspaceId,\n          thread_id: null, // this function is now only used for the default thread on workspaces\n          api_session_id: null, // do not include api-session chats in the frontend for anyone.\n          include: true,\n        },\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : { orderBy: { id: \"asc\" } }),\n      });\n      return chats;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  /**\n   * @deprecated Use markThreadHistoryInvalidV2 instead.\n   */\n  markHistoryInvalid: async function (workspaceId = null, user = null) {\n    if (!workspaceId) return;\n    try {\n      await prisma.workspace_chats.updateMany({\n        where: {\n          workspaceId,\n          user_id: user?.id,\n          thread_id: null, // this function is now only used for the default thread on workspaces\n        },\n        data: {\n          include: false,\n        },\n      });\n      return;\n    } catch (error) {\n      console.error(error.message);\n    }\n  },\n\n  /**\n   * @deprecated Use markThreadHistoryInvalidV2 instead.\n   */\n  markThreadHistoryInvalid: async function (\n    workspaceId = null,\n    user = null,\n    threadId = null\n  ) {\n    if (!workspaceId || !threadId) return;\n    try {\n      await prisma.workspace_chats.updateMany({\n        where: {\n          workspaceId,\n          thread_id: threadId,\n          user_id: user?.id,\n        },\n        data: {\n          include: false,\n        },\n      });\n      return;\n    } catch (error) {\n      console.error(error.message);\n    }\n  },\n\n  /**\n   * @description This function is used to mark a thread's history as invalid.\n   * and works with an arbitrary where clause.\n   * @param {Object} whereClause - The where clause to update the chats.\n   * @param {Object} data - The data to update the chats with.\n   * @returns {Promise<void>}\n   */\n  markThreadHistoryInvalidV2: async function (whereClause = {}) {\n    if (!whereClause) return;\n    try {\n      await prisma.workspace_chats.updateMany({\n        where: whereClause,\n        data: {\n          include: false,\n        },\n      });\n      return;\n    } catch (error) {\n      console.error(error.message);\n    }\n  },\n\n  get: async function (clause = {}, limit = null, orderBy = null) {\n    try {\n      const chat = await prisma.workspace_chats.findFirst({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return chat || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.workspace_chats.deleteMany({\n        where: clause,\n      });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  where: async function (\n    clause = {},\n    limit = null,\n    orderBy = null,\n    offset = null\n  ) {\n    try {\n      const chats = await prisma.workspace_chats.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(offset !== null ? { skip: offset } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n      });\n      return chats;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  count: async function (clause = {}) {\n    try {\n      const count = await prisma.workspace_chats.count({\n        where: clause,\n      });\n      return count;\n    } catch (error) {\n      console.error(error.message);\n      return 0;\n    }\n  },\n\n  whereWithData: async function (\n    clause = {},\n    limit = null,\n    offset = null,\n    orderBy = null\n  ) {\n    const { Workspace } = require(\"./workspace\");\n    const { User } = require(\"./user\");\n\n    try {\n      const results = await this.where(clause, limit, orderBy, offset);\n\n      for (const res of results) {\n        const workspace = await Workspace.get({ id: res.workspaceId });\n        res.workspace = workspace\n          ? { name: workspace.name, slug: workspace.slug }\n          : { name: \"deleted workspace\", slug: null };\n\n        const user = res.user_id ? await User.get({ id: res.user_id }) : null;\n        res.user = user\n          ? { username: user.username }\n          : { username: res.api_session_id !== null ? \"API\" : \"unknown user\" };\n      }\n\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n  updateFeedbackScore: async function (chatId = null, feedbackScore = null) {\n    if (!chatId) return;\n    try {\n      await prisma.workspace_chats.update({\n        where: {\n          id: Number(chatId),\n        },\n        data: {\n          feedbackScore:\n            feedbackScore === null ? null : Number(feedbackScore) === 1,\n        },\n      });\n      return;\n    } catch (error) {\n      console.error(error.message);\n    }\n  },\n\n  // Explicit update of settings + key validations.\n  // Only use this method when directly setting a key value\n  // that takes no user input for the keys being modified.\n  _update: async function (id = null, data = {}) {\n    if (!id) throw new Error(\"No workspace chat id provided for update\");\n\n    try {\n      await prisma.workspace_chats.update({\n        where: { id },\n        data,\n      });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n  bulkCreate: async function (chatsData) {\n    // TODO: Replace with createMany when we update prisma to latest version\n    // The version of prisma that we are currently using does not support createMany with SQLite\n    try {\n      const createdChats = [];\n      for (const chatData of chatsData) {\n        const chat = await prisma.workspace_chats.create({\n          data: chatData,\n        });\n        createdChats.push(chat);\n      }\n      return { chats: createdChats, message: null };\n    } catch (error) {\n      console.error(error.message);\n      return { chats: null, message: error.message };\n    }\n  },\n};\n\nmodule.exports = { WorkspaceChats };\n"
  },
  {
    "path": "server/models/workspaceParsedFiles.js",
    "content": "const prisma = require(\"../utils/prisma\");\nconst { EventLogs } = require(\"./eventLogs\");\nconst { Document } = require(\"./documents\");\nconst { documentsPath, directUploadsPath } = require(\"../utils/files\");\nconst { safeJsonParse } = require(\"../utils/http\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\nconst WorkspaceParsedFiles = {\n  create: async function ({\n    filename,\n    workspaceId,\n    userId = null,\n    threadId = null,\n    metadata = null,\n    tokenCountEstimate = 0,\n  }) {\n    try {\n      const file = await prisma.workspace_parsed_files.create({\n        data: {\n          filename,\n          workspaceId: parseInt(workspaceId),\n          userId: userId ? parseInt(userId) : null,\n          threadId: threadId ? parseInt(threadId) : null,\n          metadata,\n          tokenCountEstimate,\n        },\n      });\n\n      await EventLogs.logEvent(\n        \"workspace_file_uploaded\",\n        {\n          filename,\n          workspaceId,\n        },\n        userId\n      );\n\n      return { file, error: null };\n    } catch (error) {\n      console.error(\"FAILED TO CREATE PARSED FILE RECORD.\", error.message);\n      return { file: null, error: error.message };\n    }\n  },\n\n  /**\n   * Gets a parsed file by its ID or a clause.\n   * @param {object} clause - The clause to filter the parsed files.\n   * @returns {Promise<import(\"@prisma/client\").workspace_parsed_files | null>} The parsed file.\n   */\n  get: async function (clause = {}) {\n    try {\n      const file = await prisma.workspace_parsed_files.findFirst({\n        where: clause,\n      });\n      return file;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  where: async function (\n    clause = {},\n    limit = null,\n    orderBy = null,\n    select = null\n  ) {\n    try {\n      const files = await prisma.workspace_parsed_files.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n        ...(select !== null ? { select } : {}),\n      });\n      return files;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      const result = await prisma.workspace_parsed_files.deleteMany({\n        where: clause,\n      });\n      return result.count > 0;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  totalTokenCount: async function (clause = {}) {\n    const { _sum } = await prisma.workspace_parsed_files.aggregate({\n      where: clause,\n      _sum: { tokenCountEstimate: true },\n    });\n    return _sum.tokenCountEstimate || 0;\n  },\n\n  /**\n   * Moves a parsed file to the documents and embeds it.\n   * @param {import(\"@prisma/client\").users | null} user - The user performing the operation.\n   * @param {number} fileId - The ID of the parsed file.\n   * @param {import(\"@prisma/client\").workspaces} workspace - The workspace the file belongs to.\n   * @returns {Promise<{ success: boolean, error: string | null, document: import(\"@prisma/client\").workspace_documents | null }>} The result of the operation.\n   */\n  moveToDocumentsAndEmbed: async function (user = null, fileId, workspace) {\n    try {\n      const parsedFile = await this.get({\n        id: parseInt(fileId),\n        ...(user ? { userId: user.id } : {}),\n        workspaceId: workspace.id,\n      });\n      if (!parsedFile) throw new Error(\"File not found\");\n\n      // Get file location from metadata\n      const metadata = safeJsonParse(parsedFile.metadata, {});\n      const location = metadata.location;\n      if (!location) throw new Error(\"No file location in metadata\");\n\n      // Get file from metadata location\n      const sourceFile = path.join(directUploadsPath, path.basename(location));\n      if (!fs.existsSync(sourceFile)) throw new Error(\"Source file not found\");\n\n      // Move to custom-documents\n      const customDocsPath = path.join(documentsPath, \"custom-documents\");\n      if (!fs.existsSync(customDocsPath))\n        fs.mkdirSync(customDocsPath, { recursive: true });\n\n      // Copy the file to custom-documents\n      const targetPath = path.join(customDocsPath, path.basename(location));\n      fs.copyFileSync(sourceFile, targetPath);\n      fs.unlinkSync(sourceFile);\n\n      const {\n        failedToEmbed = [],\n        errors = [],\n        embedded = [],\n      } = await Document.addDocuments(\n        workspace,\n        [`custom-documents/${path.basename(location)}`],\n        parsedFile.userId\n      );\n\n      if (failedToEmbed.length > 0)\n        throw new Error(errors[0] || \"Failed to embed document\");\n\n      const document = await Document.get({\n        workspaceId: workspace.id,\n        docpath: embedded[0],\n      });\n      return { success: true, error: null, document };\n    } catch (error) {\n      console.error(\"Failed to move and embed file:\", error);\n      return { success: false, error: error.message, document: null };\n    } finally {\n      // Always delete the file after processing\n      await this.delete({ id: parseInt(fileId) });\n    }\n  },\n\n  getContextMetadataAndLimits: async function (\n    workspace,\n    thread = null,\n    user = null\n  ) {\n    try {\n      if (!workspace) throw new Error(\"Workspace is required\");\n      const files = await this.where({\n        workspaceId: workspace.id,\n        threadId: thread?.id || null,\n        ...(user ? { userId: user.id } : {}),\n      });\n\n      const results = [];\n      let totalTokens = 0;\n\n      for (const file of files) {\n        const metadata = safeJsonParse(file.metadata, {});\n        totalTokens += file.tokenCountEstimate || 0;\n        results.push({\n          id: file.id,\n          title: metadata.title || metadata.location,\n          location: metadata.location,\n          token_count_estimate: file.tokenCountEstimate,\n        });\n      }\n\n      return {\n        files: results,\n        contextWindow: workspace.contextWindow,\n        currentContextTokenCount: totalTokens,\n      };\n    } catch (error) {\n      console.error(\"Failed to get context metadata:\", error);\n      return {\n        files: [],\n        contextWindow: Infinity,\n        currentContextTokenCount: 0,\n      };\n    }\n  },\n\n  getContextFiles: async function (workspace, thread = null, user = null) {\n    try {\n      const files = await this.where({\n        workspaceId: workspace.id,\n        threadId: thread?.id || null,\n        ...(user ? { userId: user.id } : {}),\n      });\n\n      const results = [];\n      for (const file of files) {\n        const metadata = safeJsonParse(file.metadata, {});\n        const location = metadata.location;\n        if (!location) continue;\n\n        const sourceFile = path.join(\n          directUploadsPath,\n          path.basename(location)\n        );\n        if (!fs.existsSync(sourceFile)) continue;\n\n        const content = fs.readFileSync(sourceFile, \"utf-8\");\n        const data = safeJsonParse(content, null);\n        if (!data?.pageContent) continue;\n\n        results.push({\n          pageContent: data.pageContent,\n          token_count_estimate: file.tokenCountEstimate,\n          ...metadata,\n        });\n      }\n\n      return results;\n    } catch (error) {\n      console.error(\"Failed to get context files:\", error);\n      return [];\n    }\n  },\n};\n\nmodule.exports = { WorkspaceParsedFiles };\n"
  },
  {
    "path": "server/models/workspaceThread.js",
    "content": "const prisma = require(\"../utils/prisma\");\nconst slugifyModule = require(\"slugify\");\nconst { v4: uuidv4 } = require(\"uuid\");\n\nconst WorkspaceThread = {\n  defaultName: \"Thread\",\n  writable: [\"name\"],\n\n  /**\n   * The default Slugify module requires some additional mapping to prevent downstream issues\n   * if the user is able to define a slug externally. We have to block non-escapable URL chars\n   * so that is the slug is rendered it doesn't break the URL or UI when visited.\n   * @param  {...any} args - slugify args for npm package.\n   * @returns {string}\n   */\n  slugify: function (...args) {\n    slugifyModule.extend({\n      \"+\": \" plus \",\n      \"!\": \" bang \",\n      \"@\": \" at \",\n      \"*\": \" splat \",\n      \".\": \" dot \",\n      \":\": \"\",\n      \"~\": \"\",\n      \"(\": \"\",\n      \")\": \"\",\n      \"'\": \"\",\n      '\"': \"\",\n      \"|\": \"\",\n    });\n    return slugifyModule(...args);\n  },\n\n  new: async function (workspace, userId = null, data = {}) {\n    try {\n      const thread = await prisma.workspace_threads.create({\n        data: {\n          name: data.name ? String(data.name) : this.defaultName,\n          slug: data.slug\n            ? this.slugify(data.slug, { lowercase: true })\n            : uuidv4(),\n          user_id: userId ? Number(userId) : null,\n          workspace_id: workspace.id,\n        },\n      });\n\n      return { thread, message: null };\n    } catch (error) {\n      console.error(error.message);\n      return { thread: null, message: error.message };\n    }\n  },\n\n  update: async function (prevThread = null, data = {}) {\n    if (!prevThread) throw new Error(\"No thread id provided for update\");\n\n    const validData = {};\n    Object.entries(data).forEach(([key, value]) => {\n      if (!this.writable.includes(key)) return;\n      validData[key] = value;\n    });\n\n    if (Object.keys(validData).length === 0)\n      return { thread: prevThread, message: \"No valid fields to update!\" };\n\n    try {\n      const thread = await prisma.workspace_threads.update({\n        where: { id: prevThread.id },\n        data: validData,\n      });\n      return { thread, message: null };\n    } catch (error) {\n      console.error(error.message);\n      return { thread: null, message: error.message };\n    }\n  },\n\n  get: async function (clause = {}) {\n    try {\n      const thread = await prisma.workspace_threads.findFirst({\n        where: clause,\n      });\n\n      return thread || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.workspace_threads.deleteMany({\n        where: clause,\n      });\n      return true;\n    } catch (error) {\n      console.error(error.message);\n      return false;\n    }\n  },\n\n  where: async function (\n    clause = {},\n    limit = null,\n    orderBy = null,\n    include = null\n  ) {\n    try {\n      const results = await prisma.workspace_threads.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n        ...(orderBy !== null ? { orderBy } : {}),\n        ...(include !== null ? { include } : {}),\n      });\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  // Will fire on first message (included or not) for a thread and rename the thread with the newName prop.\n  autoRenameThread: async function ({\n    workspace = null,\n    thread = null,\n    user = null,\n    newName = null,\n    onRename = null,\n  }) {\n    if (!workspace || !thread || !newName) return false;\n    if (thread.name !== this.defaultName) return false; // don't rename if already named.\n\n    const { WorkspaceChats } = require(\"./workspaceChats\");\n    const chatCount = await WorkspaceChats.count({\n      workspaceId: workspace.id,\n      user_id: user?.id || null,\n      thread_id: thread.id,\n    });\n    if (chatCount !== 1) return { renamed: false, thread };\n    const { thread: updatedThread } = await this.update(thread, {\n      name: newName,\n    });\n\n    onRename?.(updatedThread);\n    return true;\n  },\n};\n\nmodule.exports = { WorkspaceThread };\n"
  },
  {
    "path": "server/models/workspaceUsers.js",
    "content": "const prisma = require(\"../utils/prisma\");\n\nconst WorkspaceUser = {\n  createMany: async function (userId, workspaceIds = []) {\n    if (workspaceIds.length === 0) return;\n    try {\n      await prisma.$transaction(\n        workspaceIds.map((workspaceId) =>\n          prisma.workspace_users.create({\n            data: { user_id: userId, workspace_id: workspaceId },\n          })\n        )\n      );\n    } catch (error) {\n      console.error(error.message);\n    }\n    return;\n  },\n\n  /**\n   * Create many workspace users.\n   * @param {Array<number>} userIds - An array of user IDs to create workspace users for.\n   * @param {number} workspaceId - The ID of the workspace to create workspace users for.\n   * @returns {Promise<void>} A promise that resolves when the workspace users are created.\n   */\n  createManyUsers: async function (userIds = [], workspaceId) {\n    if (userIds.length === 0) return;\n    try {\n      await prisma.$transaction(\n        userIds.map((userId) =>\n          prisma.workspace_users.create({\n            data: {\n              user_id: Number(userId),\n              workspace_id: Number(workspaceId),\n            },\n          })\n        )\n      );\n    } catch (error) {\n      console.error(error.message);\n    }\n    return;\n  },\n\n  create: async function (userId = 0, workspaceId = 0) {\n    try {\n      await prisma.workspace_users.create({\n        data: { user_id: Number(userId), workspace_id: Number(workspaceId) },\n      });\n      return true;\n    } catch (error) {\n      console.error(\n        \"FAILED TO CREATE WORKSPACE_USER RELATIONSHIP.\",\n        error.message\n      );\n      return false;\n    }\n  },\n\n  get: async function (clause = {}) {\n    try {\n      const result = await prisma.workspace_users.findFirst({ where: clause });\n      return result || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  where: async function (clause = {}, limit = null) {\n    try {\n      const results = await prisma.workspace_users.findMany({\n        where: clause,\n        ...(limit !== null ? { take: limit } : {}),\n      });\n      return results;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  count: async function (clause = {}) {\n    try {\n      const count = await prisma.workspace_users.count({ where: clause });\n      return count;\n    } catch (error) {\n      console.error(error.message);\n      return 0;\n    }\n  },\n\n  delete: async function (clause = {}) {\n    try {\n      await prisma.workspace_users.deleteMany({ where: clause });\n    } catch (error) {\n      console.error(error.message);\n    }\n    return;\n  },\n};\n\nmodule.exports.WorkspaceUser = WorkspaceUser;\n"
  },
  {
    "path": "server/models/workspacesSuggestedMessages.js",
    "content": "const prisma = require(\"../utils/prisma\");\n\nconst WorkspaceSuggestedMessages = {\n  get: async function (clause = {}) {\n    try {\n      const message = await prisma.workspace_suggested_messages.findFirst({\n        where: clause,\n      });\n      return message || null;\n    } catch (error) {\n      console.error(error.message);\n      return null;\n    }\n  },\n\n  where: async function (clause = {}, limit) {\n    try {\n      const messages = await prisma.workspace_suggested_messages.findMany({\n        where: clause,\n        take: limit || undefined,\n      });\n      return messages;\n    } catch (error) {\n      console.error(error.message);\n      return [];\n    }\n  },\n\n  saveAll: async function (messages, workspaceSlug) {\n    try {\n      const workspace = await prisma.workspaces.findUnique({\n        where: { slug: workspaceSlug },\n      });\n\n      if (!workspace) throw new Error(\"Workspace not found\");\n\n      // Delete all existing messages for the workspace\n      await prisma.workspace_suggested_messages.deleteMany({\n        where: { workspaceId: workspace.id },\n      });\n\n      // Create new messages\n      // We create each message individually because prisma\n      // with sqlite does not support createMany()\n      for (const message of messages) {\n        await prisma.workspace_suggested_messages.create({\n          data: {\n            workspaceId: workspace.id,\n            heading: message.heading,\n            message: message.message,\n          },\n        });\n      }\n    } catch (error) {\n      console.error(\"Failed to save all messages\", error.message);\n    }\n  },\n\n  getMessages: async function (workspaceSlug) {\n    try {\n      const workspace = await prisma.workspaces.findUnique({\n        where: { slug: workspaceSlug },\n      });\n\n      if (!workspace) throw new Error(\"Workspace not found\");\n\n      const messages = await prisma.workspace_suggested_messages.findMany({\n        where: { workspaceId: workspace.id },\n        orderBy: { createdAt: \"asc\" },\n      });\n\n      return messages.map((msg) => ({\n        heading: msg.heading,\n        message: msg.message,\n      }));\n    } catch (error) {\n      console.error(\"Failed to get all messages\", error.message);\n      return [];\n    }\n  },\n};\n\nmodule.exports.WorkspaceSuggestedMessages = WorkspaceSuggestedMessages;\n"
  },
  {
    "path": "server/nodemon.json",
    "content": "{\n  \"events\": {\n    \"start\": \"yarn swagger\",\n    \"restart\": \"yarn swagger\"\n  }\n}"
  },
  {
    "path": "server/package.json",
    "content": "{\n  \"name\": \"anything-llm-server\",\n  \"version\": \"1.11.1\",\n  \"description\": \"Server endpoints to process or create content for chatting\",\n  \"main\": \"index.js\",\n  \"author\": \"Timothy Carambat (Mintplex Labs)\",\n  \"license\": \"MIT\",\n  \"private\": false,\n  \"engines\": {\n    \"node\": \">=18.12.1\"\n  },\n  \"scripts\": {\n    \"dev\": \"cross-env NODE_ENV=development nodemon --ignore documents --ignore vector-cache --ignore storage --ignore swagger --trace-warnings index.js\",\n    \"start\": \"cross-env NODE_ENV=production node index.js\",\n    \"lint\": \"eslint --fix .\",\n    \"lint:check\": \"eslint .\",\n    \"swagger\": \"node ./swagger/init.js\"\n  },\n  \"prisma\": {\n    \"seed\": \"node prisma/seed.js\"\n  },\n  \"dependencies\": {\n    \"@anthropic-ai/sdk\": \"^0.39.0\",\n    \"@aws-sdk/client-bedrock-runtime\": \"^3.775.0\",\n    \"@datastax/astra-db-ts\": \"^0.1.3\",\n    \"@ladjs/graceful\": \"^3.2.2\",\n    \"@lancedb/lancedb\": \"0.15.0\",\n    \"@langchain/anthropic\": \"0.1.16\",\n    \"@langchain/aws\": \"^0.0.5\",\n    \"@langchain/cohere\": \"0.0.11\",\n    \"@langchain/community\": \"0.0.53\",\n    \"@langchain/core\": \"0.1.61\",\n    \"@langchain/openai\": \"0.0.28\",\n    \"@langchain/textsplitters\": \"0.0.0\",\n    \"@mintplex-labs/bree\": \"^9.2.5\",\n    \"@mintplex-labs/express-ws\": \"^5.0.7\",\n    \"@modelcontextprotocol/sdk\": \"^1.24.3\",\n    \"@pinecone-database/pinecone\": \"^2.0.1\",\n    \"@prisma/client\": \"5.3.1\",\n    \"@qdrant/js-client-rest\": \"^1.9.0\",\n    \"@xenova/transformers\": \"^2.14.0\",\n    \"@zilliz/milvus2-sdk-node\": \"^2.3.5\",\n    \"adm-zip\": \"^0.5.16\",\n    \"apache-arrow\": \"19.0.0\",\n    \"bcryptjs\": \"^3.0.3\",\n    \"body-parser\": \"^1.20.3\",\n    \"chalk\": \"^4\",\n    \"check-disk-space\": \"^3.4.0\",\n    \"cheerio\": \"^1.0.0\",\n    \"chromadb\": \"^2.0.1\",\n    \"cohere-ai\": \"^7.19.0\",\n    \"cors\": \"^2.8.5\",\n    \"dotenv\": \"^16.0.3\",\n    \"elevenlabs\": \"^0.5.0\",\n    \"express\": \"^4.21.2\",\n    \"extract-json-from-string\": \"^1.0.1\",\n    \"fast-levenshtein\": \"^3.0.0\",\n    \"fix-path\": \"^4.0.0\",\n    \"graphql\": \"^16.7.1\",\n    \"ip\": \"^2.0.1\",\n    \"joi\": \"^17.11.0\",\n    \"joi-password-complexity\": \"^5.2.0\",\n    \"js-tiktoken\": \"^1.0.8\",\n    \"jsonrepair\": \"^3.7.0\",\n    \"jsonwebtoken\": \"^9.0.0\",\n    \"langchain\": \"0.1.36\",\n    \"mime\": \"^3.0.0\",\n    \"moment\": \"^2.29.4\",\n    \"mssql\": \"^10.0.2\",\n    \"multer\": \"2.0.0\",\n    \"mysql2\": \"^3.9.8\",\n    \"ollama\": \"^0.6.3\",\n    \"openai\": \"4.95.1\",\n    \"pg\": \"^8.11.5\",\n    \"pinecone-client\": \"^1.1.0\",\n    \"pluralize\": \"^8.0.0\",\n    \"posthog-node\": \"^3.1.1\",\n    \"prisma\": \"5.3.1\",\n    \"slugify\": \"^1.6.6\",\n    \"strip-ansi\": \"^7.1.2\",\n    \"swagger-autogen\": \"^2.23.5\",\n    \"swagger-ui-express\": \"^5.0.0\",\n    \"truncate\": \"^3.0.0\",\n    \"url-pattern\": \"^1.0.3\",\n    \"uuid\": \"^9.0.0\",\n    \"uuid-apikey\": \"^1.5.3\",\n    \"weaviate-ts-client\": \"^1.4.0\",\n    \"web-push\": \"^3.6.7\",\n    \"winston\": \"^3.13.0\"\n  },\n  \"resolutions\": {\n    \"**/graphql-request/form-data\": \"3.0.4\",\n    \"form-data\": \"4.0.4\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"9\",\n    \"@inquirer/prompts\": \"^4.3.1\",\n    \"cross-env\": \"^7.0.3\",\n    \"eslint\": \"9\",\n    \"eslint-config-prettier\": \"^9.0.0\",\n    \"eslint-plugin-ft-flow\": \"^3.0.0\",\n    \"eslint-plugin-prettier\": \"^5.0.0\",\n    \"eslint-plugin-react\": \"^7.33.2\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"eslint-plugin-react-refresh\": \"^0.4.3\",\n    \"eslint-plugin-unused-imports\": \"^4.4.1\",\n    \"flow-bin\": \"^0.217.0\",\n    \"flow-remove-types\": \"^2.217.1\",\n    \"globals\": \"^17.4.0\",\n    \"hermes-eslint\": \"^0.15.0\",\n    \"node-html-markdown\": \"^1.3.0\",\n    \"nodemon\": \"^2.0.22\",\n    \"prettier\": \"^3.0.3\"\n  }\n}\n"
  },
  {
    "path": "server/prisma/migrations/20230921191814_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"api_keys\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"secret\" TEXT,\n    \"createdBy\" INTEGER,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\n-- CreateTable\nCREATE TABLE \"workspace_documents\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"docId\" TEXT NOT NULL,\n    \"filename\" TEXT NOT NULL,\n    \"docpath\" TEXT NOT NULL,\n    \"workspaceId\" INTEGER NOT NULL,\n    \"metadata\" TEXT,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"workspace_documents_workspaceId_fkey\" FOREIGN KEY (\"workspaceId\") REFERENCES \"workspaces\" (\"id\") ON DELETE RESTRICT ON UPDATE CASCADE\n);\n\n-- CreateTable\nCREATE TABLE \"invites\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"code\" TEXT NOT NULL,\n    \"status\" TEXT NOT NULL DEFAULT 'pending',\n    \"claimedBy\" INTEGER,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"createdBy\" INTEGER NOT NULL,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\n-- CreateTable\nCREATE TABLE \"system_settings\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"label\" TEXT NOT NULL,\n    \"value\" TEXT,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\n-- CreateTable\nCREATE TABLE \"users\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"username\" TEXT,\n    \"password\" TEXT NOT NULL,\n    \"role\" TEXT NOT NULL DEFAULT 'default',\n    \"suspended\" INTEGER NOT NULL DEFAULT 0,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\n-- CreateTable\nCREATE TABLE \"document_vectors\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"docId\" TEXT NOT NULL,\n    \"vectorId\" TEXT NOT NULL,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\n-- CreateTable\nCREATE TABLE \"welcome_messages\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"user\" TEXT NOT NULL,\n    \"response\" TEXT NOT NULL,\n    \"orderIndex\" INTEGER,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\n-- CreateTable\nCREATE TABLE \"workspaces\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"name\" TEXT NOT NULL,\n    \"slug\" TEXT NOT NULL,\n    \"vectorTag\" TEXT,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"openAiTemp\" REAL,\n    \"openAiHistory\" INTEGER NOT NULL DEFAULT 20,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"openAiPrompt\" TEXT\n);\n\n-- CreateTable\nCREATE TABLE \"workspace_chats\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"workspaceId\" INTEGER NOT NULL,\n    \"prompt\" TEXT NOT NULL,\n    \"response\" TEXT NOT NULL,\n    \"include\" BOOLEAN NOT NULL DEFAULT true,\n    \"user_id\" INTEGER,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"workspace_chats_user_id_fkey\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateTable\nCREATE TABLE \"workspace_users\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"user_id\" INTEGER NOT NULL,\n    \"workspace_id\" INTEGER NOT NULL,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"workspace_users_workspace_id_fkey\" FOREIGN KEY (\"workspace_id\") REFERENCES \"workspaces\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE,\n    CONSTRAINT \"workspace_users_user_id_fkey\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"api_keys_secret_key\" ON \"api_keys\"(\"secret\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"workspace_documents_docId_key\" ON \"workspace_documents\"(\"docId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"invites_code_key\" ON \"invites\"(\"code\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"system_settings_label_key\" ON \"system_settings\"(\"label\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"users_username_key\" ON \"users\"(\"username\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"workspaces_slug_key\" ON \"workspaces\"(\"slug\");\n"
  },
  {
    "path": "server/prisma/migrations/20231101001441_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspaces\" ADD COLUMN \"similarityThreshold\" REAL DEFAULT 0.25;\n"
  },
  {
    "path": "server/prisma/migrations/20231101195421_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"cache_data\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"name\" TEXT NOT NULL,\n    \"data\" TEXT NOT NULL,\n    \"belongsTo\" TEXT,\n    \"byId\" INTEGER,\n    \"expiresAt\" DATETIME,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n"
  },
  {
    "path": "server/prisma/migrations/20231129012019_add/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"users\" ADD COLUMN \"pfpFilename\" TEXT;\n"
  },
  {
    "path": "server/prisma/migrations/20240113013409_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspaces\" ADD COLUMN \"chatModel\" TEXT;\n"
  },
  {
    "path": "server/prisma/migrations/20240118201333_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspaces\" ADD COLUMN \"topN\" INTEGER DEFAULT 4 CHECK (\"topN\" > 0);\n"
  },
  {
    "path": "server/prisma/migrations/20240202002020_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"embed_configs\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"uuid\" TEXT NOT NULL,\n    \"enabled\" BOOLEAN NOT NULL DEFAULT false,\n    \"chat_mode\" TEXT NOT NULL DEFAULT 'query',\n    \"allowlist_domains\" TEXT,\n    \"allow_model_override\" BOOLEAN NOT NULL DEFAULT false,\n    \"allow_temperature_override\" BOOLEAN NOT NULL DEFAULT false,\n    \"allow_prompt_override\" BOOLEAN NOT NULL DEFAULT false,\n    \"max_chats_per_day\" INTEGER,\n    \"max_chats_per_session\" INTEGER,\n    \"workspace_id\" INTEGER NOT NULL,\n    \"createdBy\" INTEGER,\n    \"usersId\" INTEGER,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"embed_configs_workspace_id_fkey\" FOREIGN KEY (\"workspace_id\") REFERENCES \"workspaces\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE,\n    CONSTRAINT \"embed_configs_usersId_fkey\" FOREIGN KEY (\"usersId\") REFERENCES \"users\" (\"id\") ON DELETE SET NULL ON UPDATE CASCADE\n);\n\n-- CreateTable\nCREATE TABLE \"embed_chats\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"prompt\" TEXT NOT NULL,\n    \"response\" TEXT NOT NULL,\n    \"session_id\" TEXT NOT NULL,\n    \"include\" BOOLEAN NOT NULL DEFAULT true,\n    \"connection_information\" TEXT,\n    \"embed_id\" INTEGER NOT NULL,\n    \"usersId\" INTEGER,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"embed_chats_embed_id_fkey\" FOREIGN KEY (\"embed_id\") REFERENCES \"embed_configs\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE,\n    CONSTRAINT \"embed_chats_usersId_fkey\" FOREIGN KEY (\"usersId\") REFERENCES \"users\" (\"id\") ON DELETE SET NULL ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"embed_configs_uuid_key\" ON \"embed_configs\"(\"uuid\");\n"
  },
  {
    "path": "server/prisma/migrations/20240206181106_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"workspace_suggested_messages\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"workspaceId\" INTEGER NOT NULL,\n    \"heading\" TEXT NOT NULL,\n    \"message\" TEXT NOT NULL,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"workspace_suggested_messages_workspaceId_fkey\" FOREIGN KEY (\"workspaceId\") REFERENCES \"workspaces\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE INDEX \"workspace_suggested_messages_workspaceId_idx\" ON \"workspace_suggested_messages\"(\"workspaceId\");\n"
  },
  {
    "path": "server/prisma/migrations/20240206211916_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"event_logs\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"event\" TEXT NOT NULL,\n    \"metadata\" TEXT,\n    \"userId\" INTEGER,\n    \"occurredAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\n-- CreateIndex\nCREATE INDEX \"event_logs_event_idx\" ON \"event_logs\"(\"event\");\n"
  },
  {
    "path": "server/prisma/migrations/20240208224848_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspace_chats\" ADD COLUMN \"thread_id\" INTEGER;\n\n-- CreateTable\nCREATE TABLE \"workspace_threads\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"name\" TEXT NOT NULL,\n    \"slug\" TEXT NOT NULL,\n    \"workspace_id\" INTEGER NOT NULL,\n    \"user_id\" INTEGER,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"workspace_threads_workspace_id_fkey\" FOREIGN KEY (\"workspace_id\") REFERENCES \"workspaces\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE,\n    CONSTRAINT \"workspace_threads_user_id_fkey\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"workspace_threads_slug_key\" ON \"workspace_threads\"(\"slug\");\n\n-- CreateIndex\nCREATE INDEX \"workspace_threads_workspace_id_idx\" ON \"workspace_threads\"(\"workspace_id\");\n\n-- CreateIndex\nCREATE INDEX \"workspace_threads_user_id_idx\" ON \"workspace_threads\"(\"user_id\");\n"
  },
  {
    "path": "server/prisma/migrations/20240210004405_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspace_chats\" ADD COLUMN \"feedbackScore\" BOOLEAN;\n"
  },
  {
    "path": "server/prisma/migrations/20240216214639_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspaces\" ADD COLUMN \"chatMode\" TEXT DEFAULT 'chat';\n"
  },
  {
    "path": "server/prisma/migrations/20240219211018_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspace_documents\" ADD COLUMN \"pinned\" BOOLEAN DEFAULT false;\n"
  },
  {
    "path": "server/prisma/migrations/20240301002308_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspaces\" ADD COLUMN \"pfpFilename\" TEXT;\n"
  },
  {
    "path": "server/prisma/migrations/20240326231053_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"invites\" ADD COLUMN \"workspaceIds\" TEXT;\n"
  },
  {
    "path": "server/prisma/migrations/20240405015034_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspaces\" ADD COLUMN \"chatProvider\" TEXT;\n"
  },
  {
    "path": "server/prisma/migrations/20240412183346_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspaces\" ADD COLUMN \"agentModel\" TEXT;\nALTER TABLE \"workspaces\" ADD COLUMN \"agentProvider\" TEXT;\n\n-- CreateTable\nCREATE TABLE \"workspace_agent_invocations\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"uuid\" TEXT NOT NULL,\n    \"prompt\" TEXT NOT NULL,\n    \"closed\" BOOLEAN NOT NULL DEFAULT false,\n    \"user_id\" INTEGER,\n    \"thread_id\" INTEGER,\n    \"workspace_id\" INTEGER NOT NULL,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"workspace_agent_invocations_user_id_fkey\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE,\n    CONSTRAINT \"workspace_agent_invocations_workspace_id_fkey\" FOREIGN KEY (\"workspace_id\") REFERENCES \"workspaces\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"workspace_agent_invocations_uuid_key\" ON \"workspace_agent_invocations\"(\"uuid\");\n\n-- CreateIndex\nCREATE INDEX \"workspace_agent_invocations_uuid_idx\" ON \"workspace_agent_invocations\"(\"uuid\");\n"
  },
  {
    "path": "server/prisma/migrations/20240425004220_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"users\" ADD COLUMN \"seen_recovery_codes\" BOOLEAN DEFAULT false;\n\n-- CreateTable\nCREATE TABLE \"recovery_codes\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"user_id\" INTEGER NOT NULL,\n    \"code_hash\" TEXT NOT NULL,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"recovery_codes_user_id_fkey\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateTable\nCREATE TABLE \"password_reset_tokens\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"user_id\" INTEGER NOT NULL,\n    \"token\" TEXT NOT NULL,\n    \"expiresAt\" DATETIME NOT NULL,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"password_reset_tokens_user_id_fkey\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE INDEX \"recovery_codes_user_id_idx\" ON \"recovery_codes\"(\"user_id\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"password_reset_tokens_token_key\" ON \"password_reset_tokens\"(\"token\");\n\n-- CreateIndex\nCREATE INDEX \"password_reset_tokens_user_id_idx\" ON \"password_reset_tokens\"(\"user_id\");\n"
  },
  {
    "path": "server/prisma/migrations/20240430230707_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspaces\" ADD COLUMN \"queryRefusalResponse\" TEXT;\n"
  },
  {
    "path": "server/prisma/migrations/20240510032311_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"slash_command_presets\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"command\" TEXT NOT NULL,\n    \"prompt\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"uid\" INTEGER NOT NULL DEFAULT 0,\n    \"userId\" INTEGER,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUpdatedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"slash_command_presets_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"slash_command_presets_uid_command_key\" ON \"slash_command_presets\"(\"uid\", \"command\");\n"
  },
  {
    "path": "server/prisma/migrations/20240618224346_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspace_documents\" ADD COLUMN \"watched\" BOOLEAN DEFAULT false;\n\n-- CreateTable\nCREATE TABLE \"document_sync_queues\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"staleAfterMs\" INTEGER NOT NULL DEFAULT 604800000,\n    \"nextSyncAt\" DATETIME NOT NULL,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastSyncedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"workspaceDocId\" INTEGER NOT NULL,\n    CONSTRAINT \"document_sync_queues_workspaceDocId_fkey\" FOREIGN KEY (\"workspaceDocId\") REFERENCES \"workspace_documents\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateTable\nCREATE TABLE \"document_sync_executions\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"queueId\" INTEGER NOT NULL,\n    \"status\" TEXT NOT NULL DEFAULT 'unknown',\n    \"result\" TEXT,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"document_sync_executions_queueId_fkey\" FOREIGN KEY (\"queueId\") REFERENCES \"document_sync_queues\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"document_sync_queues_workspaceDocId_key\" ON \"document_sync_queues\"(\"workspaceDocId\");\n"
  },
  {
    "path": "server/prisma/migrations/20240821215625_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspace_chats\" ADD COLUMN \"api_session_id\" TEXT;\n"
  },
  {
    "path": "server/prisma/migrations/20240824005054_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"browser_extension_api_keys\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"key\" TEXT NOT NULL,\n    \"user_id\" INTEGER,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"lastUpdatedAt\" DATETIME NOT NULL,\n    CONSTRAINT \"browser_extension_api_keys_user_id_fkey\" FOREIGN KEY (\"user_id\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"browser_extension_api_keys_key_key\" ON \"browser_extension_api_keys\"(\"key\");\n\n-- CreateIndex\nCREATE INDEX \"browser_extension_api_keys_user_id_idx\" ON \"browser_extension_api_keys\"(\"user_id\");\n"
  },
  {
    "path": "server/prisma/migrations/20241003192954_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"users\" ADD COLUMN \"dailyMessageLimit\" INTEGER;\n"
  },
  {
    "path": "server/prisma/migrations/20241029203722_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"temporary_auth_tokens\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"token\" TEXT NOT NULL,\n    \"userId\" INTEGER NOT NULL,\n    \"expiresAt\" DATETIME NOT NULL,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"temporary_auth_tokens_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"temporary_auth_tokens_token_key\" ON \"temporary_auth_tokens\"(\"token\");\n"
  },
  {
    "path": "server/prisma/migrations/20241029233509_init/migration.sql",
    "content": "-- CreateIndex\nCREATE INDEX \"temporary_auth_tokens_token_idx\" ON \"temporary_auth_tokens\"(\"token\");\n\n-- CreateIndex\nCREATE INDEX \"temporary_auth_tokens_userId_idx\" ON \"temporary_auth_tokens\"(\"userId\");\n"
  },
  {
    "path": "server/prisma/migrations/20250102204948_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"workspaces\" ADD COLUMN \"vectorSearchMode\" TEXT DEFAULT 'default';\n"
  },
  {
    "path": "server/prisma/migrations/20250226005538_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"users\" ADD COLUMN \"bio\" TEXT DEFAULT '';\n"
  },
  {
    "path": "server/prisma/migrations/20250318154720_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"system_prompt_variables\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"key\" TEXT NOT NULL,\n    \"value\" TEXT,\n    \"description\" TEXT,\n    \"type\" TEXT NOT NULL DEFAULT 'system',\n    \"userId\" INTEGER,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" DATETIME NOT NULL,\n    CONSTRAINT \"system_prompt_variables_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"system_prompt_variables_key_key\" ON \"system_prompt_variables\"(\"key\");\n\n-- CreateIndex\nCREATE INDEX \"system_prompt_variables_userId_idx\" ON \"system_prompt_variables\"(\"userId\");\n"
  },
  {
    "path": "server/prisma/migrations/20250506214129_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"prompt_history\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"workspaceId\" INTEGER NOT NULL,\n    \"prompt\" TEXT NOT NULL,\n    \"modifiedBy\" INTEGER,\n    \"modifiedAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"prompt_history_workspaceId_fkey\" FOREIGN KEY (\"workspaceId\") REFERENCES \"workspaces\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE,\n    CONSTRAINT \"prompt_history_modifiedBy_fkey\" FOREIGN KEY (\"modifiedBy\") REFERENCES \"users\" (\"id\") ON DELETE SET NULL ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE INDEX \"prompt_history_workspaceId_idx\" ON \"prompt_history\"(\"workspaceId\");\n"
  },
  {
    "path": "server/prisma/migrations/20250709230835_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"embed_configs\" ADD COLUMN \"message_limit\" INTEGER DEFAULT 20;\n"
  },
  {
    "path": "server/prisma/migrations/20250725194841_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"desktop_mobile_devices\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"deviceOs\" TEXT NOT NULL,\n    \"deviceName\" TEXT NOT NULL,\n    \"token\" TEXT NOT NULL,\n    \"approved\" BOOLEAN NOT NULL DEFAULT false,\n    \"userId\" INTEGER,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"desktop_mobile_devices_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"desktop_mobile_devices_token_key\" ON \"desktop_mobile_devices\"(\"token\");\n\n-- CreateIndex\nCREATE INDEX \"desktop_mobile_devices_userId_idx\" ON \"desktop_mobile_devices\"(\"userId\");\n"
  },
  {
    "path": "server/prisma/migrations/20250808171557_init/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"workspace_parsed_files\" (\n    \"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,\n    \"filename\" TEXT NOT NULL,\n    \"workspaceId\" INTEGER NOT NULL,\n    \"userId\" INTEGER,\n    \"threadId\" INTEGER,\n    \"metadata\" TEXT,\n    \"tokenCountEstimate\" INTEGER DEFAULT 0,\n    \"createdAt\" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    CONSTRAINT \"workspace_parsed_files_workspaceId_fkey\" FOREIGN KEY (\"workspaceId\") REFERENCES \"workspaces\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE,\n    CONSTRAINT \"workspace_parsed_files_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"users\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE,\n    CONSTRAINT \"workspace_parsed_files_threadId_fkey\" FOREIGN KEY (\"threadId\") REFERENCES \"workspace_threads\" (\"id\") ON DELETE CASCADE ON UPDATE CASCADE\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"workspace_parsed_files_filename_key\" ON \"workspace_parsed_files\"(\"filename\");\n\n-- CreateIndex\nCREATE INDEX \"workspace_parsed_files_workspaceId_idx\" ON \"workspace_parsed_files\"(\"workspaceId\");\n\n-- CreateIndex\nCREATE INDEX \"workspace_parsed_files_userId_idx\" ON \"workspace_parsed_files\"(\"userId\");\n"
  },
  {
    "path": "server/prisma/migrations/20260130040204_init/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"users\" ADD COLUMN \"web_push_subscription_config\" TEXT;\n"
  },
  {
    "path": "server/prisma/migrations/20260313192859_init/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the `welcome_messages` table. If the table is not empty, all the data it contains will be lost.\n\n*/\n-- DropTable\nPRAGMA foreign_keys=off;\nDROP TABLE \"welcome_messages\";\nPRAGMA foreign_keys=on;\n"
  },
  {
    "path": "server/prisma/migrations/migration_lock.toml",
    "content": "# Please do not edit this file manually\n# It should be added in your version-control system (i.e. Git)\nprovider = \"sqlite\""
  },
  {
    "path": "server/prisma/schema.prisma",
    "content": "generator client {\n  provider = \"prisma-client-js\"\n}\n\n// Uncomment the following lines and comment out the SQLite datasource block above to use PostgreSQL\n// Make sure to set the correct DATABASE_URL in your .env file\n// After swapping run `yarn prisma:setup` from the root directory to migrate the database\n//\n// datasource db {\n//   provider = \"postgresql\"\n//   url      = env(\"DATABASE_URL\")\n// }\ndatasource db {\n  provider = \"sqlite\"\n  url      = \"file:../storage/anythingllm.db\"\n}\n\nmodel api_keys {\n  id            Int      @id @default(autoincrement())\n  secret        String?  @unique\n  createdBy     Int?\n  createdAt     DateTime @default(now())\n  lastUpdatedAt DateTime @default(now())\n}\n\nmodel workspace_documents {\n  id                   Int                   @id @default(autoincrement())\n  docId                String                @unique\n  filename             String\n  docpath              String\n  workspaceId          Int\n  metadata             String?\n  pinned               Boolean?              @default(false)\n  watched              Boolean?              @default(false)\n  createdAt            DateTime              @default(now())\n  lastUpdatedAt        DateTime              @default(now())\n  workspace            workspaces            @relation(fields: [workspaceId], references: [id])\n  document_sync_queues document_sync_queues?\n}\n\nmodel invites {\n  id            Int      @id @default(autoincrement())\n  code          String   @unique\n  status        String   @default(\"pending\")\n  claimedBy     Int?\n  workspaceIds  String?\n  createdAt     DateTime @default(now())\n  createdBy     Int\n  lastUpdatedAt DateTime @default(now())\n}\n\nmodel system_settings {\n  id            Int      @id @default(autoincrement())\n  label         String   @unique\n  value         String?\n  createdAt     DateTime @default(now())\n  lastUpdatedAt DateTime @default(now())\n}\n\nmodel users {\n  id                           Int                           @id @default(autoincrement())\n  username                     String?                       @unique\n  password                     String\n  pfpFilename                  String?\n  role                         String                        @default(\"default\")\n  suspended                    Int                           @default(0)\n  seen_recovery_codes          Boolean?                      @default(false)\n  createdAt                    DateTime                      @default(now())\n  lastUpdatedAt                DateTime                      @default(now())\n  dailyMessageLimit            Int?\n  bio                          String?                       @default(\"\")\n  web_push_subscription_config String?\n  workspace_chats              workspace_chats[]\n  workspace_users              workspace_users[]\n  embed_configs                embed_configs[]\n  embed_chats                  embed_chats[]\n  threads                      workspace_threads[]\n  recovery_codes               recovery_codes[]\n  password_reset_tokens        password_reset_tokens[]\n  workspace_agent_invocations  workspace_agent_invocations[]\n  slash_command_presets        slash_command_presets[]\n  browser_extension_api_keys   browser_extension_api_keys[]\n  temporary_auth_tokens        temporary_auth_tokens[]\n  system_prompt_variables      system_prompt_variables[]\n  prompt_history               prompt_history[]\n  desktop_mobile_devices       desktop_mobile_devices[]\n  workspace_parsed_files       workspace_parsed_files[]\n}\n\nmodel recovery_codes {\n  id        Int      @id @default(autoincrement())\n  user_id   Int\n  code_hash String\n  createdAt DateTime @default(now())\n  user      users    @relation(fields: [user_id], references: [id], onDelete: Cascade)\n\n  @@index([user_id])\n}\n\nmodel password_reset_tokens {\n  id        Int      @id @default(autoincrement())\n  user_id   Int\n  token     String   @unique\n  expiresAt DateTime\n  createdAt DateTime @default(now())\n  user      users    @relation(fields: [user_id], references: [id], onDelete: Cascade)\n\n  @@index([user_id])\n}\n\nmodel document_vectors {\n  id            Int      @id @default(autoincrement())\n  docId         String\n  vectorId      String\n  createdAt     DateTime @default(now())\n  lastUpdatedAt DateTime @default(now())\n}\n\nmodel workspaces {\n  id                           Int                            @id @default(autoincrement())\n  name                         String\n  slug                         String                         @unique\n  vectorTag                    String?\n  createdAt                    DateTime                       @default(now())\n  openAiTemp                   Float?\n  openAiHistory                Int                            @default(20)\n  lastUpdatedAt                DateTime                       @default(now())\n  // THIS IS THE SYSTEM PROMPT FOR THE WORKSPACE\n  openAiPrompt                 String?\n  similarityThreshold          Float?                         @default(0.25)\n  chatProvider                 String?\n  chatModel                    String?\n  topN                         Int?                           @default(4)\n  chatMode                     String?                        @default(\"chat\")\n  pfpFilename                  String?\n  agentProvider                String?\n  agentModel                   String?\n  queryRefusalResponse         String?\n  vectorSearchMode             String?                        @default(\"default\")\n  workspace_users              workspace_users[]\n  documents                    workspace_documents[]\n  workspace_suggested_messages workspace_suggested_messages[]\n  embed_configs                embed_configs[]\n  threads                      workspace_threads[]\n  workspace_agent_invocations  workspace_agent_invocations[]\n  prompt_history               prompt_history[]\n  workspace_parsed_files       workspace_parsed_files[]\n}\n\nmodel workspace_threads {\n  id                     Int                      @id @default(autoincrement())\n  name                   String\n  slug                   String                   @unique\n  workspace_id           Int\n  user_id                Int?\n  createdAt              DateTime                 @default(now())\n  lastUpdatedAt          DateTime                 @default(now())\n  workspace              workspaces               @relation(fields: [workspace_id], references: [id], onDelete: Cascade)\n  user                   users?                   @relation(fields: [user_id], references: [id], onDelete: Cascade)\n  workspace_parsed_files workspace_parsed_files[]\n\n  @@index([workspace_id])\n  @@index([user_id])\n}\n\nmodel workspace_suggested_messages {\n  id            Int        @id @default(autoincrement())\n  workspaceId   Int\n  heading       String\n  message       String\n  createdAt     DateTime   @default(now())\n  lastUpdatedAt DateTime   @default(now())\n  workspace     workspaces @relation(fields: [workspaceId], references: [id], onDelete: Cascade)\n\n  @@index([workspaceId])\n}\n\nmodel workspace_chats {\n  id             Int      @id @default(autoincrement())\n  workspaceId    Int\n  prompt         String\n  response       String\n  include        Boolean  @default(true)\n  user_id        Int?\n  thread_id      Int? // No relation to prevent whole table migration\n  api_session_id String? // String identifier for only the dev API to partition chats in any mode.\n  createdAt      DateTime @default(now())\n  lastUpdatedAt  DateTime @default(now())\n  feedbackScore  Boolean?\n  users          users?   @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade)\n}\n\nmodel workspace_agent_invocations {\n  id            Int        @id @default(autoincrement())\n  uuid          String     @unique\n  prompt        String // Contains agent invocation to parse + option additional text for seed.\n  closed        Boolean    @default(false)\n  user_id       Int?\n  thread_id     Int? // No relation to prevent whole table migration\n  workspace_id  Int\n  createdAt     DateTime   @default(now())\n  lastUpdatedAt DateTime   @default(now())\n  user          users?     @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade)\n  workspace     workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade, onUpdate: Cascade)\n\n  @@index([uuid])\n}\n\nmodel workspace_users {\n  id            Int        @id @default(autoincrement())\n  user_id       Int\n  workspace_id  Int\n  createdAt     DateTime   @default(now())\n  lastUpdatedAt DateTime   @default(now())\n  workspaces    workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade, onUpdate: Cascade)\n  users         users      @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade)\n}\n\nmodel cache_data {\n  id            Int       @id @default(autoincrement())\n  name          String\n  data          String\n  belongsTo     String?\n  byId          Int?\n  expiresAt     DateTime?\n  createdAt     DateTime  @default(now())\n  lastUpdatedAt DateTime  @default(now())\n}\n\nmodel embed_configs {\n  id                         Int           @id @default(autoincrement())\n  uuid                       String        @unique\n  enabled                    Boolean       @default(false)\n  chat_mode                  String        @default(\"query\")\n  allowlist_domains          String?\n  allow_model_override       Boolean       @default(false)\n  allow_temperature_override Boolean       @default(false)\n  allow_prompt_override      Boolean       @default(false)\n  max_chats_per_day          Int?\n  max_chats_per_session      Int?\n  message_limit              Int?          @default(20)\n  workspace_id               Int\n  createdBy                  Int?\n  usersId                    Int?\n  createdAt                  DateTime      @default(now())\n  workspace                  workspaces    @relation(fields: [workspace_id], references: [id], onDelete: Cascade)\n  embed_chats                embed_chats[]\n  users                      users?        @relation(fields: [usersId], references: [id])\n}\n\nmodel embed_chats {\n  id                     Int           @id @default(autoincrement())\n  prompt                 String\n  response               String\n  session_id             String\n  include                Boolean       @default(true)\n  connection_information String?\n  embed_id               Int\n  usersId                Int?\n  createdAt              DateTime      @default(now())\n  embed_config           embed_configs @relation(fields: [embed_id], references: [id], onDelete: Cascade)\n  users                  users?        @relation(fields: [usersId], references: [id])\n}\n\nmodel event_logs {\n  id         Int      @id @default(autoincrement())\n  event      String\n  metadata   String?\n  userId     Int?\n  occurredAt DateTime @default(now())\n\n  @@index([event])\n}\n\nmodel slash_command_presets {\n  id            Int      @id @default(autoincrement())\n  command       String\n  prompt        String\n  description   String\n  uid           Int      @default(0) // 0 is null user\n  userId        Int?\n  createdAt     DateTime @default(now())\n  lastUpdatedAt DateTime @default(now())\n  user          users?   @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n  @@unique([uid, command])\n}\n\nmodel document_sync_queues {\n  id             Int                        @id @default(autoincrement())\n  staleAfterMs   Int                        @default(604800000) // 7 days\n  nextSyncAt     DateTime\n  createdAt      DateTime                   @default(now())\n  lastSyncedAt   DateTime                   @default(now())\n  workspaceDocId Int                        @unique\n  workspaceDoc   workspace_documents?       @relation(fields: [workspaceDocId], references: [id], onDelete: Cascade)\n  runs           document_sync_executions[]\n}\n\nmodel document_sync_executions {\n  id        Int                  @id @default(autoincrement())\n  queueId   Int\n  status    String               @default(\"unknown\")\n  result    String?\n  createdAt DateTime             @default(now())\n  queue     document_sync_queues @relation(fields: [queueId], references: [id], onDelete: Cascade)\n}\n\nmodel browser_extension_api_keys {\n  id            Int      @id @default(autoincrement())\n  key           String   @unique\n  user_id       Int?\n  createdAt     DateTime @default(now())\n  lastUpdatedAt DateTime @updatedAt\n  user          users?   @relation(fields: [user_id], references: [id], onDelete: Cascade)\n\n  @@index([user_id])\n}\n\nmodel temporary_auth_tokens {\n  id        Int      @id @default(autoincrement())\n  token     String   @unique\n  userId    Int\n  expiresAt DateTime\n  createdAt DateTime @default(now())\n  user      users    @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n  @@index([token])\n  @@index([userId])\n}\n\nmodel system_prompt_variables {\n  id          Int      @id @default(autoincrement())\n  key         String   @unique\n  value       String?\n  description String?\n  type        String   @default(\"system\") // system, user, dynamic\n  userId      Int?\n  createdAt   DateTime @default(now())\n  updatedAt   DateTime @updatedAt\n  user        users?   @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n  @@index([userId])\n}\n\nmodel prompt_history {\n  id          Int        @id @default(autoincrement())\n  workspace   workspaces @relation(fields: [workspaceId], references: [id], onDelete: Cascade)\n  workspaceId Int\n  prompt      String\n  modifiedBy  Int?\n  modifiedAt  DateTime   @default(now())\n  user        users?     @relation(fields: [modifiedBy], references: [id])\n\n  @@index([workspaceId])\n}\n\n// Schema specific to mobile app <> Desktop app connection\nmodel desktop_mobile_devices {\n  id         Int      @id @default(autoincrement())\n  deviceOs   String\n  deviceName String\n  token      String   @unique\n  approved   Boolean  @default(false)\n  userId     Int?\n  createdAt  DateTime @default(now())\n  user       users?   @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n  @@index([userId])\n}\n\nmodel workspace_parsed_files {\n  id                 Int                @id @default(autoincrement())\n  filename           String             @unique\n  workspaceId        Int\n  userId             Int?\n  threadId           Int?\n  metadata           String?\n  tokenCountEstimate Int?               @default(0)\n  createdAt          DateTime           @default(now())\n  workspace          workspaces         @relation(fields: [workspaceId], references: [id], onDelete: Cascade)\n  user               users?             @relation(fields: [userId], references: [id], onDelete: Cascade)\n  thread             workspace_threads? @relation(fields: [threadId], references: [id], onDelete: Cascade)\n\n  @@index([workspaceId])\n  @@index([userId])\n}\n"
  },
  {
    "path": "server/prisma/seed.js",
    "content": "const { PrismaClient } = require(\"@prisma/client\");\nconst prisma = new PrismaClient();\n\nasync function main() {\n  const settings = [\n    { label: \"multi_user_mode\", value: \"false\" },\n    { label: \"logo_filename\", value: \"anything-llm.png\" },\n  ];\n\n  for (let setting of settings) {\n    const existing = await prisma.system_settings.findUnique({\n      where: { label: setting.label },\n    });\n\n    // Only create the setting if it doesn't already exist\n    if (!existing) {\n      await prisma.system_settings.create({\n        data: setting,\n      });\n    }\n  }\n}\n\nmain()\n  .catch((e) => {\n    console.error(e);\n    process.exit(1);\n  })\n  .finally(async () => {\n    await prisma.$disconnect();\n  });\n"
  },
  {
    "path": "server/storage/README.md",
    "content": "# AnythingLLM Storage\n\nThis folder is for the local or disk storage of ready-to-embed documents, vector-cached embeddings, and the disk-storage of LanceDB and the local SQLite database.\n\nThis folder should contain the following folders.\n`documents`\n`lancedb` (if using lancedb)\n`vector-cache`\nand a file named exactly `anythingllm.db`\n\n\n### Common issues\n**SQLITE_FILE_CANNOT_BE_OPENED** in the server log = The DB file does not exist probably because the node instance does not have the correct permissions to write a file to the disk. To solve this..\n\n- Local dev\n  - Create a `anythingllm.db` empty file in this directory. Thats all. No need to reboot the server or anything. If your permissions are correct this should not ever occur since the server will create the file if it does not exist automatically.\n\n- Docker Instance\n  - Get your AnythingLLM docker container id with `docker ps -a`. The container must be running to execute the next commands.\n  - Run `docker container exec -u 0 -t <ANYTHINGLLM DOCKER CONTAINER ID> mkdir -p /app/server/storage /app/server/storage/documents /app/server/storage/vector-cache /app/server/storage/lancedb`\n  - Run `docker container exec -u 0 -t <ANYTHINGLLM DOCKER CONTAINER ID> touch /app/server/storage/anythingllm.db`\n  - Run `docker container exec -u 0 -t <ANYTHINGLLM DOCKER CONTAINER ID> chown -R anythingllm:anythingllm /app/collector /app/server`\n\n  - The above commands will create the appropriate folders inside of the docker container and will persist as long as you do not destroy the container and volume. This will also fix any ownership issues of folder files in the collector and the server."
  },
  {
    "path": "server/storage/models/.gitignore",
    "content": "Xenova\ndownloaded/*\n!downloaded/.placeholder\nopenrouter\napipie\nnovita\nmixedbread-ai*\ngemini\ntogetherAi\ntesseract\nppio\ncontext-windows/*\nMintplexLabs\ncometapi\nfireworks\ngiteeai\ndocker-model-runner"
  },
  {
    "path": "server/storage/models/README.md",
    "content": "# Native models used by AnythingLLM\n\nThis folder is specifically created as a local cache and storage folder that is used for native models that can run on a CPU.\n\nCurrently, AnythingLLM uses this folder for the following parts of the application.\n\n## Embedding\nWhen your embedding engine preference is `native` we will use the ONNX **all-MiniLM-L6-v2** model built by [Xenova on HuggingFace.co](https://huggingface.co/Xenova/all-MiniLM-L6-v2). This model is a quantized and WASM version of the popular [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) which produces a 384-dimension vector.\n\nIf you are using the `native` embedding engine your vector database should be configured to accept 384-dimension models if that parameter is directly editable (Pinecone only).\n\n## Audio/Video transcription\nAnythingLLM allows you to upload various audio and video formats as source documents. In all cases the audio tracks will be transcribed by a locally running ONNX model **whisper-small** built by [Xenova on HuggingFace.co](https://huggingface.co/Xenova/whisper-small). The model is a smaller version of the OpenAI Whisper model. Given the model runs locally on CPU, larger files will result in longer transcription times.\n\nOnce transcribed you can embed these transcriptions into your workspace like you would any other file! \n\n**Other external model/transcription providers are also live.**\n- [OpenAI Whisper via API key.](https://openai.com/research/whisper)\n\n## Text generation (LLM selection)\n> [!IMPORTANT]\n> Use of a locally running LLM model is **experimental** and may behave unexpectedly, crash, or not function at all.\n> We suggest for production-use of a local LLM model to use a purpose-built inference server like [LocalAI](https://localai.io) or [LMStudio](https://lmstudio.ai).\n\n> [!TIP]\n> We recommend at _least_ using a 4-bit or 5-bit quantized model for your LLM. Lower quantization models tend to\n> just output unreadable garbage.\n\nIf you would like to use a local Llama compatible LLM model for chatting you can select any model from this [HuggingFace search filter](https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&other=text-generation-inference&sort=trending)\n\n**Requirements**\n- Model must be in the latest `GGUF` format\n- Model should be compatible with latest `llama.cpp`\n- You should have the proper RAM to run such a model. Requirement depends on model size.\n\n### Where do I put my GGUF model?\n> [!IMPORTANT]\n> If running in Docker you should be running the container to a mounted storage location on the host machine so you\n> can update the storage files directly without having to re-download or re-build your docker container. [See suggested Docker config](../../../README.md#recommended-usage-with-docker-easy)\n\n> [!NOTE]\n> `/server/storage/models/downloaded` is the default location that your model files should be at. \n> Your storage directory may differ if you changed the STORAGE_DIR environment variable.\n\nAll local models you want to have available for LLM selection should be placed in the `server/storage/models/downloaded` folder. Only `.gguf` files will be allowed to be selected from the UI."
  },
  {
    "path": "server/storage/models/downloaded/.placeholder",
    "content": "All your .GGUF model file downloads you want to use for chatting should go into this folder."
  },
  {
    "path": "server/swagger/dark-swagger.css",
    "content": "@media only screen and (prefers-color-scheme: dark) {\n\n  a {\n    color: #8c8cfa;\n  }\n\n  ::-webkit-scrollbar-track-piece {\n    background-color: rgba(255, 255, 255, .2) !important;\n  }\n\n  ::-webkit-scrollbar-track {\n    background-color: rgba(255, 255, 255, .3) !important;\n  }\n\n  ::-webkit-scrollbar-thumb {\n    background-color: rgba(255, 255, 255, .5) !important;\n  }\n\n  embed[type=\"application/pdf\"] {\n    filter: invert(90%);\n  }\n\n  html {\n    background: #1f1f1f !important;\n    box-sizing: border-box;\n    filter: contrast(100%) brightness(100%) saturate(100%);\n    overflow-y: scroll;\n  }\n\n  body {\n    background: #1f1f1f;\n    background-color: #1f1f1f;\n    background-image: none !important;\n  }\n\n  button,\n  input,\n  select,\n  textarea {\n    background-color: #1f1f1f;\n    color: #bfbfbf;\n  }\n\n  font,\n  html {\n    color: #bfbfbf;\n  }\n\n  .swagger-ui,\n  .swagger-ui section h3 {\n    color: #b5bac9;\n  }\n\n  .swagger-ui a {\n    background-color: transparent;\n  }\n\n  .swagger-ui mark {\n    background-color: #664b00;\n    color: #bfbfbf;\n  }\n\n  .swagger-ui legend {\n    color: inherit;\n  }\n\n  .swagger-ui .debug * {\n    outline: #e6da99 solid 1px;\n  }\n\n  .swagger-ui .debug-white * {\n    outline: #fff solid 1px;\n  }\n\n  .swagger-ui .debug-black * {\n    outline: #bfbfbf solid 1px;\n  }\n\n  .swagger-ui .debug-grid {\n    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTRDOTY4N0U2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTRDOTY4N0Q2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3NjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3NzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsBS+GMAAAAjSURBVHjaYvz//z8DLsD4gcGXiYEAGBIKGBne//fFpwAgwAB98AaF2pjlUQAAAABJRU5ErkJggg==) 0 0;\n  }\n\n  .swagger-ui .debug-grid-16 {\n    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODYyRjhERDU2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODYyRjhERDQ2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QTY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3QjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvCS01IAAABMSURBVHjaYmR4/5+BFPBfAMFm/MBgx8RAGWCn1AAmSg34Q6kBDKMGMDCwICeMIemF/5QawEipAWwUhwEjMDvbAWlWkvVBwu8vQIABAEwBCph8U6c0AAAAAElFTkSuQmCC) 0 0;\n  }\n\n  .swagger-ui .debug-grid-8-solid {\n    background: url(data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAAAAD/4QMxaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzExMSA3OS4xNTgzMjUsIDIwMTUvMDkvMTAtMDE6MTA6MjAgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE1IChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkIxMjI0OTczNjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkIxMjI0OTc0NjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QjEyMjQ5NzE2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QjEyMjQ5NzI2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAbGhopHSlBJiZBQi8vL0JHPz4+P0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHAR0pKTQmND8oKD9HPzU/R0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0f/wAARCAAIAAgDASIAAhEBAxEB/8QAWQABAQAAAAAAAAAAAAAAAAAAAAYBAQEAAAAAAAAAAAAAAAAAAAIEEAEBAAMBAAAAAAAAAAAAAAABADECA0ERAAEDBQAAAAAAAAAAAAAAAAARITFBUWESIv/aAAwDAQACEQMRAD8AoOnTV1QTD7JJshP3vSM3P//Z) 0 0 #1c1c21;\n  }\n\n  .swagger-ui .debug-grid-16-solid {\n    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzY3MkJEN0U2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzY3MkJEN0Y2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3RDY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pve6J3kAAAAzSURBVHjaYvz//z8D0UDsMwMjSRoYP5Gq4SPNbRjVMEQ1fCRDg+in/6+J1AJUxsgAEGAA31BAJMS0GYEAAAAASUVORK5CYII=) 0 0 #1c1c21;\n  }\n\n  .swagger-ui .b--black {\n    border-color: #000;\n  }\n\n  .swagger-ui .b--near-black {\n    border-color: #121212;\n  }\n\n  .swagger-ui .b--dark-gray {\n    border-color: #333;\n  }\n\n  .swagger-ui .b--mid-gray {\n    border-color: #545454;\n  }\n\n  .swagger-ui .b--gray {\n    border-color: #787878;\n  }\n\n  .swagger-ui .b--silver {\n    border-color: #999;\n  }\n\n  .swagger-ui .b--light-silver {\n    border-color: #6e6e6e;\n  }\n\n  .swagger-ui .b--moon-gray {\n    border-color: #4d4d4d;\n  }\n\n  .swagger-ui .b--light-gray {\n    border-color: #2b2b2b;\n  }\n\n  .swagger-ui .b--near-white {\n    border-color: #242424;\n  }\n\n  .swagger-ui .b--white {\n    border-color: #1c1c21;\n  }\n\n  .swagger-ui .b--white-90 {\n    border-color: rgba(28, 28, 33, .9);\n  }\n\n  .swagger-ui .b--white-80 {\n    border-color: rgba(28, 28, 33, .8);\n  }\n\n  .swagger-ui .b--white-70 {\n    border-color: rgba(28, 28, 33, .7);\n  }\n\n  .swagger-ui .b--white-60 {\n    border-color: rgba(28, 28, 33, .6);\n  }\n\n  .swagger-ui .b--white-50 {\n    border-color: rgba(28, 28, 33, .5);\n  }\n\n  .swagger-ui .b--white-40 {\n    border-color: rgba(28, 28, 33, .4);\n  }\n\n  .swagger-ui .b--white-30 {\n    border-color: rgba(28, 28, 33, .3);\n  }\n\n  .swagger-ui .b--white-20 {\n    border-color: rgba(28, 28, 33, .2);\n  }\n\n  .swagger-ui .b--white-10 {\n    border-color: rgba(28, 28, 33, .1);\n  }\n\n  .swagger-ui .b--white-05 {\n    border-color: rgba(28, 28, 33, .05);\n  }\n\n  .swagger-ui .b--white-025 {\n    border-color: rgba(28, 28, 33, .024);\n  }\n\n  .swagger-ui .b--white-0125 {\n    border-color: rgba(28, 28, 33, .01);\n  }\n\n  .swagger-ui .b--black-90 {\n    border-color: rgba(0, 0, 0, .9);\n  }\n\n  .swagger-ui .b--black-80 {\n    border-color: rgba(0, 0, 0, .8);\n  }\n\n  .swagger-ui .b--black-70 {\n    border-color: rgba(0, 0, 0, .7);\n  }\n\n  .swagger-ui .b--black-60 {\n    border-color: rgba(0, 0, 0, .6);\n  }\n\n  .swagger-ui .b--black-50 {\n    border-color: rgba(0, 0, 0, .5);\n  }\n\n  .swagger-ui .b--black-40 {\n    border-color: rgba(0, 0, 0, .4);\n  }\n\n  .swagger-ui .b--black-30 {\n    border-color: rgba(0, 0, 0, .3);\n  }\n\n  .swagger-ui .b--black-20 {\n    border-color: rgba(0, 0, 0, .2);\n  }\n\n  .swagger-ui .b--black-10 {\n    border-color: rgba(0, 0, 0, .1);\n  }\n\n  .swagger-ui .b--black-05 {\n    border-color: rgba(0, 0, 0, .05);\n  }\n\n  .swagger-ui .b--black-025 {\n    border-color: rgba(0, 0, 0, .024);\n  }\n\n  .swagger-ui .b--black-0125 {\n    border-color: rgba(0, 0, 0, .01);\n  }\n\n  .swagger-ui .b--dark-red {\n    border-color: #bc2f36;\n  }\n\n  .swagger-ui .b--red {\n    border-color: #c83932;\n  }\n\n  .swagger-ui .b--light-red {\n    border-color: #ab3c2b;\n  }\n\n  .swagger-ui .b--orange {\n    border-color: #cc6e33;\n  }\n\n  .swagger-ui .b--purple {\n    border-color: #5e2ca5;\n  }\n\n  .swagger-ui .b--light-purple {\n    border-color: #672caf;\n  }\n\n  .swagger-ui .b--dark-pink {\n    border-color: #ab2b81;\n  }\n\n  .swagger-ui .b--hot-pink {\n    border-color: #c03086;\n  }\n\n  .swagger-ui .b--pink {\n    border-color: #8f2464;\n  }\n\n  .swagger-ui .b--light-pink {\n    border-color: #721d4d;\n  }\n\n  .swagger-ui .b--dark-green {\n    border-color: #1c6e50;\n  }\n\n  .swagger-ui .b--green {\n    border-color: #279b70;\n  }\n\n  .swagger-ui .b--light-green {\n    border-color: #228762;\n  }\n\n  .swagger-ui .b--navy {\n    border-color: #0d1d35;\n  }\n\n  .swagger-ui .b--dark-blue {\n    border-color: #20497e;\n  }\n\n  .swagger-ui .b--blue {\n    border-color: #4380d0;\n  }\n\n  .swagger-ui .b--light-blue {\n    border-color: #20517e;\n  }\n\n  .swagger-ui .b--lightest-blue {\n    border-color: #143a52;\n  }\n\n  .swagger-ui .b--washed-blue {\n    border-color: #0c312d;\n  }\n\n  .swagger-ui .b--washed-green {\n    border-color: #0f3d2c;\n  }\n\n  .swagger-ui .b--washed-red {\n    border-color: #411010;\n  }\n\n  .swagger-ui .b--transparent {\n    border-color: transparent;\n  }\n\n  .swagger-ui .b--gold,\n  .swagger-ui .b--light-yellow,\n  .swagger-ui .b--washed-yellow,\n  .swagger-ui .b--yellow {\n    border-color: #664b00;\n  }\n\n  .swagger-ui .shadow-1 {\n    box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px;\n  }\n\n  .swagger-ui .shadow-2 {\n    box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px;\n  }\n\n  .swagger-ui .shadow-3 {\n    box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px;\n  }\n\n  .swagger-ui .shadow-4 {\n    box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0;\n  }\n\n  .swagger-ui .shadow-5 {\n    box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0;\n  }\n\n  @media screen and (min-width: 30em) {\n    .swagger-ui .shadow-1-ns {\n      box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px;\n    }\n\n    .swagger-ui .shadow-2-ns {\n      box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px;\n    }\n\n    .swagger-ui .shadow-3-ns {\n      box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px;\n    }\n\n    .swagger-ui .shadow-4-ns {\n      box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0;\n    }\n\n    .swagger-ui .shadow-5-ns {\n      box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0;\n    }\n  }\n\n  @media screen and (max-width: 60em) and (min-width: 30em) {\n    .swagger-ui .shadow-1-m {\n      box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px;\n    }\n\n    .swagger-ui .shadow-2-m {\n      box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px;\n    }\n\n    .swagger-ui .shadow-3-m {\n      box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px;\n    }\n\n    .swagger-ui .shadow-4-m {\n      box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0;\n    }\n\n    .swagger-ui .shadow-5-m {\n      box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0;\n    }\n  }\n\n  @media screen and (min-width: 60em) {\n    .swagger-ui .shadow-1-l {\n      box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px;\n    }\n\n    .swagger-ui .shadow-2-l {\n      box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px;\n    }\n\n    .swagger-ui .shadow-3-l {\n      box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px;\n    }\n\n    .swagger-ui .shadow-4-l {\n      box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0;\n    }\n\n    .swagger-ui .shadow-5-l {\n      box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0;\n    }\n  }\n\n  .swagger-ui .black-05 {\n    color: rgba(191, 191, 191, .05);\n  }\n\n  .swagger-ui .bg-black-05 {\n    background-color: rgba(0, 0, 0, .05);\n  }\n\n  .swagger-ui .black-90,\n  .swagger-ui .hover-black-90:focus,\n  .swagger-ui .hover-black-90:hover {\n    color: rgba(191, 191, 191, .9);\n  }\n\n  .swagger-ui .black-80,\n  .swagger-ui .hover-black-80:focus,\n  .swagger-ui .hover-black-80:hover {\n    color: rgba(191, 191, 191, .8);\n  }\n\n  .swagger-ui .black-70,\n  .swagger-ui .hover-black-70:focus,\n  .swagger-ui .hover-black-70:hover {\n    color: rgba(191, 191, 191, .7);\n  }\n\n  .swagger-ui .black-60,\n  .swagger-ui .hover-black-60:focus,\n  .swagger-ui .hover-black-60:hover {\n    color: rgba(191, 191, 191, .6);\n  }\n\n  .swagger-ui .black-50,\n  .swagger-ui .hover-black-50:focus,\n  .swagger-ui .hover-black-50:hover {\n    color: rgba(191, 191, 191, .5);\n  }\n\n  .swagger-ui .black-40,\n  .swagger-ui .hover-black-40:focus,\n  .swagger-ui .hover-black-40:hover {\n    color: rgba(191, 191, 191, .4);\n  }\n\n  .swagger-ui .black-30,\n  .swagger-ui .hover-black-30:focus,\n  .swagger-ui .hover-black-30:hover {\n    color: rgba(191, 191, 191, .3);\n  }\n\n  .swagger-ui .black-20,\n  .swagger-ui .hover-black-20:focus,\n  .swagger-ui .hover-black-20:hover {\n    color: rgba(191, 191, 191, .2);\n  }\n\n  .swagger-ui .black-10,\n  .swagger-ui .hover-black-10:focus,\n  .swagger-ui .hover-black-10:hover {\n    color: rgba(191, 191, 191, .1);\n  }\n\n  .swagger-ui .hover-white-90:focus,\n  .swagger-ui .hover-white-90:hover,\n  .swagger-ui .white-90 {\n    color: rgba(255, 255, 255, .9);\n  }\n\n  .swagger-ui .hover-white-80:focus,\n  .swagger-ui .hover-white-80:hover,\n  .swagger-ui .white-80 {\n    color: rgba(255, 255, 255, .8);\n  }\n\n  .swagger-ui .hover-white-70:focus,\n  .swagger-ui .hover-white-70:hover,\n  .swagger-ui .white-70 {\n    color: rgba(255, 255, 255, .7);\n  }\n\n  .swagger-ui .hover-white-60:focus,\n  .swagger-ui .hover-white-60:hover,\n  .swagger-ui .white-60 {\n    color: rgba(255, 255, 255, .6);\n  }\n\n  .swagger-ui .hover-white-50:focus,\n  .swagger-ui .hover-white-50:hover,\n  .swagger-ui .white-50 {\n    color: rgba(255, 255, 255, .5);\n  }\n\n  .swagger-ui .hover-white-40:focus,\n  .swagger-ui .hover-white-40:hover,\n  .swagger-ui .white-40 {\n    color: rgba(255, 255, 255, .4);\n  }\n\n  .swagger-ui .hover-white-30:focus,\n  .swagger-ui .hover-white-30:hover,\n  .swagger-ui .white-30 {\n    color: rgba(255, 255, 255, .3);\n  }\n\n  .swagger-ui .hover-white-20:focus,\n  .swagger-ui .hover-white-20:hover,\n  .swagger-ui .white-20 {\n    color: rgba(255, 255, 255, .2);\n  }\n\n  .swagger-ui .hover-white-10:focus,\n  .swagger-ui .hover-white-10:hover,\n  .swagger-ui .white-10 {\n    color: rgba(255, 255, 255, .1);\n  }\n\n  .swagger-ui .hover-moon-gray:focus,\n  .swagger-ui .hover-moon-gray:hover,\n  .swagger-ui .moon-gray {\n    color: #ccc;\n  }\n\n  .swagger-ui .hover-light-gray:focus,\n  .swagger-ui .hover-light-gray:hover,\n  .swagger-ui .light-gray {\n    color: #ededed;\n  }\n\n  .swagger-ui .hover-near-white:focus,\n  .swagger-ui .hover-near-white:hover,\n  .swagger-ui .near-white {\n    color: #f5f5f5;\n  }\n\n  .swagger-ui .dark-red,\n  .swagger-ui .hover-dark-red:focus,\n  .swagger-ui .hover-dark-red:hover {\n    color: #e6999d;\n  }\n\n  .swagger-ui .hover-red:focus,\n  .swagger-ui .hover-red:hover,\n  .swagger-ui .red {\n    color: #e69d99;\n  }\n\n  .swagger-ui .hover-light-red:focus,\n  .swagger-ui .hover-light-red:hover,\n  .swagger-ui .light-red {\n    color: #e6a399;\n  }\n\n  .swagger-ui .hover-orange:focus,\n  .swagger-ui .hover-orange:hover,\n  .swagger-ui .orange {\n    color: #e6b699;\n  }\n\n  .swagger-ui .gold,\n  .swagger-ui .hover-gold:focus,\n  .swagger-ui .hover-gold:hover {\n    color: #e6d099;\n  }\n\n  .swagger-ui .hover-yellow:focus,\n  .swagger-ui .hover-yellow:hover,\n  .swagger-ui .yellow {\n    color: #e6da99;\n  }\n\n  .swagger-ui .hover-light-yellow:focus,\n  .swagger-ui .hover-light-yellow:hover,\n  .swagger-ui .light-yellow {\n    color: #ede6b6;\n  }\n\n  .swagger-ui .hover-purple:focus,\n  .swagger-ui .hover-purple:hover,\n  .swagger-ui .purple {\n    color: #b99ae4;\n  }\n\n  .swagger-ui .hover-light-purple:focus,\n  .swagger-ui .hover-light-purple:hover,\n  .swagger-ui .light-purple {\n    color: #bb99e6;\n  }\n\n  .swagger-ui .dark-pink,\n  .swagger-ui .hover-dark-pink:focus,\n  .swagger-ui .hover-dark-pink:hover {\n    color: #e699cc;\n  }\n\n  .swagger-ui .hot-pink,\n  .swagger-ui .hover-hot-pink:focus,\n  .swagger-ui .hover-hot-pink:hover,\n  .swagger-ui .hover-pink:focus,\n  .swagger-ui .hover-pink:hover,\n  .swagger-ui .pink {\n    color: #e699c7;\n  }\n\n  .swagger-ui .hover-light-pink:focus,\n  .swagger-ui .hover-light-pink:hover,\n  .swagger-ui .light-pink {\n    color: #edb6d5;\n  }\n\n  .swagger-ui .dark-green,\n  .swagger-ui .green,\n  .swagger-ui .hover-dark-green:focus,\n  .swagger-ui .hover-dark-green:hover,\n  .swagger-ui .hover-green:focus,\n  .swagger-ui .hover-green:hover {\n    color: #99e6c9;\n  }\n\n  .swagger-ui .hover-light-green:focus,\n  .swagger-ui .hover-light-green:hover,\n  .swagger-ui .light-green {\n    color: #a1e8ce;\n  }\n\n  .swagger-ui .hover-navy:focus,\n  .swagger-ui .hover-navy:hover,\n  .swagger-ui .navy {\n    color: #99b8e6;\n  }\n\n  .swagger-ui .blue,\n  .swagger-ui .dark-blue,\n  .swagger-ui .hover-blue:focus,\n  .swagger-ui .hover-blue:hover,\n  .swagger-ui .hover-dark-blue:focus,\n  .swagger-ui .hover-dark-blue:hover {\n    color: #99bae6;\n  }\n\n  .swagger-ui .hover-light-blue:focus,\n  .swagger-ui .hover-light-blue:hover,\n  .swagger-ui .light-blue {\n    color: #a9cbea;\n  }\n\n  .swagger-ui .hover-lightest-blue:focus,\n  .swagger-ui .hover-lightest-blue:hover,\n  .swagger-ui .lightest-blue {\n    color: #d6e9f5;\n  }\n\n  .swagger-ui .hover-washed-blue:focus,\n  .swagger-ui .hover-washed-blue:hover,\n  .swagger-ui .washed-blue {\n    color: #f7fdfc;\n  }\n\n  .swagger-ui .hover-washed-green:focus,\n  .swagger-ui .hover-washed-green:hover,\n  .swagger-ui .washed-green {\n    color: #ebfaf4;\n  }\n\n  .swagger-ui .hover-washed-yellow:focus,\n  .swagger-ui .hover-washed-yellow:hover,\n  .swagger-ui .washed-yellow {\n    color: #fbf9ef;\n  }\n\n  .swagger-ui .hover-washed-red:focus,\n  .swagger-ui .hover-washed-red:hover,\n  .swagger-ui .washed-red {\n    color: #f9e7e7;\n  }\n\n  .swagger-ui .color-inherit,\n  .swagger-ui .hover-inherit:focus,\n  .swagger-ui .hover-inherit:hover {\n    color: inherit;\n  }\n\n  .swagger-ui .bg-black-90,\n  .swagger-ui .hover-bg-black-90:focus,\n  .swagger-ui .hover-bg-black-90:hover {\n    background-color: rgba(0, 0, 0, .9);\n  }\n\n  .swagger-ui .bg-black-80,\n  .swagger-ui .hover-bg-black-80:focus,\n  .swagger-ui .hover-bg-black-80:hover {\n    background-color: rgba(0, 0, 0, .8);\n  }\n\n  .swagger-ui .bg-black-70,\n  .swagger-ui .hover-bg-black-70:focus,\n  .swagger-ui .hover-bg-black-70:hover {\n    background-color: rgba(0, 0, 0, .7);\n  }\n\n  .swagger-ui .bg-black-60,\n  .swagger-ui .hover-bg-black-60:focus,\n  .swagger-ui .hover-bg-black-60:hover {\n    background-color: rgba(0, 0, 0, .6);\n  }\n\n  .swagger-ui .bg-black-50,\n  .swagger-ui .hover-bg-black-50:focus,\n  .swagger-ui .hover-bg-black-50:hover {\n    background-color: rgba(0, 0, 0, .5);\n  }\n\n  .swagger-ui .bg-black-40,\n  .swagger-ui .hover-bg-black-40:focus,\n  .swagger-ui .hover-bg-black-40:hover {\n    background-color: rgba(0, 0, 0, .4);\n  }\n\n  .swagger-ui .bg-black-30,\n  .swagger-ui .hover-bg-black-30:focus,\n  .swagger-ui .hover-bg-black-30:hover {\n    background-color: rgba(0, 0, 0, .3);\n  }\n\n  .swagger-ui .bg-black-20,\n  .swagger-ui .hover-bg-black-20:focus,\n  .swagger-ui .hover-bg-black-20:hover {\n    background-color: rgba(0, 0, 0, .2);\n  }\n\n  .swagger-ui .bg-white-90,\n  .swagger-ui .hover-bg-white-90:focus,\n  .swagger-ui .hover-bg-white-90:hover {\n    background-color: rgba(28, 28, 33, .9);\n  }\n\n  .swagger-ui .bg-white-80,\n  .swagger-ui .hover-bg-white-80:focus,\n  .swagger-ui .hover-bg-white-80:hover {\n    background-color: rgba(28, 28, 33, .8);\n  }\n\n  .swagger-ui .bg-white-70,\n  .swagger-ui .hover-bg-white-70:focus,\n  .swagger-ui .hover-bg-white-70:hover {\n    background-color: rgba(28, 28, 33, .7);\n  }\n\n  .swagger-ui .bg-white-60,\n  .swagger-ui .hover-bg-white-60:focus,\n  .swagger-ui .hover-bg-white-60:hover {\n    background-color: rgba(28, 28, 33, .6);\n  }\n\n  .swagger-ui .bg-white-50,\n  .swagger-ui .hover-bg-white-50:focus,\n  .swagger-ui .hover-bg-white-50:hover {\n    background-color: rgba(28, 28, 33, .5);\n  }\n\n  .swagger-ui .bg-white-40,\n  .swagger-ui .hover-bg-white-40:focus,\n  .swagger-ui .hover-bg-white-40:hover {\n    background-color: rgba(28, 28, 33, .4);\n  }\n\n  .swagger-ui .bg-white-30,\n  .swagger-ui .hover-bg-white-30:focus,\n  .swagger-ui .hover-bg-white-30:hover {\n    background-color: rgba(28, 28, 33, .3);\n  }\n\n  .swagger-ui .bg-white-20,\n  .swagger-ui .hover-bg-white-20:focus,\n  .swagger-ui .hover-bg-white-20:hover {\n    background-color: rgba(28, 28, 33, .2);\n  }\n\n  .swagger-ui .bg-black,\n  .swagger-ui .hover-bg-black:focus,\n  .swagger-ui .hover-bg-black:hover {\n    background-color: #000;\n  }\n\n  .swagger-ui .bg-near-black,\n  .swagger-ui .hover-bg-near-black:focus,\n  .swagger-ui .hover-bg-near-black:hover {\n    background-color: #121212;\n  }\n\n  .swagger-ui .bg-dark-gray,\n  .swagger-ui .hover-bg-dark-gray:focus,\n  .swagger-ui .hover-bg-dark-gray:hover {\n    background-color: #333;\n  }\n\n  .swagger-ui .bg-mid-gray,\n  .swagger-ui .hover-bg-mid-gray:focus,\n  .swagger-ui .hover-bg-mid-gray:hover {\n    background-color: #545454;\n  }\n\n  .swagger-ui .bg-gray,\n  .swagger-ui .hover-bg-gray:focus,\n  .swagger-ui .hover-bg-gray:hover {\n    background-color: #787878;\n  }\n\n  .swagger-ui .bg-silver,\n  .swagger-ui .hover-bg-silver:focus,\n  .swagger-ui .hover-bg-silver:hover {\n    background-color: #999;\n  }\n\n  .swagger-ui .bg-white,\n  .swagger-ui .hover-bg-white:focus,\n  .swagger-ui .hover-bg-white:hover {\n    background-color: #1c1c21;\n  }\n\n  .swagger-ui .bg-transparent,\n  .swagger-ui .hover-bg-transparent:focus,\n  .swagger-ui .hover-bg-transparent:hover {\n    background-color: transparent;\n  }\n\n  .swagger-ui .bg-dark-red,\n  .swagger-ui .hover-bg-dark-red:focus,\n  .swagger-ui .hover-bg-dark-red:hover {\n    background-color: #bc2f36;\n  }\n\n  .swagger-ui .bg-red,\n  .swagger-ui .hover-bg-red:focus,\n  .swagger-ui .hover-bg-red:hover {\n    background-color: #c83932;\n  }\n\n  .swagger-ui .bg-light-red,\n  .swagger-ui .hover-bg-light-red:focus,\n  .swagger-ui .hover-bg-light-red:hover {\n    background-color: #ab3c2b;\n  }\n\n  .swagger-ui .bg-orange,\n  .swagger-ui .hover-bg-orange:focus,\n  .swagger-ui .hover-bg-orange:hover {\n    background-color: #cc6e33;\n  }\n\n  .swagger-ui .bg-gold,\n  .swagger-ui .bg-light-yellow,\n  .swagger-ui .bg-washed-yellow,\n  .swagger-ui .bg-yellow,\n  .swagger-ui .hover-bg-gold:focus,\n  .swagger-ui .hover-bg-gold:hover,\n  .swagger-ui .hover-bg-light-yellow:focus,\n  .swagger-ui .hover-bg-light-yellow:hover,\n  .swagger-ui .hover-bg-washed-yellow:focus,\n  .swagger-ui .hover-bg-washed-yellow:hover,\n  .swagger-ui .hover-bg-yellow:focus,\n  .swagger-ui .hover-bg-yellow:hover {\n    background-color: #664b00;\n  }\n\n  .swagger-ui .bg-purple,\n  .swagger-ui .hover-bg-purple:focus,\n  .swagger-ui .hover-bg-purple:hover {\n    background-color: #5e2ca5;\n  }\n\n  .swagger-ui .bg-light-purple,\n  .swagger-ui .hover-bg-light-purple:focus,\n  .swagger-ui .hover-bg-light-purple:hover {\n    background-color: #672caf;\n  }\n\n  .swagger-ui .bg-dark-pink,\n  .swagger-ui .hover-bg-dark-pink:focus,\n  .swagger-ui .hover-bg-dark-pink:hover {\n    background-color: #ab2b81;\n  }\n\n  .swagger-ui .bg-hot-pink,\n  .swagger-ui .hover-bg-hot-pink:focus,\n  .swagger-ui .hover-bg-hot-pink:hover {\n    background-color: #c03086;\n  }\n\n  .swagger-ui .bg-pink,\n  .swagger-ui .hover-bg-pink:focus,\n  .swagger-ui .hover-bg-pink:hover {\n    background-color: #8f2464;\n  }\n\n  .swagger-ui .bg-light-pink,\n  .swagger-ui .hover-bg-light-pink:focus,\n  .swagger-ui .hover-bg-light-pink:hover {\n    background-color: #721d4d;\n  }\n\n  .swagger-ui .bg-dark-green,\n  .swagger-ui .hover-bg-dark-green:focus,\n  .swagger-ui .hover-bg-dark-green:hover {\n    background-color: #1c6e50;\n  }\n\n  .swagger-ui .bg-green,\n  .swagger-ui .hover-bg-green:focus,\n  .swagger-ui .hover-bg-green:hover {\n    background-color: #279b70;\n  }\n\n  .swagger-ui .bg-light-green,\n  .swagger-ui .hover-bg-light-green:focus,\n  .swagger-ui .hover-bg-light-green:hover {\n    background-color: #228762;\n  }\n\n  .swagger-ui .bg-navy,\n  .swagger-ui .hover-bg-navy:focus,\n  .swagger-ui .hover-bg-navy:hover {\n    background-color: #0d1d35;\n  }\n\n  .swagger-ui .bg-dark-blue,\n  .swagger-ui .hover-bg-dark-blue:focus,\n  .swagger-ui .hover-bg-dark-blue:hover {\n    background-color: #20497e;\n  }\n\n  .swagger-ui .bg-blue,\n  .swagger-ui .hover-bg-blue:focus,\n  .swagger-ui .hover-bg-blue:hover {\n    background-color: #4380d0;\n  }\n\n  .swagger-ui .bg-light-blue,\n  .swagger-ui .hover-bg-light-blue:focus,\n  .swagger-ui .hover-bg-light-blue:hover {\n    background-color: #20517e;\n  }\n\n  .swagger-ui .bg-lightest-blue,\n  .swagger-ui .hover-bg-lightest-blue:focus,\n  .swagger-ui .hover-bg-lightest-blue:hover {\n    background-color: #143a52;\n  }\n\n  .swagger-ui .bg-washed-blue,\n  .swagger-ui .hover-bg-washed-blue:focus,\n  .swagger-ui .hover-bg-washed-blue:hover {\n    background-color: #0c312d;\n  }\n\n  .swagger-ui .bg-washed-green,\n  .swagger-ui .hover-bg-washed-green:focus,\n  .swagger-ui .hover-bg-washed-green:hover {\n    background-color: #0f3d2c;\n  }\n\n  .swagger-ui .bg-washed-red,\n  .swagger-ui .hover-bg-washed-red:focus,\n  .swagger-ui .hover-bg-washed-red:hover {\n    background-color: #411010;\n  }\n\n  .swagger-ui .bg-inherit,\n  .swagger-ui .hover-bg-inherit:focus,\n  .swagger-ui .hover-bg-inherit:hover {\n    background-color: inherit;\n  }\n\n  .swagger-ui .shadow-hover {\n    transition: all .5s cubic-bezier(.165, .84, .44, 1) 0s;\n  }\n\n  .swagger-ui .shadow-hover::after {\n    border-radius: inherit;\n    box-shadow: rgba(0, 0, 0, .2) 0 0 16px 2px;\n    content: \"\";\n    height: 100%;\n    left: 0;\n    opacity: 0;\n    position: absolute;\n    top: 0;\n    transition: opacity .5s cubic-bezier(.165, .84, .44, 1) 0s;\n    width: 100%;\n    z-index: -1;\n  }\n\n  .swagger-ui .bg-animate,\n  .swagger-ui .bg-animate:focus,\n  .swagger-ui .bg-animate:hover {\n    transition: background-color .15s ease-in-out 0s;\n  }\n\n  .swagger-ui .nested-links a {\n    color: #99bae6;\n    transition: color .15s ease-in 0s;\n  }\n\n  .swagger-ui .nested-links a:focus,\n  .swagger-ui .nested-links a:hover {\n    color: #a9cbea;\n    transition: color .15s ease-in 0s;\n  }\n\n  .swagger-ui .opblock-tag {\n    border-bottom: 1px solid rgba(58, 64, 80, .3);\n    color: #b5bac9;\n    transition: all .2s ease 0s;\n  }\n\n  .swagger-ui .opblock-tag svg,\n  .swagger-ui section.models h4 svg {\n    transition: all .4s ease 0s;\n  }\n\n  .swagger-ui .opblock {\n    border: 1px solid #000;\n    border-radius: 4px;\n    box-shadow: rgba(0, 0, 0, .19) 0 0 3px;\n    margin: 0 0 15px;\n  }\n\n  .swagger-ui .opblock .tab-header .tab-item.active h4 span::after {\n    background: gray;\n  }\n\n  .swagger-ui .opblock.is-open .opblock-summary {\n    border-bottom: 1px solid #000;\n  }\n\n  .swagger-ui .opblock .opblock-section-header {\n    background: rgba(28, 28, 33, .8);\n    box-shadow: rgba(0, 0, 0, .1) 0 1px 2px;\n  }\n\n  .swagger-ui .opblock .opblock-section-header>label>span {\n    padding: 0 10px 0 0;\n  }\n\n  .swagger-ui .opblock .opblock-summary-method {\n    background: #000;\n    color: #fff;\n    text-shadow: rgba(0, 0, 0, .1) 0 1px 0;\n  }\n\n  .swagger-ui .opblock.opblock-post {\n    background: rgba(72, 203, 144, .1);\n    border-color: #48cb90;\n  }\n\n  .swagger-ui .opblock.opblock-post .opblock-summary-method,\n  .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after {\n    background: #48cb90;\n  }\n\n  .swagger-ui .opblock.opblock-post .opblock-summary {\n    border-color: #48cb90;\n  }\n\n  .swagger-ui .opblock.opblock-put {\n    background: rgba(213, 157, 88, .1);\n    border-color: #d59d58;\n  }\n\n  .swagger-ui .opblock.opblock-put .opblock-summary-method,\n  .swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span::after {\n    background: #d59d58;\n  }\n\n  .swagger-ui .opblock.opblock-put .opblock-summary {\n    border-color: #d59d58;\n  }\n\n  .swagger-ui .opblock.opblock-delete {\n    background: rgba(200, 50, 50, .1);\n    border-color: #c83232;\n  }\n\n  .swagger-ui .opblock.opblock-delete .opblock-summary-method,\n  .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after {\n    background: #c83232;\n  }\n\n  .swagger-ui .opblock.opblock-delete .opblock-summary {\n    border-color: #c83232;\n  }\n\n  .swagger-ui .opblock.opblock-get {\n    background: rgba(42, 105, 167, .1);\n    border-color: #2a69a7;\n  }\n\n  .swagger-ui .opblock.opblock-get .opblock-summary-method,\n  .swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after {\n    background: #2a69a7;\n  }\n\n  .swagger-ui .opblock.opblock-get .opblock-summary {\n    border-color: #2a69a7;\n  }\n\n  .swagger-ui .opblock.opblock-patch {\n    background: rgba(92, 214, 188, .1);\n    border-color: #5cd6bc;\n  }\n\n  .swagger-ui .opblock.opblock-patch .opblock-summary-method,\n  .swagger-ui .opblock.opblock-patch .tab-header .tab-item.active h4 span::after {\n    background: #5cd6bc;\n  }\n\n  .swagger-ui .opblock.opblock-patch .opblock-summary {\n    border-color: #5cd6bc;\n  }\n\n  .swagger-ui .opblock.opblock-head {\n    background: rgba(140, 63, 207, .1);\n    border-color: #8c3fcf;\n  }\n\n  .swagger-ui .opblock.opblock-head .opblock-summary-method,\n  .swagger-ui .opblock.opblock-head .tab-header .tab-item.active h4 span::after {\n    background: #8c3fcf;\n  }\n\n  .swagger-ui .opblock.opblock-head .opblock-summary {\n    border-color: #8c3fcf;\n  }\n\n  .swagger-ui .opblock.opblock-options {\n    background: rgba(36, 89, 143, .1);\n    border-color: #24598f;\n  }\n\n  .swagger-ui .opblock.opblock-options .opblock-summary-method,\n  .swagger-ui .opblock.opblock-options .tab-header .tab-item.active h4 span::after {\n    background: #24598f;\n  }\n\n  .swagger-ui .opblock.opblock-options .opblock-summary {\n    border-color: #24598f;\n  }\n\n  .swagger-ui .opblock.opblock-deprecated {\n    background: rgba(46, 46, 46, .1);\n    border-color: #2e2e2e;\n    opacity: .6;\n  }\n\n  .swagger-ui .opblock.opblock-deprecated .opblock-summary-method,\n  .swagger-ui .opblock.opblock-deprecated .tab-header .tab-item.active h4 span::after {\n    background: #2e2e2e;\n  }\n\n  .swagger-ui .opblock.opblock-deprecated .opblock-summary {\n    border-color: #2e2e2e;\n  }\n\n  .swagger-ui .filter .operation-filter-input {\n    border: 2px solid #2b3446;\n  }\n\n  .swagger-ui .tab li:first-of-type::after {\n    background: rgba(0, 0, 0, .2);\n  }\n\n  .swagger-ui .download-contents {\n    background: #7c8192;\n    color: #fff;\n  }\n\n  .swagger-ui .scheme-container {\n    background: #1c1c21;\n    box-shadow: rgba(0, 0, 0, .15) 0 1px 2px 0;\n  }\n\n  .swagger-ui .loading-container .loading::before {\n    animation: 1s linear 0s infinite normal none running rotation, .5s ease 0s 1 normal none running opacity;\n    border-color: rgba(0, 0, 0, .6) rgba(84, 84, 84, .1) rgba(84, 84, 84, .1);\n  }\n\n  .swagger-ui .response-control-media-type--accept-controller select {\n    border-color: #196619;\n  }\n\n  .swagger-ui .response-control-media-type__accept-message {\n    color: #99e699;\n  }\n\n  .swagger-ui .version-pragma__message code {\n    background-color: #3b3b3b;\n  }\n\n  .swagger-ui .btn {\n    background: 0 0;\n    border: 2px solid gray;\n    box-shadow: rgba(0, 0, 0, .1) 0 1px 2px;\n    color: #b5bac9;\n  }\n\n  .swagger-ui .btn:hover {\n    box-shadow: rgba(0, 0, 0, .3) 0 0 5px;\n  }\n\n  .swagger-ui .btn.authorize,\n  .swagger-ui .btn.cancel {\n    background-color: transparent;\n    border-color: #a72a2a;\n    color: #e69999;\n  }\n\n  .swagger-ui .btn.cancel:hover {\n    background-color: #a72a2a;\n    color: #fff;\n  }\n\n  .swagger-ui .btn.authorize {\n    border-color: #48cb90;\n    color: #9ce3c3;\n  }\n\n  .swagger-ui .btn.authorize svg {\n    fill: #9ce3c3;\n  }\n\n  .btn.authorize.unlocked:hover {\n    background-color: #48cb90;\n    color: #fff;\n  }\n\n  .btn.authorize.unlocked:hover svg {\n    fill: #fbfbfb;\n  }\n\n  .swagger-ui .btn.execute {\n    background-color: #5892d5;\n    border-color: #5892d5;\n    color: #fff;\n  }\n\n  .swagger-ui .copy-to-clipboard {\n    background: #7c8192;\n  }\n\n  .swagger-ui .copy-to-clipboard button {\n    background: url(\"data:image/svg+xml;charset=utf-8,<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"16\\\" height=\\\"16\\\" aria-hidden=\\\"true\\\"><path fill=\\\"%23fff\\\" fill-rule=\\\"evenodd\\\" d=\\\"M2 13h4v1H2v-1zm5-6H2v1h5V7zm2 3V8l-3 3 3 3v-2h5v-2H9zM4.5 9H2v1h2.5V9zM2 12h2.5v-1H2v1zm9 1h1v2c-.02.28-.11.52-.3.7-.19.18-.42.28-.7.3H1c-.55 0-1-.45-1-1V4c0-.55.45-1 1-1h3c0-1.11.89-2 2-2 1.11 0 2 .89 2 2h3c.55 0 1 .45 1 1v5h-1V6H1v9h10v-2zM2 5h8c0-.55-.45-1-1-1H8c-.55 0-1-.45-1-1s-.45-1-1-1-1 .45-1 1-.45 1-1 1H3c-.55 0-1 .45-1 1z\\\"/></svg>\") 50% center no-repeat;\n  }\n\n  .swagger-ui select {\n    background: url(\"data:image/svg+xml;charset=utf-8,<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 20 20\\\"><path d=\\\"M13.418 7.859a.695.695 0 01.978 0 .68.68 0 010 .969l-3.908 3.83a.697.697 0 01-.979 0l-3.908-3.83a.68.68 0 010-.969.695.695 0 01.978 0L10 11l3.418-3.141z\\\"/></svg>\") right 10px center/20px no-repeat #212121;\n    background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4wICg0MDM1YTRmYjQ5LCAyMDIwLTA1LTAxKSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZG93bmxvYWQuc3ZnIgogICBpZD0ic3ZnNCIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMjAgMjAiPgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTEwIj4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZGVmcwogICAgIGlkPSJkZWZzOCIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnNCIKICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIgogICAgIGlua3NjYXBlOndpbmRvdy15PSItOSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iLTkiCiAgICAgaW5rc2NhcGU6Y3k9IjEwIgogICAgIGlua3NjYXBlOmN4PSIxMCIKICAgICBpbmtzY2FwZTp6b29tPSI0MS41IgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpZD0ibmFtZWR2aWV3NiIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDAxIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICAgIGd1aWRldG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBvYmplY3R0b2xlcmFuY2U9IjEwIgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAvPgogIDxwYXRoCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIKICAgICBpZD0icGF0aDIiCiAgICAgZD0iTTEzLjQxOCA3Ljg1OWEuNjk1LjY5NSAwIDAxLjk3OCAwIC42OC42OCAwIDAxMCAuOTY5bC0zLjkwOCAzLjgzYS42OTcuNjk3IDAgMDEtLjk3OSAwbC0zLjkwOC0zLjgzYS42OC42OCAwIDAxMC0uOTY5LjY5NS42OTUgMCAwMS45NzggMEwxMCAxMWwzLjQxOC0zLjE0MXoiIC8+Cjwvc3ZnPgo=) right 10px center/20px no-repeat #1c1c21;\n    border: 2px solid #41444e;\n  }\n\n  .swagger-ui select[multiple] {\n    background: #212121;\n  }\n\n  .swagger-ui button.invalid,\n  .swagger-ui input[type=email].invalid,\n  .swagger-ui input[type=file].invalid,\n  .swagger-ui input[type=password].invalid,\n  .swagger-ui input[type=search].invalid,\n  .swagger-ui input[type=text].invalid,\n  .swagger-ui select.invalid,\n  .swagger-ui textarea.invalid {\n    background: #390e0e;\n    border-color: #c83232;\n  }\n\n  .swagger-ui input[type=email],\n  .swagger-ui input[type=file],\n  .swagger-ui input[type=password],\n  .swagger-ui input[type=search],\n  .swagger-ui input[type=text],\n  .swagger-ui textarea {\n    background: #1c1c21;\n    border: 1px solid #404040;\n  }\n\n  .swagger-ui textarea {\n    background: rgba(28, 28, 33, .8);\n    color: #b5bac9;\n  }\n\n  .swagger-ui input[disabled],\n  .swagger-ui select[disabled] {\n    background-color: #1f1f1f;\n    color: #bfbfbf;\n  }\n\n  .swagger-ui textarea[disabled] {\n    background-color: #41444e;\n    color: #fff;\n  }\n\n  .swagger-ui select[disabled] {\n    border-color: #878787;\n  }\n\n  .swagger-ui textarea:focus {\n    border: 2px solid #2a69a7;\n  }\n\n  .swagger-ui .checkbox input[type=checkbox]+label>.item {\n    background: #303030;\n    box-shadow: #303030 0 0 0 2px;\n  }\n\n  .swagger-ui .checkbox input[type=checkbox]:checked+label>.item {\n    background: url(\"data:image/svg+xml;charset=utf-8,<svg width=\\\"10\\\" height=\\\"8\\\" viewBox=\\\"3 7 10 8\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\"><path fill=\\\"%2341474E\\\" fill-rule=\\\"evenodd\\\" d=\\\"M6.333 15L3 11.667l1.333-1.334 2 2L11.667 7 13 8.333z\\\"/></svg>\") 50% center no-repeat #303030;\n  }\n\n  .swagger-ui .dialog-ux .backdrop-ux {\n    background: rgba(0, 0, 0, .8);\n  }\n\n  .swagger-ui .dialog-ux .modal-ux {\n    background: #1c1c21;\n    border: 1px solid #2e2e2e;\n    box-shadow: rgba(0, 0, 0, .2) 0 10px 30px 0;\n  }\n\n  .swagger-ui .dialog-ux .modal-ux-header .close-modal {\n    background: 0 0;\n  }\n\n  .swagger-ui .model .deprecated span,\n  .swagger-ui .model .deprecated td {\n    color: #bfbfbf !important;\n  }\n\n  .swagger-ui .model-toggle::after {\n    background: url(\"data:image/svg+xml;charset=utf-8,<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"24\\\" height=\\\"24\\\"><path d=\\\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\\\"/></svg>\") 50% center/100% no-repeat;\n  }\n\n  .swagger-ui .model-hint {\n    background: rgba(0, 0, 0, .7);\n    color: #ebebeb;\n  }\n\n  .swagger-ui section.models {\n    border: 1px solid rgba(58, 64, 80, .3);\n  }\n\n  .swagger-ui section.models.is-open h4 {\n    border-bottom: 1px solid rgba(58, 64, 80, .3);\n  }\n\n  .swagger-ui section.models .model-container {\n    background: rgba(0, 0, 0, .05);\n  }\n\n  .swagger-ui section.models .model-container:hover {\n    background: rgba(0, 0, 0, .07);\n  }\n\n  .swagger-ui .model-box {\n    background: rgba(0, 0, 0, .1);\n  }\n\n  .swagger-ui .prop-type {\n    color: #aaaad4;\n  }\n\n  .swagger-ui table thead tr td,\n  .swagger-ui table thead tr th {\n    border-bottom: 1px solid rgba(58, 64, 80, .2);\n    color: #b5bac9;\n  }\n\n  .swagger-ui .parameter__name.required::after {\n    color: rgba(230, 153, 153, .6);\n  }\n\n  .swagger-ui .topbar .download-url-wrapper .select-label {\n    color: #f0f0f0;\n  }\n\n  .swagger-ui .topbar .download-url-wrapper .download-url-button {\n    background: #63a040;\n    color: #fff;\n  }\n\n  .swagger-ui .info .title small {\n    background: #7c8492;\n  }\n\n  .swagger-ui .info .title small.version-stamp {\n    background-color: #7a9b27;\n  }\n\n  .swagger-ui .auth-container .errors {\n    background-color: #350d0d;\n    color: #b5bac9;\n  }\n\n  .swagger-ui .errors-wrapper {\n    background: rgba(200, 50, 50, .1);\n    border: 2px solid #c83232;\n  }\n\n  .swagger-ui .markdown code,\n  .swagger-ui .renderedmarkdown code {\n    background: rgba(0, 0, 0, .05);\n    color: #c299e6;\n  }\n\n  .swagger-ui .model-toggle:after {\n    background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4wICg0MDM1YTRmYjQ5LCAyMDIwLTA1LTAxKSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZG93bmxvYWQyLnN2ZyIKICAgaWQ9InN2ZzQiCiAgIHZlcnNpb249IjEuMSIKICAgaGVpZ2h0PSIyNCIKICAgd2lkdGg9IjI0Ij4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGExMCI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGRlZnMKICAgICBpZD0iZGVmczgiIC8+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9InN2ZzQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTkiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9Ii05IgogICAgIGlua3NjYXBlOmN5PSIxMiIKICAgICBpbmtzY2FwZTpjeD0iMTIiCiAgICAgaW5rc2NhcGU6em9vbT0iMzQuNTgzMzMzIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpZD0ibmFtZWR2aWV3NiIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDAxIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICAgIGd1aWRldG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBvYmplY3R0b2xlcmFuY2U9IjEwIgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAvPgogIDxwYXRoCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIKICAgICBpZD0icGF0aDIiCiAgICAgZD0iTTEwIDZMOC41OSA3LjQxIDEzLjE3IDEybC00LjU4IDQuNTlMMTAgMThsNi02eiIgLz4KPC9zdmc+Cg==) 50% no-repeat;\n  }\n\n  /* arrows for each operation and request are now white */\n  .arrow,\n  #large-arrow-up {\n    fill: #fff;\n  }\n\n  #unlocked {\n    fill: #fff;\n  }\n\n  ::-webkit-scrollbar-track {\n    background-color: #646464 !important;\n  }\n\n  ::-webkit-scrollbar-thumb {\n    background-color: #242424 !important;\n    border: 2px solid #3e4346 !important;\n  }\n\n  ::-webkit-scrollbar-button:vertical:start:decrement {\n    background: linear-gradient(130deg, #696969 40%, rgba(255, 0, 0, 0) 41%), linear-gradient(230deg, #696969 40%, transparent 41%), linear-gradient(0deg, #696969 40%, transparent 31%);\n    background-color: #b6b6b6;\n  }\n\n  ::-webkit-scrollbar-button:vertical:end:increment {\n    background: linear-gradient(310deg, #696969 40%, transparent 41%), linear-gradient(50deg, #696969 40%, transparent 41%), linear-gradient(180deg, #696969 40%, transparent 31%);\n    background-color: #b6b6b6;\n  }\n\n  ::-webkit-scrollbar-button:horizontal:end:increment {\n    background: linear-gradient(210deg, #696969 40%, transparent 41%), linear-gradient(330deg, #696969 40%, transparent 41%), linear-gradient(90deg, #696969 30%, transparent 31%);\n    background-color: #b6b6b6;\n  }\n\n  ::-webkit-scrollbar-button:horizontal:start:decrement {\n    background: linear-gradient(30deg, #696969 40%, transparent 41%), linear-gradient(150deg, #696969 40%, transparent 41%), linear-gradient(270deg, #696969 30%, transparent 31%);\n    background-color: #b6b6b6;\n  }\n\n  ::-webkit-scrollbar-button,\n  ::-webkit-scrollbar-track-piece {\n    background-color: #3e4346 !important;\n  }\n\n  .swagger-ui .black,\n  .swagger-ui .checkbox,\n  .swagger-ui .dark-gray,\n  .swagger-ui .download-url-wrapper .loading,\n  .swagger-ui .errors-wrapper .errors small,\n  .swagger-ui .fallback,\n  .swagger-ui .filter .loading,\n  .swagger-ui .gray,\n  .swagger-ui .hover-black:focus,\n  .swagger-ui .hover-black:hover,\n  .swagger-ui .hover-dark-gray:focus,\n  .swagger-ui .hover-dark-gray:hover,\n  .swagger-ui .hover-gray:focus,\n  .swagger-ui .hover-gray:hover,\n  .swagger-ui .hover-light-silver:focus,\n  .swagger-ui .hover-light-silver:hover,\n  .swagger-ui .hover-mid-gray:focus,\n  .swagger-ui .hover-mid-gray:hover,\n  .swagger-ui .hover-near-black:focus,\n  .swagger-ui .hover-near-black:hover,\n  .swagger-ui .hover-silver:focus,\n  .swagger-ui .hover-silver:hover,\n  .swagger-ui .light-silver,\n  .swagger-ui .markdown pre,\n  .swagger-ui .mid-gray,\n  .swagger-ui .model .property,\n  .swagger-ui .model .property.primitive,\n  .swagger-ui .model-title,\n  .swagger-ui .near-black,\n  .swagger-ui .parameter__extension,\n  .swagger-ui .parameter__in,\n  .swagger-ui .prop-format,\n  .swagger-ui .renderedmarkdown pre,\n  .swagger-ui .response-col_links .response-undocumented,\n  .swagger-ui .response-col_status .response-undocumented,\n  .swagger-ui .silver,\n  .swagger-ui section.models h4,\n  .swagger-ui section.models h5,\n  .swagger-ui span.token-not-formatted,\n  .swagger-ui span.token-string,\n  .swagger-ui table.headers .header-example,\n  .swagger-ui table.model tr.description,\n  .swagger-ui table.model tr.extension {\n    color: #bfbfbf;\n  }\n\n  .swagger-ui .hover-white:focus,\n  .swagger-ui .hover-white:hover,\n  .swagger-ui .info .title small pre,\n  .swagger-ui .topbar a,\n  .swagger-ui .white {\n    color: #fff;\n  }\n\n  .swagger-ui .bg-black-10,\n  .swagger-ui .hover-bg-black-10:focus,\n  .swagger-ui .hover-bg-black-10:hover,\n  .swagger-ui .stripe-dark:nth-child(2n + 1) {\n    background-color: rgba(0, 0, 0, .1);\n  }\n\n  .swagger-ui .bg-white-10,\n  .swagger-ui .hover-bg-white-10:focus,\n  .swagger-ui .hover-bg-white-10:hover,\n  .swagger-ui .stripe-light:nth-child(2n + 1) {\n    background-color: rgba(28, 28, 33, .1);\n  }\n\n  .swagger-ui .bg-light-silver,\n  .swagger-ui .hover-bg-light-silver:focus,\n  .swagger-ui .hover-bg-light-silver:hover,\n  .swagger-ui .striped--light-silver:nth-child(2n + 1) {\n    background-color: #6e6e6e;\n  }\n\n  .swagger-ui .bg-moon-gray,\n  .swagger-ui .hover-bg-moon-gray:focus,\n  .swagger-ui .hover-bg-moon-gray:hover,\n  .swagger-ui .striped--moon-gray:nth-child(2n + 1) {\n    background-color: #4d4d4d;\n  }\n\n  .swagger-ui .bg-light-gray,\n  .swagger-ui .hover-bg-light-gray:focus,\n  .swagger-ui .hover-bg-light-gray:hover,\n  .swagger-ui .striped--light-gray:nth-child(2n + 1) {\n    background-color: #2b2b2b;\n  }\n\n  .swagger-ui .bg-near-white,\n  .swagger-ui .hover-bg-near-white:focus,\n  .swagger-ui .hover-bg-near-white:hover,\n  .swagger-ui .striped--near-white:nth-child(2n + 1) {\n    background-color: #242424;\n  }\n\n  .swagger-ui .opblock-tag:hover,\n  .swagger-ui section.models h4:hover {\n    background: rgba(0, 0, 0, .02);\n  }\n\n  .swagger-ui .checkbox p,\n  .swagger-ui .dialog-ux .modal-ux-content h4,\n  .swagger-ui .dialog-ux .modal-ux-content p,\n  .swagger-ui .dialog-ux .modal-ux-header h3,\n  .swagger-ui .errors-wrapper .errors h4,\n  .swagger-ui .errors-wrapper hgroup h4,\n  .swagger-ui .info .base-url,\n  .swagger-ui .info .title,\n  .swagger-ui .info h1,\n  .swagger-ui .info h2,\n  .swagger-ui .info h3,\n  .swagger-ui .info h4,\n  .swagger-ui .info h5,\n  .swagger-ui .info li,\n  .swagger-ui .info p,\n  .swagger-ui .info table,\n  .swagger-ui .loading-container .loading::after,\n  .swagger-ui .model,\n  .swagger-ui .opblock .opblock-section-header h4,\n  .swagger-ui .opblock .opblock-section-header>label,\n  .swagger-ui .opblock .opblock-summary-description,\n  .swagger-ui .opblock .opblock-summary-operation-id,\n  .swagger-ui .opblock .opblock-summary-path,\n  .swagger-ui .opblock .opblock-summary-path__deprecated,\n  .swagger-ui .opblock-description-wrapper,\n  .swagger-ui .opblock-description-wrapper h4,\n  .swagger-ui .opblock-description-wrapper p,\n  .swagger-ui .opblock-external-docs-wrapper,\n  .swagger-ui .opblock-external-docs-wrapper h4,\n  .swagger-ui .opblock-external-docs-wrapper p,\n  .swagger-ui .opblock-tag small,\n  .swagger-ui .opblock-title_normal,\n  .swagger-ui .opblock-title_normal h4,\n  .swagger-ui .opblock-title_normal p,\n  .swagger-ui .parameter__name,\n  .swagger-ui .parameter__type,\n  .swagger-ui .response-col_links,\n  .swagger-ui .response-col_status,\n  .swagger-ui .responses-inner h4,\n  .swagger-ui .responses-inner h5,\n  .swagger-ui .scheme-container .schemes>label,\n  .swagger-ui .scopes h2,\n  .swagger-ui .servers>label,\n  .swagger-ui .tab li,\n  .swagger-ui label,\n  .swagger-ui select,\n  .swagger-ui table.headers td {\n    color: #b5bac9;\n  }\n\n  .swagger-ui .download-url-wrapper .failed,\n  .swagger-ui .filter .failed,\n  .swagger-ui .model-deprecated-warning,\n  .swagger-ui .parameter__deprecated,\n  .swagger-ui .parameter__name.required span,\n  .swagger-ui table.model tr.property-row .star {\n    color: #e69999;\n  }\n\n  .swagger-ui .opblock-body pre.microlight,\n  .swagger-ui textarea.curl {\n    background: #41444e;\n    border-radius: 4px;\n    color: #fff;\n  }\n\n  .swagger-ui .expand-methods svg,\n  .swagger-ui .expand-methods:hover svg {\n    fill: #bfbfbf;\n  }\n\n  .swagger-ui .auth-container,\n  .swagger-ui .dialog-ux .modal-ux-header {\n    border-bottom: 1px solid #2e2e2e;\n  }\n\n  .swagger-ui .topbar .download-url-wrapper .select-label select,\n  .swagger-ui .topbar .download-url-wrapper input[type=text] {\n    border: 2px solid #63a040;\n  }\n\n  .swagger-ui .info a,\n  .swagger-ui .info a:hover,\n  .swagger-ui .scopes h2 a {\n    color: #99bde6;\n  }\n\n  /* Dark Scrollbar */\n  ::-webkit-scrollbar {\n    width: 14px;\n    height: 14px;\n  }\n\n  ::-webkit-scrollbar-button {\n    background-color: #3e4346 !important;\n  }\n\n  ::-webkit-scrollbar-track {\n    background-color: #646464 !important;\n  }\n\n  ::-webkit-scrollbar-track-piece {\n    background-color: #3e4346 !important;\n  }\n\n  ::-webkit-scrollbar-thumb {\n    height: 50px;\n    background-color: #242424 !important;\n    border: 2px solid #3e4346 !important;\n  }\n\n  ::-webkit-scrollbar-corner {}\n\n  ::-webkit-resizer {}\n\n  ::-webkit-scrollbar-button:vertical:start:decrement {\n    background:\n      linear-gradient(130deg, #696969 40%, rgba(255, 0, 0, 0) 41%),\n      linear-gradient(230deg, #696969 40%, rgba(0, 0, 0, 0) 41%),\n      linear-gradient(0deg, #696969 40%, rgba(0, 0, 0, 0) 31%);\n    background-color: #b6b6b6;\n  }\n\n  ::-webkit-scrollbar-button:vertical:end:increment {\n    background:\n      linear-gradient(310deg, #696969 40%, rgba(0, 0, 0, 0) 41%),\n      linear-gradient(50deg, #696969 40%, rgba(0, 0, 0, 0) 41%),\n      linear-gradient(180deg, #696969 40%, rgba(0, 0, 0, 0) 31%);\n    background-color: #b6b6b6;\n  }\n\n  ::-webkit-scrollbar-button:horizontal:end:increment {\n    background:\n      linear-gradient(210deg, #696969 40%, rgba(0, 0, 0, 0) 41%),\n      linear-gradient(330deg, #696969 40%, rgba(0, 0, 0, 0) 41%),\n      linear-gradient(90deg, #696969 30%, rgba(0, 0, 0, 0) 31%);\n    background-color: #b6b6b6;\n  }\n\n  ::-webkit-scrollbar-button:horizontal:start:decrement {\n    background:\n      linear-gradient(30deg, #696969 40%, rgba(0, 0, 0, 0) 41%),\n      linear-gradient(150deg, #696969 40%, rgba(0, 0, 0, 0) 41%),\n      linear-gradient(270deg, #696969 30%, rgba(0, 0, 0, 0) 31%);\n    background-color: #b6b6b6;\n  }\n}"
  },
  {
    "path": "server/swagger/index.css",
    "content": ".schemes.wrapper>div:first-of-type {\n  display: none;\n}"
  },
  {
    "path": "server/swagger/index.js",
    "content": "function waitForElm(selector) {\n  return new Promise((resolve) => {\n    if (document.querySelector(selector)) {\n      return resolve(document.querySelector(selector));\n    }\n\n    const observer = new MutationObserver((_mutations) => {\n      if (document.querySelector(selector)) {\n        resolve(document.querySelector(selector));\n        observer.disconnect();\n      }\n    });\n\n    observer.observe(document.body, {\n      childList: true,\n      subtree: true,\n    });\n  });\n}\n\n// Force change the Swagger logo in the header\nwaitForElm(\".topbar-wrapper\").then((elm) => {\n  if (window.SWAGGER_DOCS_ENV === \"development\") {\n    elm.innerHTML = `<img href='${window.location.origin}' src='http://localhost:3000/public/anything-llm-light.png' width='200'/>`;\n  } else {\n    elm.innerHTML = `<img href='${window.location.origin}' src='${window.location.origin}/anything-llm-light.png' width='200'/>`;\n  }\n});\n"
  },
  {
    "path": "server/swagger/init.js",
    "content": "const swaggerAutogen = require(\"swagger-autogen\")({ openapi: \"3.0.0\" });\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\nconst doc = {\n  info: {\n    version: \"1.0.0\",\n    title: \"AnythingLLM Developer API\",\n    description:\n      \"API endpoints that enable programmatic reading, writing, and updating of your AnythingLLM instance. UI supplied by Swagger.io.\",\n  },\n  // Swagger-autogen does not allow us to use relative paths as these will resolve to\n  // http:///api in the openapi.json file, so we need to monkey-patch this post-generation.\n  host: \"/api\",\n  schemes: [\"http\"],\n  securityDefinitions: {\n    BearerAuth: {\n      type: \"http\",\n      scheme: \"bearer\",\n      bearerFormat: \"JWT\",\n    },\n  },\n  security: [{ BearerAuth: [] }],\n  definitions: {\n    InvalidAPIKey: {\n      message: \"Invalid API Key\",\n    },\n  },\n};\n\nconst outputFile = path.resolve(__dirname, \"./openapi.json\");\nconst endpointsFiles = [\n  \"../endpoints/api/auth/index.js\",\n  \"../endpoints/api/admin/index.js\",\n  \"../endpoints/api/document/index.js\",\n  \"../endpoints/api/workspace/index.js\",\n  \"../endpoints/api/system/index.js\",\n  \"../endpoints/api/workspaceThread/index.js\",\n  \"../endpoints/api/userManagement/index.js\",\n  \"../endpoints/api/openai/index.js\",\n  \"../endpoints/api/embed/index.js\",\n];\n\nswaggerAutogen(outputFile, endpointsFiles, doc).then(({ data }) => {\n  // Remove Authorization parameters from arguments.\n  for (const path of Object.keys(data.paths)) {\n    if (data.paths[path].hasOwnProperty(\"get\")) {\n      let parameters = data.paths[path].get?.parameters || [];\n      parameters = parameters.filter((arg) => arg.name !== \"Authorization\");\n      data.paths[path].get.parameters = parameters;\n    }\n\n    if (data.paths[path].hasOwnProperty(\"post\")) {\n      let parameters = data.paths[path].post?.parameters || [];\n      parameters = parameters.filter((arg) => arg.name !== \"Authorization\");\n      data.paths[path].post.parameters = parameters;\n    }\n\n    if (data.paths[path].hasOwnProperty(\"delete\")) {\n      let parameters = data.paths[path].delete?.parameters || [];\n      parameters = parameters.filter((arg) => arg.name !== \"Authorization\");\n      data.paths[path].delete.parameters = parameters;\n    }\n  }\n\n  const openApiSpec = {\n    ...data,\n    servers: [\n      {\n        url: \"/api\",\n      },\n    ],\n  };\n  fs.writeFileSync(outputFile, JSON.stringify(openApiSpec, null, 2), {\n    encoding: \"utf-8\",\n    flag: \"w\",\n  });\n  console.log(`Swagger-autogen:  \\x1b[32mPatched servers.url ✔\\x1b[0m`);\n});\n"
  },
  {
    "path": "server/swagger/openapi.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"version\": \"1.0.0\",\n    \"title\": \"AnythingLLM Developer API\",\n    \"description\": \"API endpoints that enable programmatic reading, writing, and updating of your AnythingLLM instance. UI supplied by Swagger.io.\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"/api\"\n    }\n  ],\n  \"paths\": {\n    \"/v1/auth\": {\n      \"get\": {\n        \"tags\": [\n          \"Authentication\"\n        ],\n        \"description\": \"Verify the attached Authentication header contains a valid API token.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Valid auth token was found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"authenticated\": true\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/admin/is-multi-user-mode\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"description\": \"Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"isMultiUser\": true\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/admin/users\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"description\": \"Check to see if the instance is in multi-user-mode first. Methods are disabled until multi user mode is enabled via the UI.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"users\": [\n                      {\n                        \"username\": \"sample-sam\",\n                        \"role\": \"default\"\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Instance is not in Multi-User mode. Method denied\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/admin/users/new\": {\n      \"post\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"description\": \"Create a new user with username and password. Methods are disabled until multi user mode is enabled via the UI.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"user\": {\n                      \"id\": 1,\n                      \"username\": \"sample-sam\",\n                      \"role\": \"default\"\n                    },\n                    \"error\": null\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"401\": {\n            \"description\": \"Instance is not in Multi-User mode. Method denied\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Key pair object that will define the new user to add to the system.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"username\": \"sample-sam\",\n                \"password\": \"hunter2\",\n                \"role\": \"default | admin\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/admin/users/{id}\": {\n      \"post\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"description\": \"Update existing user settings. Methods are disabled until multi user mode is enabled via the UI.\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"id of the user in the database.\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"error\": null\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Instance is not in Multi-User mode. Method denied\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Key pair object that will update the found user. All fields are optional and will not update unless specified.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"username\": \"sample-sam\",\n                \"password\": \"hunter2\",\n                \"role\": \"default | admin\",\n                \"suspended\": 0\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"description\": \"Delete existing user by id. Methods are disabled until multi user mode is enabled via the UI.\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"id of the user in the database.\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"error\": null\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Instance is not in Multi-User mode. Method denied\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/admin/invites\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"description\": \"List all existing invitations to instance regardless of status. Methods are disabled until multi user mode is enabled via the UI.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"invites\": [\n                      {\n                        \"id\": 1,\n                        \"status\": \"pending\",\n                        \"code\": \"abc-123\",\n                        \"claimedBy\": null\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Instance is not in Multi-User mode. Method denied\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/admin/invite/new\": {\n      \"post\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"description\": \"Create a new invite code for someone to use to register with instance. Methods are disabled until multi user mode is enabled via the UI.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"invite\": {\n                      \"id\": 1,\n                      \"status\": \"pending\",\n                      \"code\": \"abc-123\"\n                    },\n                    \"error\": null\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Instance is not in Multi-User mode. Method denied\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Request body for creation parameters of the invitation\",\n          \"required\": false,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"workspaceIds\": [\n                  1,\n                  2,\n                  45\n                ]\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/admin/invite/{id}\": {\n      \"delete\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"description\": \"Deactivates (soft-delete) invite by id. Methods are disabled until multi user mode is enabled via the UI.\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"id of the invite in the database.\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"error\": null\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Instance is not in Multi-User mode. Method denied\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/admin/workspaces/{workspaceId}/users\": {\n      \"get\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"description\": \"Retrieve a list of users with permissions to access the specified workspace.\",\n        \"parameters\": [\n          {\n            \"name\": \"workspaceId\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"id of the workspace.\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"users\": [\n                      {\n                        \"userId\": 1,\n                        \"role\": \"admin\"\n                      },\n                      {\n                        \"userId\": 2,\n                        \"role\": \"member\"\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Instance is not in Multi-User mode. Method denied\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/admin/workspaces/{workspaceId}/update-users\": {\n      \"post\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"description\": \"Overwrite workspace permissions to only be accessible by the given user ids and admins. Methods are disabled until multi user mode is enabled via the UI.\",\n        \"parameters\": [\n          {\n            \"name\": \"workspaceId\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"id of the workspace in the database.\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"error\": null\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Instance is not in Multi-User mode. Method denied\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Entire array of user ids who can access the workspace. All fields are optional and will not update unless specified.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"userIds\": [\n                  1,\n                  2,\n                  4,\n                  12\n                ]\n              }\n            }\n          }\n        },\n        \"deprecated\": true\n      }\n    },\n    \"/v1/admin/workspaces/{workspaceSlug}/manage-users\": {\n      \"post\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"description\": \"Set workspace permissions to be accessible by the given user ids and admins. Methods are disabled until multi user mode is enabled via the UI.\",\n        \"parameters\": [\n          {\n            \"name\": \"workspaceSlug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"slug of the workspace in the database\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"error\": null,\n                    \"users\": [\n                      {\n                        \"userId\": 1,\n                        \"username\": \"main-admin\",\n                        \"role\": \"admin\"\n                      },\n                      {\n                        \"userId\": 2,\n                        \"username\": \"sample-sam\",\n                        \"role\": \"default\"\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Instance is not in Multi-User mode. Method denied\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Not Found\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Array of user ids who will be given access to the target workspace. <code>reset</code> will remove all existing users from the workspace and only add the new users - default <code>false</code>.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"userIds\": [\n                  1,\n                  2,\n                  4,\n                  12\n                ],\n                \"reset\": false\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/admin/workspace-chats\": {\n      \"post\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"description\": \"All chats in the system ordered by most recent. Methods are disabled until multi user mode is enabled via the UI.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"error\": null\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Page offset to show of workspace chats. All fields are optional and will not update unless specified.\",\n          \"required\": false,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"offset\": 2\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/admin/preferences\": {\n      \"post\": {\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"description\": \"Update multi-user preferences for instance. Methods are disabled until multi user mode is enabled via the UI.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"error\": null\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Instance is not in Multi-User mode. Method denied\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Object with setting key and new value to set. All keys are optional and will not update unless specified.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"support_email\": \"support@example.com\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/document/upload\": {\n      \"post\": {\n        \"tags\": [\n          \"Documents\"\n        ],\n        \"description\": \"Upload a new file to AnythingLLM to be parsed and prepared for embedding, with optional metadata.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"error\": null,\n                    \"documents\": [\n                      {\n                        \"location\": \"custom-documents/anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json\",\n                        \"name\": \"anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json\",\n                        \"url\": \"file://Users/tim/Documents/anything-llm/collector/hotdir/anythingllm.txt\",\n                        \"title\": \"anythingllm.txt\",\n                        \"docAuthor\": \"Unknown\",\n                        \"description\": \"Unknown\",\n                        \"docSource\": \"a text file uploaded by the user.\",\n                        \"chunkSource\": \"anythingllm.txt\",\n                        \"published\": \"1/16/2024, 3:07:00 PM\",\n                        \"wordCount\": 93,\n                        \"token_count_estimate\": 115\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Unprocessable Entity\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"File to be uploaded.\",\n          \"required\": true,\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"required\": [\n                  \"file\"\n                ],\n                \"properties\": {\n                  \"file\": {\n                    \"type\": \"string\",\n                    \"format\": \"binary\",\n                    \"description\": \"The file to upload\"\n                  },\n                  \"addToWorkspaces\": {\n                    \"type\": \"string\",\n                    \"description\": \"comma-separated text-string of workspace slugs to embed the document into post-upload. eg: workspace1,workspace2\"\n                  },\n                  \"metadata\": {\n                    \"type\": \"object\",\n                    \"description\": \"Key:Value pairs of metadata to attach to the document in JSON Object format. Only specific keys are allowed - see example.\",\n                    \"example\": {\n                      \"title\": \"Custom Title\",\n                      \"docAuthor\": \"Author Name\",\n                      \"description\": \"A brief description\",\n                      \"docSource\": \"Source of the document\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/document/upload/{folderName}\": {\n      \"post\": {\n        \"tags\": [\n          \"Documents\"\n        ],\n        \"description\": \"Upload a new file to a specific folder in AnythingLLM to be parsed and prepared for embedding. If the folder does not exist, it will be created.\",\n        \"parameters\": [\n          {\n            \"name\": \"folderName\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Target folder path (defaults to 'custom-documents' if not provided)\",\n            \"example\": \"my-folder\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"error\": null,\n                    \"documents\": [\n                      {\n                        \"location\": \"custom-documents/anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json\",\n                        \"name\": \"anythingllm.txt-6e8be64c-c162-4b43-9997-b068c0071e8b.json\",\n                        \"url\": \"file://Users/tim/Documents/anything-llm/collector/hotdir/anythingllm.txt\",\n                        \"title\": \"anythingllm.txt\",\n                        \"docAuthor\": \"Unknown\",\n                        \"description\": \"Unknown\",\n                        \"docSource\": \"a text file uploaded by the user.\",\n                        \"chunkSource\": \"anythingllm.txt\",\n                        \"published\": \"1/16/2024, 3:07:00 PM\",\n                        \"wordCount\": 93,\n                        \"token_count_estimate\": 115\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Unprocessable Entity\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": false,\n                    \"error\": \"Document processing API is not online. Document will not be processed automatically.\"\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"File to be uploaded, with optional metadata.\",\n          \"required\": true,\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"required\": [\n                  \"file\"\n                ],\n                \"properties\": {\n                  \"file\": {\n                    \"type\": \"string\",\n                    \"format\": \"binary\",\n                    \"description\": \"The file to upload\"\n                  },\n                  \"addToWorkspaces\": {\n                    \"type\": \"string\",\n                    \"description\": \"comma-separated text-string of workspace slugs to embed the document into post-upload. eg: workspace1,workspace2\"\n                  },\n                  \"metadata\": {\n                    \"type\": \"object\",\n                    \"description\": \"Key:Value pairs of metadata to attach to the document in JSON Object format. Only specific keys are allowed - see example.\",\n                    \"example\": {\n                      \"title\": \"Custom Title\",\n                      \"docAuthor\": \"Author Name\",\n                      \"description\": \"A brief description\",\n                      \"docSource\": \"Source of the document\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/document/upload-link\": {\n      \"post\": {\n        \"tags\": [\n          \"Documents\"\n        ],\n        \"description\": \"Upload a valid URL for AnythingLLM to scrape and prepare for embedding. Optionally, specify a comma-separated list of workspace slugs to embed the document into post-upload.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"error\": null,\n                    \"documents\": [\n                      {\n                        \"id\": \"c530dbe6-bff1-4b9e-b87f-710d539d20bc\",\n                        \"url\": \"file://useanything_com.html\",\n                        \"title\": \"useanything_com.html\",\n                        \"docAuthor\": \"no author found\",\n                        \"description\": \"No description found.\",\n                        \"docSource\": \"URL link uploaded by the user.\",\n                        \"chunkSource\": \"https:anythingllm.com.html\",\n                        \"published\": \"1/16/2024, 3:46:33 PM\",\n                        \"wordCount\": 252,\n                        \"pageContent\": \"AnythingLLM is the best....\",\n                        \"token_count_estimate\": 447,\n                        \"location\": \"custom-documents/url-useanything_com-c530dbe6-bff1-4b9e-b87f-710d539d20bc.json\"\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Unprocessable Entity\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Link of web address to be scraped and optionally a comma-separated list of workspace slugs to embed the document into post-upload, and optional metadata.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"example\": {\n                  \"link\": \"https://anythingllm.com\",\n                  \"addToWorkspaces\": \"workspace1,workspace2\",\n                  \"scraperHeaders\": {\n                    \"Authorization\": \"Bearer token123\",\n                    \"My-Custom-Header\": \"value\"\n                  },\n                  \"metadata\": {\n                    \"title\": \"Custom Title\",\n                    \"docAuthor\": \"Author Name\",\n                    \"description\": \"A brief description\",\n                    \"docSource\": \"Source of the document\"\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/document/raw-text\": {\n      \"post\": {\n        \"tags\": [\n          \"Documents\"\n        ],\n        \"description\": \"Upload a file by specifying its raw text content and metadata values without having to upload a file.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"error\": null,\n                    \"documents\": [\n                      {\n                        \"id\": \"c530dbe6-bff1-4b9e-b87f-710d539d20bc\",\n                        \"url\": \"file://my-document.txt\",\n                        \"title\": \"hello-world.txt\",\n                        \"docAuthor\": \"no author found\",\n                        \"description\": \"No description found.\",\n                        \"docSource\": \"My custom description set during upload\",\n                        \"chunkSource\": \"no chunk source specified\",\n                        \"published\": \"1/16/2024, 3:46:33 PM\",\n                        \"wordCount\": 252,\n                        \"pageContent\": \"AnythingLLM is the best....\",\n                        \"token_count_estimate\": 447,\n                        \"location\": \"custom-documents/raw-my-doc-text-c530dbe6-bff1-4b9e-b87f-710d539d20bc.json\"\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"422\": {\n            \"description\": \"Unprocessable Entity\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Text content and metadata of the file to be saved to the system. Use metadata-schema endpoint to get the possible metadata keys\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"example\": {\n                  \"textContent\": \"This is the raw text that will be saved as a document in AnythingLLM.\",\n                  \"addToWorkspaces\": \"workspace1,workspace2\",\n                  \"metadata\": {\n                    \"title\": \"This key is required. See in /server/endpoints/api/document/index.js:287\",\n                    \"keyOne\": \"valueOne\",\n                    \"keyTwo\": \"valueTwo\",\n                    \"etc\": \"etc\"\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/documents\": {\n      \"get\": {\n        \"tags\": [\n          \"Documents\"\n        ],\n        \"description\": \"List of all locally-stored documents in instance\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"localFiles\": {\n                      \"name\": \"documents\",\n                      \"type\": \"folder\",\n                      \"items\": [\n                        {\n                          \"name\": \"my-stored-document.json\",\n                          \"type\": \"file\",\n                          \"id\": \"bb07c334-4dab-4419-9462-9d00065a49a1\",\n                          \"url\": \"file://my-stored-document.txt\",\n                          \"title\": \"my-stored-document.txt\",\n                          \"cached\": false\n                        }\n                      ]\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/documents/folder/{folderName}\": {\n      \"get\": {\n        \"tags\": [\n          \"Documents\"\n        ],\n        \"description\": \"Get all documents stored in a specific folder.\",\n        \"parameters\": [\n          {\n            \"name\": \"folderName\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Name of the folder to retrieve documents from\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"folder\": \"custom-documents\",\n                    \"documents\": [\n                      {\n                        \"name\": \"document1.json\",\n                        \"type\": \"file\",\n                        \"cached\": false,\n                        \"pinnedWorkspaces\": [],\n                        \"watched\": false,\n                        \"more\": \"data\"\n                      },\n                      {\n                        \"name\": \"document2.json\",\n                        \"type\": \"file\",\n                        \"cached\": false,\n                        \"pinnedWorkspaces\": [],\n                        \"watched\": false,\n                        \"more\": \"data\"\n                      }\n                    ]\n                  }\n                }\n              }\n            },\n            \"description\": \"OK\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/document/accepted-file-types\": {\n      \"get\": {\n        \"tags\": [\n          \"Documents\"\n        ],\n        \"description\": \"Check available filetypes and MIMEs that can be uploaded.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"types\": {\n                      \"application/mbox\": [\n                        \".mbox\"\n                      ],\n                      \"application/pdf\": [\n                        \".pdf\"\n                      ],\n                      \"application/vnd.oasis.opendocument.text\": [\n                        \".odt\"\n                      ],\n                      \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\": [\n                        \".docx\"\n                      ],\n                      \"text/plain\": [\n                        \".txt\",\n                        \".md\"\n                      ]\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Not Found\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/document/metadata-schema\": {\n      \"get\": {\n        \"tags\": [\n          \"Documents\"\n        ],\n        \"description\": \"Get the known available metadata schema for when doing a raw-text upload and the acceptable type of value for each key.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"schema\": {\n                      \"keyOne\": \"string | number | nullable\",\n                      \"keyTwo\": \"string | number | nullable\",\n                      \"specialKey\": \"number\",\n                      \"title\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/document/{docName}\": {\n      \"get\": {\n        \"tags\": [\n          \"Documents\"\n        ],\n        \"description\": \"Get a single document by its unique AnythingLLM document name\",\n        \"parameters\": [\n          {\n            \"name\": \"docName\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique document name to find (name in /documents)\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"localFiles\": {\n                      \"name\": \"documents\",\n                      \"type\": \"folder\",\n                      \"items\": [\n                        {\n                          \"name\": \"my-stored-document.txt-uuid1234.json\",\n                          \"type\": \"file\",\n                          \"id\": \"bb07c334-4dab-4419-9462-9d00065a49a1\",\n                          \"url\": \"file://my-stored-document.txt\",\n                          \"title\": \"my-stored-document.txt\",\n                          \"cached\": false\n                        }\n                      ]\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Not Found\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/document/create-folder\": {\n      \"post\": {\n        \"tags\": [\n          \"Documents\"\n        ],\n        \"description\": \"Create a new folder inside the documents storage directory.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"message\": null\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Name of the folder to create.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"string\",\n                \"example\": {\n                  \"name\": \"new-folder\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/document/remove-folder\": {\n      \"delete\": {\n        \"tags\": [\n          \"Documents\"\n        ],\n        \"description\": \"Remove a folder and all its contents from the documents storage directory.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"message\": \"Folder removed successfully\"\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Name of the folder to remove.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\",\n                    \"example\": \"my-folder\"\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/document/move-files\": {\n      \"post\": {\n        \"tags\": [\n          \"Documents\"\n        ],\n        \"description\": \"Move files within the documents storage directory.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"message\": null\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Array of objects containing source and destination paths of files to move.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"example\": {\n                  \"files\": [\n                    {\n                      \"from\": \"custom-documents/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json\",\n                      \"to\": \"folder/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json\"\n                    }\n                  ]\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/workspace/new\": {\n      \"post\": {\n        \"tags\": [\n          \"Workspaces\"\n        ],\n        \"description\": \"Create a new workspace\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"workspace\": {\n                      \"id\": 79,\n                      \"name\": \"Sample workspace\",\n                      \"slug\": \"sample-workspace\",\n                      \"createdAt\": \"2023-08-17 00:45:03\",\n                      \"openAiTemp\": null,\n                      \"lastUpdatedAt\": \"2023-08-17 00:45:03\",\n                      \"openAiHistory\": 20,\n                      \"openAiPrompt\": null\n                    },\n                    \"message\": \"Workspace created\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"JSON object containing workspace configuration.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"name\": \"My New Workspace\",\n                \"similarityThreshold\": 0.7,\n                \"openAiTemp\": 0.7,\n                \"openAiHistory\": 20,\n                \"openAiPrompt\": \"Custom prompt for responses\",\n                \"queryRefusalResponse\": \"Custom refusal message\",\n                \"chatMode\": \"chat\",\n                \"topN\": 4\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/workspaces\": {\n      \"get\": {\n        \"tags\": [\n          \"Workspaces\"\n        ],\n        \"description\": \"List all current workspaces\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"workspaces\": [\n                      {\n                        \"id\": 79,\n                        \"name\": \"Sample workspace\",\n                        \"slug\": \"sample-workspace\",\n                        \"createdAt\": \"2023-08-17 00:45:03\",\n                        \"openAiTemp\": null,\n                        \"lastUpdatedAt\": \"2023-08-17 00:45:03\",\n                        \"openAiHistory\": 20,\n                        \"openAiPrompt\": null,\n                        \"threads\": []\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}\": {\n      \"get\": {\n        \"tags\": [\n          \"Workspaces\"\n        ],\n        \"description\": \"Get a workspace by its unique slug.\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of workspace to find\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"workspace\": [\n                      {\n                        \"id\": 79,\n                        \"name\": \"My workspace\",\n                        \"slug\": \"my-workspace-123\",\n                        \"createdAt\": \"2023-08-17 00:45:03\",\n                        \"openAiTemp\": null,\n                        \"lastUpdatedAt\": \"2023-08-17 00:45:03\",\n                        \"openAiHistory\": 20,\n                        \"openAiPrompt\": null,\n                        \"documents\": [],\n                        \"threads\": []\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Workspaces\"\n        ],\n        \"description\": \"Deletes a workspace by its slug.\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of workspace to delete\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\"\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}/update\": {\n      \"post\": {\n        \"tags\": [\n          \"Workspaces\"\n        ],\n        \"description\": \"Update workspace settings by its unique slug.\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of workspace to find\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"workspace\": {\n                      \"id\": 79,\n                      \"name\": \"My workspace\",\n                      \"slug\": \"my-workspace-123\",\n                      \"createdAt\": \"2023-08-17 00:45:03\",\n                      \"openAiTemp\": null,\n                      \"lastUpdatedAt\": \"2023-08-17 00:45:03\",\n                      \"openAiHistory\": 20,\n                      \"openAiPrompt\": null,\n                      \"documents\": []\n                    },\n                    \"message\": null\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"JSON object containing new settings to update a workspace. All keys are optional and will not update unless provided\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"name\": \"Updated Workspace Name\",\n                \"openAiTemp\": 0.2,\n                \"openAiHistory\": 20,\n                \"openAiPrompt\": \"Respond to all inquires and questions in binary - do not respond in any other format.\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}/chats\": {\n      \"get\": {\n        \"tags\": [\n          \"Workspaces\"\n        ],\n        \"description\": \"Get a workspaces chats regardless of user by its unique slug.\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of workspace to find\"\n          },\n          {\n            \"name\": \"apiSessionId\",\n            \"in\": \"query\",\n            \"description\": \"Optional apiSessionId to filter by\",\n            \"required\": false,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"name\": \"limit\",\n            \"in\": \"query\",\n            \"description\": \"Optional number of chat messages to return (default: 100)\",\n            \"required\": false,\n            \"schema\": {\n              \"type\": \"integer\"\n            }\n          },\n          {\n            \"name\": \"orderBy\",\n            \"in\": \"query\",\n            \"description\": \"Optional order of chat messages (asc or desc)\",\n            \"required\": false,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"history\": [\n                      {\n                        \"role\": \"user\",\n                        \"content\": \"What is AnythingLLM?\",\n                        \"sentAt\": 1692851630\n                      },\n                      {\n                        \"role\": \"assistant\",\n                        \"content\": \"AnythingLLM is a platform that allows you to convert notes, PDFs, and other source materials into a chatbot. It ensures privacy, cites its answers, and allows multiple people to interact with the same documents simultaneously. It is particularly useful for businesses to enhance the visibility and readability of various written communications such as SOPs, contracts, and sales calls. You can try it out with a free trial to see if it meets your business needs.\",\n                        \"sources\": [\n                          {\n                            \"source\": \"object about source document and snippets used\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}/update-embeddings\": {\n      \"post\": {\n        \"tags\": [\n          \"Workspaces\"\n        ],\n        \"description\": \"Add or remove documents from a workspace by its unique slug.\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of workspace to find\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"workspace\": {\n                      \"id\": 79,\n                      \"name\": \"My workspace\",\n                      \"slug\": \"my-workspace-123\",\n                      \"createdAt\": \"2023-08-17 00:45:03\",\n                      \"openAiTemp\": null,\n                      \"lastUpdatedAt\": \"2023-08-17 00:45:03\",\n                      \"openAiHistory\": 20,\n                      \"openAiPrompt\": null,\n                      \"documents\": []\n                    },\n                    \"message\": null\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"JSON object of additions and removals of documents to add to update a workspace. The value should be the folder + filename with the exclusions of the top-level documents path.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"adds\": [\n                  \"custom-documents/my-pdf.pdf-hash.json\"\n                ],\n                \"deletes\": [\n                  \"custom-documents/anythingllm.txt-hash.json\"\n                ]\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}/update-pin\": {\n      \"post\": {\n        \"tags\": [\n          \"Workspaces\"\n        ],\n        \"description\": \"Add or remove pin from a document in a workspace by its unique slug.\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of workspace to find\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"message\": \"Pin status updated successfully\"\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\"\n          },\n          \"404\": {\n            \"description\": \"Document not found\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"JSON object with the document path and pin status to update.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"docPath\": \"custom-documents/my-pdf.pdf-hash.json\",\n                \"pinStatus\": true\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}/chat\": {\n      \"post\": {\n        \"tags\": [\n          \"Workspaces\"\n        ],\n        \"description\": \"Execute a chat with a workspace\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"id\": \"chat-uuid\",\n                    \"type\": \"abort | textResponse\",\n                    \"textResponse\": \"Response to your query\",\n                    \"sources\": [\n                      {\n                        \"title\": \"anythingllm.txt\",\n                        \"chunk\": \"This is a context chunk used in the answer of the prompt by the LLM,\"\n                      }\n                    ],\n                    \"close\": true,\n                    \"error\": \"null | text string of the failure mode.\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Send a prompt to the workspace and the type of conversation (automatic, query or chat).<br/><b>Query:</b> Will not use LLM unless there are relevant sources from vectorDB & does not recall chat history.<br/><b>Automatic:</b> Will use tool-calling if the provider supports native tool calling without needing to invoke @agent.<br/><b>Chat:</b> Uses LLM general knowledge w/custom embeddings to produce output, uses rolling chat history.<br/><b>Attachments:</b> Can include images and documents.<br/><b>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Document attachments:</b> must have the mime type <code>application/anythingllm-document</code> - otherwise it will be passed to the LLM as an image and may fail to process. This uses the built-in document processor to first parse the document to text before injecting it into the context window.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"message\": \"What is AnythingLLM?\",\n                \"mode\": \"automatic | query | chat\",\n                \"sessionId\": \"identifier-to-partition-chats-by-external-id\",\n                \"attachments\": [\n                  {\n                    \"name\": \"image.png\",\n                    \"mime\": \"image/png\",\n                    \"contentString\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n                  },\n                  {\n                    \"name\": \"this is a document.pdf\",\n                    \"mime\": \"application/anythingllm-document\",\n                    \"contentString\": \"data:application/pdf;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n                  }\n                ],\n                \"reset\": false\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}/stream-chat\": {\n      \"post\": {\n        \"tags\": [\n          \"Workspaces\"\n        ],\n        \"description\": \"Execute a streamable chat with a workspace\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"text/event-stream\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"string\"\n                  },\n                  \"example\": [\n                    {\n                      \"id\": \"uuid-123\",\n                      \"type\": \"abort | textResponseChunk\",\n                      \"textResponse\": \"First chunk\",\n                      \"sources\": [],\n                      \"close\": false,\n                      \"error\": \"null | text string of the failure mode.\"\n                    },\n                    {\n                      \"id\": \"uuid-123\",\n                      \"type\": \"abort | textResponseChunk\",\n                      \"textResponse\": \"chunk two\",\n                      \"sources\": [],\n                      \"close\": false,\n                      \"error\": \"null | text string of the failure mode.\"\n                    },\n                    {\n                      \"id\": \"uuid-123\",\n                      \"type\": \"abort | textResponseChunk\",\n                      \"textResponse\": \"final chunk of LLM output!\",\n                      \"sources\": [\n                        {\n                          \"title\": \"anythingllm.txt\",\n                          \"chunk\": \"This is a context chunk used in the answer of the prompt by the LLM. This will only return in the final chunk.\"\n                        }\n                      ],\n                      \"close\": true,\n                      \"error\": \"null | text string of the failure mode.\"\n                    }\n                  ]\n                }\n              }\n            },\n            \"description\": \"OK\"\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Send a prompt to the workspace and the type of conversation (automatic, query or chat).<br/><b>Query:</b> Will not use LLM unless there are relevant sources from vectorDB & does not recall chat history.<br/><b>Automatic:</b> Will use tool-calling if the provider supports native tool calling without needing to invoke @agent.<br/><b>Chat:</b> Uses LLM general knowledge w/custom embeddings to produce output, uses rolling chat history.<br/><b>Attachments:</b> Can include images and documents.<br/><b>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Document attachments:</b> must have the mime type <code>application/anythingllm-document</code> - otherwise it will be passed to the LLM as an image and may fail to process. This uses the built-in document processor to first parse the document to text before injecting it into the context window.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"message\": \"What is AnythingLLM?\",\n                \"mode\": \"automatic | query | chat\",\n                \"sessionId\": \"identifier-to-partition-chats-by-external-id\",\n                \"attachments\": [\n                  {\n                    \"name\": \"image.png\",\n                    \"mime\": \"image/png\",\n                    \"contentString\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n                  },\n                  {\n                    \"name\": \"this is a document.pdf\",\n                    \"mime\": \"application/anythingllm-document\",\n                    \"contentString\": \"data:application/pdf;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n                  }\n                ],\n                \"reset\": false\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}/vector-search\": {\n      \"post\": {\n        \"tags\": [\n          \"Workspaces\"\n        ],\n        \"description\": \"Perform a vector similarity search in a workspace\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of workspace to search in\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"results\": [\n                      {\n                        \"id\": \"5a6bee0a-306c-47fc-942b-8ab9bf3899c4\",\n                        \"text\": \"Document chunk content...\",\n                        \"metadata\": {\n                          \"url\": \"file://document.txt\",\n                          \"title\": \"document.txt\",\n                          \"author\": \"no author specified\",\n                          \"description\": \"no description found\",\n                          \"docSource\": \"post:123456\",\n                          \"chunkSource\": \"document.txt\",\n                          \"published\": \"12/1/2024, 11:39:39 AM\",\n                          \"wordCount\": 8,\n                          \"tokenCount\": 9\n                        },\n                        \"distance\": 0.541887640953064,\n                        \"score\": 0.45811235904693604\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Query to perform vector search with and optional parameters\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"query\": \"What is the meaning of life?\",\n                \"topN\": 4,\n                \"scoreThreshold\": 0.75\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/system/env-dump\": {\n      \"get\": {\n        \"tags\": [\n          \"System Settings\"\n        ],\n        \"description\": \"Dump all settings to file storage\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/system\": {\n      \"get\": {\n        \"tags\": [\n          \"System Settings\"\n        ],\n        \"description\": \"Get all current system settings that are defined.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"settings\": {\n                      \"VectorDB\": \"pinecone\",\n                      \"PineConeKey\": true,\n                      \"PineConeIndex\": \"my-pinecone-index\",\n                      \"LLMProvider\": \"azure\",\n                      \"[KEY_NAME]\": \"KEY_VALUE\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/system/vector-count\": {\n      \"get\": {\n        \"tags\": [\n          \"System Settings\"\n        ],\n        \"description\": \"Number of all vectors in connected vector database\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"vectorCount\": 5450\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/system/update-env\": {\n      \"post\": {\n        \"tags\": [\n          \"System Settings\"\n        ],\n        \"description\": \"Update a system setting or preference.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"newValues\": {\n                      \"[ENV_KEY]\": \"Value\"\n                    },\n                    \"error\": \"error goes here, otherwise null\"\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Key pair object that matches a valid setting and value. Get keys from GET /v1/system or refer to codebase.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"VectorDB\": \"lancedb\",\n                \"AnotherKey\": \"updatedValue\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/system/export-chats\": {\n      \"get\": {\n        \"tags\": [\n          \"System Settings\"\n        ],\n        \"description\": \"Export all of the chats from the system in a known format. Output depends on the type sent. Will be send with the correct header for the output.\",\n        \"parameters\": [\n          {\n            \"name\": \"type\",\n            \"in\": \"query\",\n            \"description\": \"Export format jsonl, json, csv, jsonAlpaca\",\n            \"required\": false,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": [\n                    {\n                      \"role\": \"user\",\n                      \"content\": \"What is AnythinglLM?\"\n                    },\n                    {\n                      \"role\": \"assistant\",\n                      \"content\": \"AnythingLLM is a knowledge graph and vector database management system built using NodeJS express server. It provides an interface for handling all interactions, including vectorDB management and LLM (Language Model) interactions.\"\n                    }\n                  ]\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/system/remove-documents\": {\n      \"delete\": {\n        \"tags\": [\n          \"System Settings\"\n        ],\n        \"description\": \"Permanently remove documents from the system.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Documents removed successfully.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"message\": \"Documents removed successfully\"\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Array of document names to be removed permanently.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"names\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"type\": \"string\"\n                    },\n                    \"example\": [\n                      \"custom-documents/file.txt-fc4beeeb-e436-454d-8bb4-e5b8979cb48f.json\"\n                    ]\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}/thread/new\": {\n      \"post\": {\n        \"tags\": [\n          \"Workspace Threads\"\n        ],\n        \"description\": \"Create a new workspace thread\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of workspace\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"thread\": {\n                      \"id\": 1,\n                      \"name\": \"Thread\",\n                      \"slug\": \"thread-uuid\",\n                      \"user_id\": 1,\n                      \"workspace_id\": 1\n                    },\n                    \"message\": null\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Optional userId associated with the thread, thread slug and thread name\",\n          \"required\": false,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"userId\": 1,\n                \"name\": \"Name\",\n                \"slug\": \"thread-slug\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}/thread/{threadSlug}/update\": {\n      \"post\": {\n        \"tags\": [\n          \"Workspace Threads\"\n        ],\n        \"description\": \"Update thread name by its unique slug.\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of workspace\"\n          },\n          {\n            \"name\": \"threadSlug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of thread\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"thread\": {\n                      \"id\": 1,\n                      \"name\": \"Updated Thread Name\",\n                      \"slug\": \"thread-uuid\",\n                      \"user_id\": 1,\n                      \"workspace_id\": 1\n                    },\n                    \"message\": null\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"JSON object containing new name to update the thread.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"name\": \"Updated Thread Name\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}/thread/{threadSlug}\": {\n      \"delete\": {\n        \"tags\": [\n          \"Workspace Threads\"\n        ],\n        \"description\": \"Delete a workspace thread\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of workspace\"\n          },\n          {\n            \"name\": \"threadSlug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of thread\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Thread deleted successfully\"\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}/thread/{threadSlug}/chats\": {\n      \"get\": {\n        \"tags\": [\n          \"Workspace Threads\"\n        ],\n        \"description\": \"Get chats for a workspace thread\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of workspace\"\n          },\n          {\n            \"name\": \"threadSlug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of thread\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"history\": [\n                      {\n                        \"role\": \"user\",\n                        \"content\": \"What is AnythingLLM?\",\n                        \"sentAt\": 1692851630\n                      },\n                      {\n                        \"role\": \"assistant\",\n                        \"content\": \"AnythingLLM is a platform that allows you to convert notes, PDFs, and other source materials into a chatbot. It ensures privacy, cites its answers, and allows multiple people to interact with the same documents simultaneously. It is particularly useful for businesses to enhance the visibility and readability of various written communications such as SOPs, contracts, and sales calls. You can try it out with a free trial to see if it meets your business needs.\",\n                        \"sources\": [\n                          {\n                            \"source\": \"object about source document and snippets used\"\n                          }\n                        ]\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}/thread/{threadSlug}/chat\": {\n      \"post\": {\n        \"tags\": [\n          \"Workspace Threads\"\n        ],\n        \"description\": \"Chat with a workspace thread\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of workspace\"\n          },\n          {\n            \"name\": \"threadSlug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of thread\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"id\": \"chat-uuid\",\n                    \"type\": \"abort | textResponse\",\n                    \"textResponse\": \"Response to your query\",\n                    \"sources\": [\n                      {\n                        \"title\": \"anythingllm.txt\",\n                        \"chunk\": \"This is a context chunk used in the answer of the prompt by the LLM.\"\n                      }\n                    ],\n                    \"close\": true,\n                    \"error\": \"null | text string of the failure mode.\"\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Send a prompt to the workspace thread and the type of conversation (query or chat).\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"message\": \"What is AnythingLLM?\",\n                \"mode\": \"query | chat\",\n                \"userId\": 1,\n                \"attachments\": [\n                  {\n                    \"name\": \"image.png\",\n                    \"mime\": \"image/png\",\n                    \"contentString\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n                  }\n                ],\n                \"reset\": false\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/workspace/{slug}/thread/{threadSlug}/stream-chat\": {\n      \"post\": {\n        \"tags\": [\n          \"Workspace Threads\"\n        ],\n        \"description\": \"Stream chat with a workspace thread\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of workspace\"\n          },\n          {\n            \"name\": \"threadSlug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"Unique slug of thread\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"text/event-stream\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"string\"\n                  },\n                  \"example\": [\n                    {\n                      \"id\": \"uuid-123\",\n                      \"type\": \"abort | textResponseChunk\",\n                      \"textResponse\": \"First chunk\",\n                      \"sources\": [],\n                      \"close\": false,\n                      \"error\": \"null | text string of the failure mode.\"\n                    },\n                    {\n                      \"id\": \"uuid-123\",\n                      \"type\": \"abort | textResponseChunk\",\n                      \"textResponse\": \"chunk two\",\n                      \"sources\": [],\n                      \"close\": false,\n                      \"error\": \"null | text string of the failure mode.\"\n                    },\n                    {\n                      \"id\": \"uuid-123\",\n                      \"type\": \"abort | textResponseChunk\",\n                      \"textResponse\": \"final chunk of LLM output!\",\n                      \"sources\": [\n                        {\n                          \"title\": \"anythingllm.txt\",\n                          \"chunk\": \"This is a context chunk used in the answer of the prompt by the LLM. This will only return in the final chunk.\"\n                        }\n                      ],\n                      \"close\": true,\n                      \"error\": \"null | text string of the failure mode.\"\n                    }\n                  ]\n                }\n              }\n            },\n            \"description\": \"OK\"\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Send a prompt to the workspace thread and the type of conversation (query or chat).\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"message\": \"What is AnythingLLM?\",\n                \"mode\": \"query | chat\",\n                \"userId\": 1,\n                \"attachments\": [\n                  {\n                    \"name\": \"image.png\",\n                    \"mime\": \"image/png\",\n                    \"contentString\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n                  },\n                  {\n                    \"name\": \"this is a document.pdf\",\n                    \"mime\": \"application/anythingllm-document\",\n                    \"contentString\": \"data:application/pdf;base64,iVBORw0KGgoAAAANSUhEUgAA...\"\n                  }\n                ],\n                \"reset\": false\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/users\": {\n      \"get\": {\n        \"tags\": [\n          \"User Management\"\n        ],\n        \"description\": \"List all users\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"users\": [\n                      {\n                        \"id\": 1,\n                        \"username\": \"john_doe\",\n                        \"role\": \"admin\"\n                      },\n                      {\n                        \"id\": 2,\n                        \"username\": \"jane_smith\",\n                        \"role\": \"default\"\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Instance is not in Multi-User mode. Permission denied.\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/users/{id}/issue-auth-token\": {\n      \"get\": {\n        \"tags\": [\n          \"User Management\"\n        ],\n        \"description\": \"Issue a temporary auth token for a user\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"The ID of the user to issue a temporary auth token for\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"token\": \"1234567890\",\n                    \"loginPath\": \"/sso/simple?token=1234567890\"\n                  }\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Instance is not in Multi-User mode. Permission denied.\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Not Found\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/openai/models\": {\n      \"get\": {\n        \"tags\": [\n          \"OpenAI Compatible Endpoints\"\n        ],\n        \"description\": \"Get all available \\\"models\\\" which are workspaces you can use for chatting.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"object\": \"list\",\n                    \"data\": [\n                      {\n                        \"id\": \"model-id-0\",\n                        \"object\": \"model\",\n                        \"created\": 1686935002,\n                        \"owned_by\": \"organization-owner\"\n                      },\n                      {\n                        \"id\": \"model-id-1\",\n                        \"object\": \"model\",\n                        \"created\": 1686935002,\n                        \"owned_by\": \"organization-owner\"\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/openai/chat/completions\": {\n      \"post\": {\n        \"tags\": [\n          \"OpenAI Compatible Endpoints\"\n        ],\n        \"description\": \"Execute a chat with a workspace with OpenAI compatibility. Supports streaming as well. Model must be a workspace slug from /models.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\"\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"Send a prompt to the workspace with full use of documents as if sending a chat in AnythingLLM. Only supports some values of OpenAI API. See example below.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"messages\": [\n                  {\n                    \"role\": \"system\",\n                    \"content\": \"You are a helpful assistant\"\n                  },\n                  {\n                    \"role\": \"user\",\n                    \"content\": \"What is AnythingLLM?\"\n                  },\n                  {\n                    \"role\": \"assistant\",\n                    \"content\": \"AnythingLLM is....\"\n                  },\n                  {\n                    \"role\": \"user\",\n                    \"content\": \"Follow up question...\"\n                  }\n                ],\n                \"model\": \"sample-workspace\",\n                \"stream\": true,\n                \"temperature\": 0.7\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/openai/embeddings\": {\n      \"post\": {\n        \"tags\": [\n          \"OpenAI Compatible Endpoints\"\n        ],\n        \"description\": \"Get the embeddings of any arbitrary text string. This will use the embedder provider set in the system. Please ensure the token length of each string fits within the context of your embedder model.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"The input string(s) to be embedded. If the text is too long for the embedder model context, it will fail to embed. The vector and associated chunk metadata will be returned in the array order provided\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"example\": {\n                \"input\": [\n                  \"This is my first string to embed\",\n                  \"This is my second string to embed\"\n                ],\n                \"model\": null\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/openai/vector_stores\": {\n      \"get\": {\n        \"tags\": [\n          \"OpenAI Compatible Endpoints\"\n        ],\n        \"description\": \"List all the vector database collections connected to AnythingLLM. These are essentially workspaces but return their unique vector db identifier - this is the same as the workspace slug.\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"data\": [\n                      {\n                        \"id\": \"slug-here\",\n                        \"object\": \"vector_store\",\n                        \"name\": \"My workspace\",\n                        \"file_counts\": {\n                          \"total\": 3\n                        },\n                        \"provider\": \"LanceDB\"\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/embed\": {\n      \"get\": {\n        \"tags\": [\n          \"Embed\"\n        ],\n        \"description\": \"List all active embeds\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"embeds\": [\n                      {\n                        \"id\": 1,\n                        \"uuid\": \"embed-uuid-1\",\n                        \"enabled\": true,\n                        \"chat_mode\": \"query\",\n                        \"createdAt\": \"2023-04-01T12:00:00Z\",\n                        \"workspace\": {\n                          \"id\": 1,\n                          \"name\": \"Workspace 1\"\n                        },\n                        \"chat_count\": 10\n                      },\n                      {\n                        \"id\": 2,\n                        \"uuid\": \"embed-uuid-2\",\n                        \"enabled\": false,\n                        \"chat_mode\": \"chat\",\n                        \"createdAt\": \"2023-04-02T14:30:00Z\",\n                        \"workspace\": {\n                          \"id\": 1,\n                          \"name\": \"Workspace 1\"\n                        },\n                        \"chat_count\": 10\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/embed/{embedUuid}/chats\": {\n      \"get\": {\n        \"tags\": [\n          \"Embed\"\n        ],\n        \"description\": \"Get all chats for a specific embed\",\n        \"parameters\": [\n          {\n            \"name\": \"embedUuid\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"UUID of the embed\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"chats\": [\n                      {\n                        \"id\": 1,\n                        \"session_id\": \"session-uuid-1\",\n                        \"prompt\": \"Hello\",\n                        \"response\": \"Hi there!\",\n                        \"createdAt\": \"2023-04-01T12:00:00Z\"\n                      },\n                      {\n                        \"id\": 2,\n                        \"session_id\": \"session-uuid-2\",\n                        \"prompt\": \"How are you?\",\n                        \"response\": \"I'm doing well, thank you!\",\n                        \"createdAt\": \"2023-04-02T14:30:00Z\"\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Embed not found\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/embed/{embedUuid}/chats/{sessionUuid}\": {\n      \"get\": {\n        \"tags\": [\n          \"Embed\"\n        ],\n        \"description\": \"Get chats for a specific embed and session\",\n        \"parameters\": [\n          {\n            \"name\": \"embedUuid\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"UUID of the embed\"\n          },\n          {\n            \"name\": \"sessionUuid\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"UUID of the session\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"chats\": [\n                      {\n                        \"id\": 1,\n                        \"prompt\": \"Hello\",\n                        \"response\": \"Hi there!\",\n                        \"createdAt\": \"2023-04-01T12:00:00Z\"\n                      }\n                    ]\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Embed or session not found\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    },\n    \"/v1/embed/new\": {\n      \"post\": {\n        \"tags\": [\n          \"Embed\"\n        ],\n        \"description\": \"Create a new embed configuration\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"embed\": {\n                      \"id\": 1,\n                      \"uuid\": \"embed-uuid-1\",\n                      \"enabled\": true,\n                      \"chat_mode\": \"chat\",\n                      \"allowlist_domains\": [\n                        \"example.com\"\n                      ],\n                      \"allow_model_override\": false,\n                      \"allow_temperature_override\": false,\n                      \"allow_prompt_override\": false,\n                      \"max_chats_per_day\": 100,\n                      \"max_chats_per_session\": 10,\n                      \"createdAt\": \"2023-04-01T12:00:00Z\",\n                      \"workspace_slug\": \"workspace-slug-1\"\n                    },\n                    \"error\": null\n                  }\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad Request\"\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Workspace not found\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"JSON object containing embed configuration details\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"example\": {\n                  \"workspace_slug\": \"workspace-slug-1\",\n                  \"chat_mode\": \"chat\",\n                  \"allowlist_domains\": [\n                    \"example.com\"\n                  ],\n                  \"allow_model_override\": false,\n                  \"allow_temperature_override\": false,\n                  \"allow_prompt_override\": false,\n                  \"max_chats_per_day\": 100,\n                  \"max_chats_per_session\": 10\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/v1/embed/{embedUuid}\": {\n      \"post\": {\n        \"tags\": [\n          \"Embed\"\n        ],\n        \"description\": \"Update an existing embed configuration\",\n        \"parameters\": [\n          {\n            \"name\": \"embedUuid\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"UUID of the embed to update\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"error\": null\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Embed not found\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        },\n        \"requestBody\": {\n          \"description\": \"JSON object containing embed configuration updates\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"example\": {\n                  \"enabled\": true,\n                  \"chat_mode\": \"chat\",\n                  \"allowlist_domains\": [\n                    \"example.com\"\n                  ],\n                  \"allow_model_override\": false,\n                  \"allow_temperature_override\": false,\n                  \"allow_prompt_override\": false,\n                  \"max_chats_per_day\": 100,\n                  \"max_chats_per_session\": 10\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"tags\": [\n          \"Embed\"\n        ],\n        \"description\": \"Delete an existing embed configuration\",\n        \"parameters\": [\n          {\n            \"name\": \"embedUuid\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"description\": \"UUID of the embed to delete\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"example\": {\n                    \"success\": true,\n                    \"error\": null\n                  }\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              },\n              \"application/xml\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/InvalidAPIKey\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Embed not found\"\n          },\n          \"500\": {\n            \"description\": \"Internal Server Error\"\n          }\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"schemas\": {\n      \"InvalidAPIKey\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"message\": {\n            \"type\": \"string\",\n            \"example\": \"Invalid API Key\"\n          }\n        },\n        \"xml\": {\n          \"name\": \"InvalidAPIKey\"\n        }\n      }\n    },\n    \"securitySchemes\": {\n      \"BearerAuth\": {\n        \"type\": \"http\",\n        \"scheme\": \"bearer\",\n        \"bearerFormat\": \"JWT\"\n      }\n    }\n  },\n  \"security\": [\n    {\n      \"BearerAuth\": []\n    }\n  ]\n}"
  },
  {
    "path": "server/swagger/utils.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst swaggerUi = require(\"swagger-ui-express\");\n\nfunction faviconUrl() {\n  return process.env.NODE_ENV === \"production\"\n    ? \"/public/favicon.png\"\n    : \"http://localhost:3000/public/favicon.png\";\n}\n\nfunction useSwagger(app) {\n  if (process.env.DISABLE_SWAGGER_DOCS === \"true\") {\n    console.log(\n      `\\x1b[33m[SWAGGER DISABLED]\\x1b[0m Swagger documentation is disabled via DISABLE_SWAGGER_DOCS environment variable.`\n    );\n    return;\n  }\n  app.use(\"/api/docs\", swaggerUi.serve);\n  const options = {\n    customCss: [\n      fs.readFileSync(path.resolve(__dirname, \"index.css\")),\n      fs.readFileSync(path.resolve(__dirname, \"dark-swagger.css\")),\n    ].join(\"\\n\\n\\n\"),\n    customSiteTitle: \"AnythingLLM Developer API Documentation\",\n    customfavIcon: faviconUrl(),\n  };\n\n  if (process.env.NODE_ENV === \"production\") {\n    const swaggerDocument = require(\"./openapi.json\");\n    app.get(\n      \"/api/docs\",\n      swaggerUi.setup(swaggerDocument, {\n        ...options,\n        customJsStr:\n          'window.SWAGGER_DOCS_ENV = \"production\";\\n\\n' +\n          fs.readFileSync(path.resolve(__dirname, \"index.js\"), \"utf8\"),\n      })\n    );\n  } else {\n    // we regenerate the html page only in development mode to ensure it is up-to-date when the code is hot-reloaded.\n    app.get(\"/api/docs\", async (_, response) => {\n      // #swagger.ignore = true\n      const swaggerDocument = require(\"./openapi.json\");\n      return response.send(\n        swaggerUi.generateHTML(swaggerDocument, {\n          ...options,\n          customJsStr:\n            'window.SWAGGER_DOCS_ENV = \"development\";\\n\\n' +\n            fs.readFileSync(path.resolve(__dirname, \"index.js\"), \"utf8\"),\n        })\n      );\n    });\n  }\n}\n\nmodule.exports = { faviconUrl, useSwagger };\n"
  },
  {
    "path": "server/utils/AiProviders/anthropic/index.js",
    "content": "const { v4 } = require(\"uuid\");\nconst {\n  writeResponseChunk,\n  clientAbortedHandler,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst { MODEL_MAP } = require(\"../modelMap\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\n\nclass AnthropicLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.ANTHROPIC_API_KEY)\n      throw new Error(\"No Anthropic API key was set.\");\n\n    this.className = \"AnthropicLLM\";\n    // Docs: https://www.npmjs.com/package/@anthropic-ai/sdk\n    const AnthropicAI = require(\"@anthropic-ai/sdk\");\n    const anthropic = new AnthropicAI({\n      apiKey: process.env.ANTHROPIC_API_KEY,\n    });\n    this.anthropic = anthropic;\n    this.model =\n      modelPreference ||\n      process.env.ANTHROPIC_MODEL_PREF ||\n      \"claude-3-5-sonnet-20241022\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.log(\n      `Initialized with ${this.model}. Cache ${this.cacheControl ? `enabled (${this.cacheControl.ttl})` : \"disabled\"}`\n    );\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    return MODEL_MAP.get(\"anthropic\", modelName) ?? 100_000;\n  }\n\n  promptWindowLimit() {\n    return MODEL_MAP.get(\"anthropic\", this.model) ?? 100_000;\n  }\n\n  isValidChatCompletionModel(_modelName = \"\") {\n    return true;\n  }\n\n  /**\n   * Parses the cache control ENV variable\n   *\n   * If caching is enabled, we can pass less than 1024 tokens and Anthropic will just\n   * ignore it unless it is above the model's minimum. Since this feature is opt-in\n   * we can safely assume that if caching is enabled that we should just pass the content as is.\n   * https://docs.claude.com/en/docs/build-with-claude/prompt-caching#cache-limitations\n   *\n   * @param {string} value - The ENV value (5m or 1h)\n   * @returns {null|{type: \"ephemeral\", ttl: \"5m\" | \"1h\"}} Cache control configuration\n   */\n  get cacheControl() {\n    // Store result in instance variable to avoid recalculating\n    if (this._cacheControl) return this._cacheControl;\n\n    if (!process.env.ANTHROPIC_CACHE_CONTROL) this._cacheControl = null;\n    else {\n      const normalized =\n        process.env.ANTHROPIC_CACHE_CONTROL.toLowerCase().trim();\n      if ([\"5m\", \"1h\"].includes(normalized))\n        this._cacheControl = { type: \"ephemeral\", ttl: normalized };\n      else this._cacheControl = null;\n    }\n    return this._cacheControl;\n  }\n\n  /**\n   * Builds system parameter with cache control if applicable\n   * @param {string} systemContent - The system prompt content\n   * @returns {string|array} System parameter for API call\n   */\n  #buildSystemPrompt(systemContent) {\n    if (!systemContent || !this.cacheControl) return systemContent;\n    return [\n      {\n        type: \"text\",\n        text: systemContent,\n        cache_control: this.cacheControl,\n      },\n    ];\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image\",\n        source: {\n          type: \"base64\",\n          media_type: attachment.mime,\n          data: attachment.contentString.split(\"base64,\")[1],\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [], // This is the specific attachment for only this prompt\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    try {\n      const systemContent = messages[0].content;\n      const result = await LLMPerformanceMonitor.measureAsyncFunction(\n        this.anthropic.messages.create({\n          model: this.model,\n          max_tokens: 4096,\n          system: this.#buildSystemPrompt(systemContent),\n          messages: messages.slice(1), // Pop off the system message\n          temperature: Number(temperature ?? this.defaultTemp),\n        })\n      );\n\n      const promptTokens = result.output.usage.input_tokens;\n      const completionTokens = result.output.usage.output_tokens;\n\n      return {\n        textResponse: result.output.content[0].text,\n        metrics: {\n          prompt_tokens: promptTokens,\n          completion_tokens: completionTokens,\n          total_tokens: promptTokens + completionTokens,\n          outputTps: completionTokens / result.duration,\n          duration: result.duration,\n          model: this.model,\n          provider: this.className,\n          timestamp: new Date(),\n        },\n      };\n    } catch (error) {\n      console.log(error);\n      return { textResponse: error, metrics: {} };\n    }\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    const systemContent = messages[0].content;\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.anthropic.messages.stream({\n        model: this.model,\n        max_tokens: 4096,\n        system: this.#buildSystemPrompt(systemContent),\n        messages: messages.slice(1), // Pop off the system message\n        temperature: Number(temperature ?? this.defaultTemp),\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  /**\n   * Handles the stream response from the Anthropic API.\n   * @param {Object} response - the response object\n   * @param {import('../../helpers/chat/LLMPerformanceMonitor').MonitoredStream} stream - the stream response from the Anthropic API w/tracking\n   * @param {Object} responseProps - the response properties\n   * @returns {Promise<string>}\n   */\n  handleStream(response, stream, responseProps) {\n    return new Promise((resolve) => {\n      let fullText = \"\";\n      const { uuid = v4(), sources = [] } = responseProps;\n      let usage = {\n        prompt_tokens: 0,\n        completion_tokens: 0,\n      };\n\n      // Establish listener to early-abort a streaming response\n      // in case things go sideways or the user does not like the response.\n      // We preserve the generated text but continue as if chat was completed\n      // to preserve previously generated content.\n      const handleAbort = () => {\n        stream?.endMeasurement(usage);\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      stream.on(\"error\", (event) => {\n        const parseErrorMsg = (event) => {\n          const error = event?.error?.error;\n          if (!!error)\n            return `Anthropic Error:${error?.type || \"unknown\"} ${\n              error?.message || \"unknown error.\"\n            }`;\n          return event.message;\n        };\n\n        writeResponseChunk(response, {\n          uuid,\n          sources: [],\n          type: \"abort\",\n          textResponse: null,\n          close: true,\n          error: parseErrorMsg(event),\n        });\n        response.removeListener(\"close\", handleAbort);\n        stream?.endMeasurement(usage);\n        resolve(fullText);\n      });\n\n      stream.on(\"streamEvent\", (message) => {\n        const data = message;\n\n        if (data.type === \"message_start\")\n          usage.prompt_tokens = data?.message?.usage?.input_tokens;\n        if (data.type === \"message_delta\")\n          usage.completion_tokens = data?.usage?.output_tokens;\n\n        if (\n          data.type === \"content_block_delta\" &&\n          data.delta.type === \"text_delta\"\n        ) {\n          const text = data.delta.text;\n          fullText += text;\n\n          writeResponseChunk(response, {\n            uuid,\n            sources,\n            type: \"textResponseChunk\",\n            textResponse: text,\n            close: false,\n            error: false,\n          });\n        }\n\n        if (\n          message.type === \"message_stop\" ||\n          (data.stop_reason && data.stop_reason === \"end_turn\")\n        ) {\n          writeResponseChunk(response, {\n            uuid,\n            sources,\n            type: \"textResponseChunk\",\n            textResponse: \"\",\n            close: true,\n            error: false,\n          });\n          response.removeListener(\"close\", handleAbort);\n          stream?.endMeasurement(usage);\n          resolve(fullText);\n        }\n      });\n    });\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageStringCompressor } = require(\"../../helpers/chat\");\n    const compressedPrompt = await messageStringCompressor(\n      this,\n      promptArgs,\n      rawHistory\n    );\n    return compressedPrompt;\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n}\n\nmodule.exports = {\n  AnthropicLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/apipie/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst {\n  writeResponseChunk,\n  clientAbortedHandler,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { safeJsonParse } = require(\"../../http\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\n\nconst cacheFolder = path.resolve(\n  process.env.STORAGE_DIR\n    ? path.resolve(process.env.STORAGE_DIR, \"models\", \"apipie\")\n    : path.resolve(__dirname, `../../../storage/models/apipie`)\n);\n\nclass ApiPieLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.APIPIE_LLM_API_KEY)\n      throw new Error(\"No ApiPie LLM API key was set.\");\n\n    this.className = \"ApiPieLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.basePath = \"https://apipie.ai/v1\";\n    this.openai = new OpenAIApi({\n      baseURL: this.basePath,\n      apiKey: process.env.APIPIE_LLM_API_KEY ?? null,\n    });\n    this.model =\n      modelPreference ||\n      process.env.APIPIE_LLM_MODEL_PREF ||\n      \"openrouter/mistral-7b-instruct\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n\n    if (!fs.existsSync(cacheFolder))\n      fs.mkdirSync(cacheFolder, { recursive: true });\n    this.cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n    this.cacheAtPath = path.resolve(cacheFolder, \".cached_at\");\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  // This checks if the .cached_at file has a timestamp that is more than 1Week (in millis)\n  // from the current date. If it is, then we will refetch the API so that all the models are up\n  // to date.\n  #cacheIsStale() {\n    const MAX_STALE = 6.048e8; // 1 Week in MS\n    if (!fs.existsSync(this.cacheAtPath)) return true;\n    const now = Number(new Date());\n    const timestampMs = Number(fs.readFileSync(this.cacheAtPath));\n    return now - timestampMs > MAX_STALE;\n  }\n\n  // This function fetches the models from the ApiPie API and caches them locally.\n  // We do this because the ApiPie API has a lot of models, and we need to get the proper token context window\n  // for each model and this is a constructor property - so we can really only get it if this cache exists.\n  // We used to have this as a chore, but given there is an API to get the info - this makes little sense.\n  // This might slow down the first request, but we need the proper token context window\n  // for each model and this is a constructor property - so we can really only get it if this cache exists.\n  async #syncModels() {\n    if (fs.existsSync(this.cacheModelPath) && !this.#cacheIsStale())\n      return false;\n\n    this.log(\"Model cache is not present or stale. Fetching from ApiPie API.\");\n    await fetchApiPieModels();\n    return;\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  models() {\n    if (!fs.existsSync(this.cacheModelPath)) return {};\n    return safeJsonParse(\n      fs.readFileSync(this.cacheModelPath, { encoding: \"utf-8\" }),\n      {}\n    );\n  }\n\n  chatModels() {\n    const allModels = this.models();\n    return Object.entries(allModels).reduce(\n      (chatModels, [modelId, modelInfo]) => {\n        // Filter for chat models\n        if (\n          modelInfo.subtype &&\n          (modelInfo.subtype.includes(\"chat\") ||\n            modelInfo.subtype.includes(\"chatx\"))\n        ) {\n          chatModels[modelId] = modelInfo;\n        }\n        return chatModels;\n      },\n      {}\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    const cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n    const availableModels = fs.existsSync(cacheModelPath)\n      ? safeJsonParse(\n          fs.readFileSync(cacheModelPath, { encoding: \"utf-8\" }),\n          {}\n        )\n      : {};\n    return availableModels[modelName]?.maxLength || 4096;\n  }\n\n  promptWindowLimit() {\n    const availableModels = this.chatModels();\n    return availableModels[this.model]?.maxLength || 4096;\n  }\n\n  async isValidChatCompletionModel(model = \"\") {\n    await this.#syncModels();\n    const availableModels = this.chatModels();\n    return availableModels.hasOwnProperty(model);\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"auto\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `ApiPie chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage?.prompt_tokens || 0,\n        completion_tokens: result.output.usage?.completion_tokens || 0,\n        total_tokens: result.output.usage?.total_tokens || 0,\n        outputTps:\n          (result.output.usage?.completion_tokens || 0) / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `ApiPie chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n\n      // Establish listener to early-abort a streaming response\n      // in case things go sideways or the user does not like the response.\n      // We preserve the generated text but continue as if chat was completed\n      // to preserve previously generated content.\n      const handleAbort = () => {\n        stream?.endMeasurement({\n          completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n        });\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      try {\n        for await (const chunk of stream) {\n          const message = chunk?.choices?.[0];\n          const token = message?.delta?.content;\n\n          if (token) {\n            fullText += token;\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: token,\n              close: false,\n              error: false,\n            });\n          }\n\n          if (message === undefined || message.finish_reason !== null) {\n            writeResponseChunk(response, {\n              uuid,\n              sources,\n              type: \"textResponseChunk\",\n              textResponse: \"\",\n              close: true,\n              error: false,\n            });\n            response.removeListener(\"close\", handleAbort);\n            stream?.endMeasurement({\n              completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n            });\n            resolve(fullText);\n          }\n        }\n      } catch (e) {\n        writeResponseChunk(response, {\n          uuid,\n          sources,\n          type: \"abort\",\n          textResponse: null,\n          close: true,\n          error: e.message,\n        });\n        response.removeListener(\"close\", handleAbort);\n        stream?.endMeasurement({\n          completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n        });\n        resolve(fullText);\n      }\n    });\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nasync function fetchApiPieModels(providedApiKey = null) {\n  const apiKey = providedApiKey || process.env.APIPIE_LLM_API_KEY || null;\n  return await fetch(`https://apipie.ai/v1/models`, {\n    method: \"GET\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),\n    },\n  })\n    .then((res) => res.json())\n    .then(({ data = [] }) => {\n      const models = {};\n      data.forEach((model) => {\n        models[`${model.provider}/${model.model}`] = {\n          id: `${model.provider}/${model.model}`,\n          name: `${model.provider}/${model.model}`,\n          organization: model.provider,\n          subtype: model.subtype,\n          maxLength: model.max_tokens,\n        };\n      });\n\n      // Cache all response information\n      if (!fs.existsSync(cacheFolder))\n        fs.mkdirSync(cacheFolder, { recursive: true });\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \"models.json\"),\n        JSON.stringify(models),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \".cached_at\"),\n        String(Number(new Date())),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n\n      return models;\n    })\n    .catch((e) => {\n      console.error(e);\n      return {};\n    });\n}\n\nmodule.exports = {\n  ApiPieLLM,\n  fetchApiPieModels,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/azureOpenAi/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  formatChatHistory,\n  handleDefaultStreamResponseV2,\n} = require(\"../../helpers/chat/responses\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\n\nclass AzureOpenAiLLM {\n  constructor(embedder = null, modelPreference = null) {\n    const { OpenAI } = require(\"openai\");\n    if (!process.env.AZURE_OPENAI_ENDPOINT)\n      throw new Error(\"No Azure API endpoint was set.\");\n    if (!process.env.AZURE_OPENAI_KEY)\n      throw new Error(\"No Azure API key was set.\");\n\n    this.className = \"AzureOpenAiLLM\";\n    this.openai = new OpenAI({\n      apiKey: process.env.AZURE_OPENAI_KEY,\n      baseURL: AzureOpenAiLLM.formatBaseUrl(process.env.AZURE_OPENAI_ENDPOINT),\n    });\n    this.model =\n      modelPreference ||\n      process.env.AZURE_OPENAI_MODEL_PREF ||\n      process.env.OPEN_MODEL_PREF;\n    /* \n      Note: Azure OpenAI deployments do not expose model metadata that would allow us to\n      programmatically detect whether the deployment uses a reasoning model (o1, o1-mini, o3-mini, etc.).\n      As a result, we rely on the user to explicitly set AZURE_OPENAI_MODEL_TYPE=\"reasoning\"\n      when using reasoning models, as incorrect configuration might result in chat errors.\n    */\n    this.isOTypeModel =\n      process.env.AZURE_OPENAI_MODEL_TYPE === \"reasoning\" || false;\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.#log(\n      `Initialized. Model \"${this.model}\" @ ${this.promptWindowLimit()} tokens.\\nAPI-Version: ${this.apiVersion}.\\nModel Type: ${this.isOTypeModel ? \"reasoning\" : \"default\"}`\n    );\n  }\n\n  /**\n   * Formats the Azure OpenAI endpoint URL to the correct format.\n   * @param {string} azureOpenAiEndpoint - The Azure OpenAI endpoint URL.\n   * @returns {string} The formatted URL.\n   */\n  static formatBaseUrl(azureOpenAiEndpoint) {\n    try {\n      const url = new URL(azureOpenAiEndpoint);\n      url.pathname = \"/openai/v1\";\n      url.protocol = \"https\";\n      url.search = \"\";\n      url.hash = \"\";\n      return url.href;\n    } catch {\n      throw new Error(\n        `\"${azureOpenAiEndpoint}\" is not a valid URL. Check your settings for the Azure OpenAI provider and set a valid endpoint URL.`\n      );\n    }\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[32m[AzureOpenAi]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(_modelName) {\n    return !!process.env.AZURE_OPENAI_TOKEN_LIMIT\n      ? Number(process.env.AZURE_OPENAI_TOKEN_LIMIT)\n      : 4096;\n  }\n\n  // Sure the user selected a proper value for the token limit\n  // could be any of these https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#gpt-4-models\n  // and if undefined - assume it is the lowest end.\n  promptWindowLimit() {\n    return !!process.env.AZURE_OPENAI_TOKEN_LIMIT\n      ? Number(process.env.AZURE_OPENAI_TOKEN_LIMIT)\n      : 4096;\n  }\n\n  isValidChatCompletionModel(_modelName = \"\") {\n    // The Azure user names their \"models\" as deployments and they can be any name\n    // so we rely on the user to put in the correct deployment as only they would\n    // know it.\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [], // This is the specific attachment for only this prompt\n  }) {\n    const prompt = {\n      role: this.isOTypeModel ? \"user\" : \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = [], { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        \"No AZURE_OPENAI_MODEL_PREF ENV defined. This must the name of a deployment on your Azure account for an LLM chat model like GPT-3.5.\"\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions.create({\n        messages,\n        model: this.model,\n        ...(this.isOTypeModel ? {} : { temperature }),\n      })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = [], { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        \"No AZURE_OPENAI_MODEL_PREF ENV defined. This must the name of a deployment on your Azure account for an LLM chat model like GPT-3.5.\"\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: await this.openai.chat.completions.create({\n        messages,\n        model: this.model,\n        ...(this.isOTypeModel ? {} : { temperature }),\n        n: 1,\n        stream: true,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  AzureOpenAiLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/bedrock/index.js",
    "content": "const {\n  ConverseCommand,\n  ConverseStreamCommand,\n} = require(\"@aws-sdk/client-bedrock-runtime\");\nconst {\n  writeResponseChunk,\n  clientAbortedHandler,\n} = require(\"../../helpers/chat/responses\");\nconst { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst {\n  DEFAULT_MAX_OUTPUT_TOKENS,\n  DEFAULT_CONTEXT_WINDOW_TOKENS,\n  getImageFormatFromMime,\n  base64ToUint8Array,\n  createBedrockCredentials,\n  createBedrockRuntimeClient,\n  getBedrockAuthMethod,\n} = require(\"./utils\");\n\nclass AWSBedrockLLM {\n  /**\n   * List of Bedrock models observed to not support system prompts when using the Converse API.\n   * @type {string[]}\n   */\n  noSystemPromptModels = [\n    \"amazon.titan-text-express-v1\",\n    \"amazon.titan-text-lite-v1\",\n    \"cohere.command-text-v14\",\n    \"cohere.command-light-text-v14\",\n    \"us.deepseek.r1-v1:0\",\n    // Add other models here if identified\n  ];\n\n  /**\n   * Initializes the AWS Bedrock LLM connector.\n   * @param {object | null} [embedder=null] - An optional embedder instance. Defaults to NativeEmbedder.\n   * @param {string | null} [modelPreference=null] - Optional model ID override. Defaults to environment variable.\n   * @throws {Error} If required environment variables are missing or invalid.\n   */\n  constructor(embedder = null, modelPreference = null) {\n    const requiredEnvVars = [\n      ...(![\"iam_role\", \"apiKey\"].includes(this.authMethod)\n        ? [\n            // required for iam and sessionToken\n            \"AWS_BEDROCK_LLM_ACCESS_KEY_ID\",\n            \"AWS_BEDROCK_LLM_ACCESS_KEY\",\n          ]\n        : []),\n      ...(this.authMethod === \"sessionToken\"\n        ? [\n            // required for sessionToken\n            \"AWS_BEDROCK_LLM_SESSION_TOKEN\",\n          ]\n        : []),\n      ...(this.authMethod === \"apiKey\"\n        ? [\n            // required for bedrock api key\n            \"AWS_BEDROCK_LLM_API_KEY\",\n          ]\n        : []),\n      \"AWS_BEDROCK_LLM_REGION\",\n      \"AWS_BEDROCK_LLM_MODEL_PREFERENCE\",\n    ];\n\n    // Validate required environment variables\n    for (const envVar of requiredEnvVars) {\n      if (!process.env[envVar])\n        throw new Error(`Required environment variable ${envVar} is not set.`);\n    }\n\n    this.className = \"AWSBedrockLLM\";\n    this.model =\n      modelPreference || process.env.AWS_BEDROCK_LLM_MODEL_PREFERENCE;\n\n    const contextWindowLimit = this.promptWindowLimit();\n    this.limits = {\n      history: Math.floor(contextWindowLimit * 0.15),\n      system: Math.floor(contextWindowLimit * 0.15),\n      user: Math.floor(contextWindowLimit * 0.7),\n    };\n\n    this.bedrockClient = createBedrockRuntimeClient(\n      this.authMethod,\n      this.credentials\n    );\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.#log(\n      `Initialized with model: ${this.model}. Auth: ${this.authMethod}. Context Window: ${contextWindowLimit}.`\n    );\n  }\n\n  /**\n   * Gets the credentials for the AWS Bedrock LLM based on the authentication method provided.\n   * @returns {object} The credentials object.\n   */\n  get credentials() {\n    return createBedrockCredentials(this.authMethod);\n  }\n\n  /**\n   * Gets the configured AWS authentication method ('iam' or 'sessionToken').\n   * Defaults to 'iam' if the environment variable is invalid.\n   * @returns {\"iam\" | \"iam_role\" | \"sessionToken\"} The authentication method.\n   */\n  get authMethod() {\n    return getBedrockAuthMethod();\n  }\n\n  /**\n   * Appends context texts to a string with standard formatting.\n   * @param {string[]} contextTexts - An array of context text snippets.\n   * @returns {string} Formatted context string or empty string if no context provided.\n   * @private\n   */\n  #appendContext(contextTexts = []) {\n    if (!contextTexts?.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`)\n        .join(\"\")\n    );\n  }\n\n  /**\n   * Internal logging helper with provider prefix.\n   * @param {string} text - The log message.\n   * @param  {...any} args - Additional arguments to log.\n   * @private\n   */\n  #log(text, ...args) {\n    console.log(`\\x1b[32m[AWSBedrock]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Internal logging helper with provider prefix for static methods.\n   * @private\n   */\n  static #slog(text, ...args) {\n    console.log(`\\x1b[32m[AWSBedrock]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Some Bedrock models (Titan, Cohere) don't support streaming.\n   * Set AWS_BEDROCK_STREAMING_DISABLED to any value to disable streaming for those models.\n   * Since this can be any model even custom models we leave it to the user to disable streaming if needed.\n   * @returns {boolean} True if streaming is supported, false otherwise.\n   */\n  streamingEnabled() {\n    if (!!process.env.AWS_BEDROCK_STREAMING_DISABLED) return false;\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  /**\n   * @static\n   * Gets the total prompt window limit (total context window: input + output) from the environment variable.\n   * This value is used for calculating input limits, NOT for setting the max output tokens in API calls.\n   * @returns {number} The total context window token limit. Defaults to 8191.\n   */\n  static promptWindowLimit() {\n    const limit =\n      process.env.AWS_BEDROCK_LLM_MODEL_TOKEN_LIMIT ??\n      DEFAULT_CONTEXT_WINDOW_TOKENS;\n    const numericLimit = Number(limit);\n    if (isNaN(numericLimit) || numericLimit <= 0) {\n      this.#slog(\n        `[AWSBedrock ERROR] Invalid AWS_BEDROCK_LLM_MODEL_TOKEN_LIMIT found: \"${limit}\". Must be a positive number - returning default ${DEFAULT_CONTEXT_WINDOW_TOKENS}.`\n      );\n      return DEFAULT_CONTEXT_WINDOW_TOKENS;\n    }\n    return numericLimit;\n  }\n\n  /**\n   * Gets the total prompt window limit (total context window) for the current model instance.\n   * @returns {number} The token limit.\n   */\n  promptWindowLimit() {\n    return AWSBedrockLLM.promptWindowLimit();\n  }\n\n  /**\n   * Gets the maximum number of tokens the model should generate in its response.\n   * Reads from the AWS_BEDROCK_LLM_MAX_OUTPUT_TOKENS environment variable or uses a default.\n   * This is distinct from the total context window limit.\n   * @returns {number} The maximum output tokens limit for API calls.\n   */\n  getMaxOutputTokens() {\n    const outputLimitSource = process.env.AWS_BEDROCK_LLM_MAX_OUTPUT_TOKENS;\n    if (isNaN(Number(outputLimitSource))) {\n      this.#log(\n        `[AWSBedrock ERROR] Invalid AWS_BEDROCK_LLM_MAX_OUTPUT_TOKENS found: \"${outputLimitSource}\". Must be a positive number - returning default ${DEFAULT_MAX_OUTPUT_TOKENS}.`\n      );\n      return DEFAULT_MAX_OUTPUT_TOKENS;\n    }\n\n    const numericOutputLimit = Number(outputLimitSource);\n    if (numericOutputLimit <= 0) {\n      this.#log(\n        `[AWSBedrock ERROR] Invalid AWS_BEDROCK_LLM_MAX_OUTPUT_TOKENS found: \"${outputLimitSource}\". Must be a greater than 0 - returning default ${DEFAULT_MAX_OUTPUT_TOKENS}.`\n      );\n      return DEFAULT_MAX_OUTPUT_TOKENS;\n    }\n\n    return numericOutputLimit;\n  }\n\n  /** Stubbed method for compatibility with LLM interface. */\n  async isValidChatCompletionModel(_modelName = \"\") {\n    return true;\n  }\n\n  /**\n   * Validates attachments array and returns a new array with valid attachments.\n   * @param {Array<{contentString: string, mime: string}>} attachments - Array of attachments.\n   * @returns {Array<{image: {format: string, source: {bytes: Uint8Array}}>} Array of valid attachments.\n   * @private\n   */\n  #validateAttachments(attachments = []) {\n    if (!Array.isArray(attachments) || !attachments?.length) return [];\n    const validAttachments = [];\n    for (const attachment of attachments) {\n      if (\n        !attachment ||\n        typeof attachment.mime !== \"string\" ||\n        typeof attachment.contentString !== \"string\"\n      ) {\n        this.#log(\"Skipping invalid attachment object.\", attachment);\n        continue;\n      }\n\n      // Strip data URI prefix (e.g., \"data:image/png;base64,\")\n      const base64Data = attachment.contentString.replace(\n        /^data:image\\/\\w+;base64,/,\n        \"\"\n      );\n\n      const format = getImageFormatFromMime(attachment.mime);\n      const attachmentInfo = {\n        valid: format !== null,\n        format,\n        imageBytes: base64ToUint8Array(base64Data),\n      };\n\n      if (!attachmentInfo.valid) {\n        this.#log(\n          `Skipping attachment with unsupported/invalid MIME type: ${attachment.mime}`\n        );\n        continue;\n      }\n\n      validAttachments.push({\n        image: {\n          format: format,\n          source: { bytes: attachmentInfo.imageBytes },\n        },\n      });\n    }\n\n    return validAttachments;\n  }\n\n  /**\n   * Generates the Bedrock Converse API content array for a message,\n   * processing text and formatting valid image attachments.\n   * @param {object} params\n   * @param {string} params.userPrompt - The text part of the message.\n   * @param {Array<{contentString: string, mime: string}>} params.attachments - Array of attachments for the message.\n   * @returns {Array<object>} Array of content blocks (e.g., [{text: \"...\"}, {image: {...}}]).\n   * @private\n   */\n  #generateContent({ userPrompt = \"\", attachments = [] }) {\n    const content = [];\n    // Add text block if prompt is not empty\n    if (userPrompt?.trim()?.length) content.push({ text: userPrompt });\n\n    // Validate attachments and add valid attachments to content\n    const validAttachments = this.#validateAttachments(attachments);\n    if (validAttachments?.length) content.push(...validAttachments);\n\n    // Ensure content array is never empty (Bedrock requires at least one block)\n    if (content.length === 0) content.push({ text: \"\" });\n    return content;\n  }\n\n  /**\n   * Constructs the complete message array in the format expected by the Bedrock Converse API.\n   * @param {object} params\n   * @param {string} params.systemPrompt - The system prompt text.\n   * @param {string[]} params.contextTexts - Array of context text snippets.\n   * @param {Array<{role: 'user' | 'assistant', content: string, attachments?: Array<{contentString: string, mime: string}>}>} params.chatHistory - Previous messages.\n   * @param {string} params.userPrompt - The latest user prompt text.\n   * @param {Array<{contentString: string, mime: string}>} params.attachments - Attachments for the latest user prompt.\n   * @returns {Array<object>} The formatted message array for the API call.\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const systemMessageContent = `${systemPrompt}${this.#appendContext(contextTexts)}`;\n    let messages = [];\n\n    // Handle system prompt (either real or simulated)\n    if (this.noSystemPromptModels.includes(this.model)) {\n      if (systemMessageContent.trim().length > 0) {\n        this.#log(\n          `Model ${this.model} doesn't support system prompts; simulating.`\n        );\n        messages.push(\n          {\n            role: \"user\",\n            content: this.#generateContent({\n              userPrompt: systemMessageContent,\n            }),\n          },\n          { role: \"assistant\", content: [{ text: \"Okay.\" }] }\n        );\n      }\n    } else if (systemMessageContent.trim().length > 0) {\n      messages.push({\n        role: \"system\",\n        content: this.#generateContent({ userPrompt: systemMessageContent }),\n      });\n    }\n\n    // Add chat history\n    messages = messages.concat(\n      chatHistory.map((msg) => ({\n        role: msg.role,\n        content: this.#generateContent({\n          userPrompt: msg.content,\n          attachments: Array.isArray(msg.attachments) ? msg.attachments : [],\n        }),\n      }))\n    );\n\n    // Add final user prompt\n    messages.push({\n      role: \"user\",\n      content: this.#generateContent({\n        userPrompt: userPrompt,\n        attachments: Array.isArray(attachments) ? attachments : [],\n      }),\n    });\n\n    return messages;\n  }\n\n  /**\n   * Parses reasoning steps from the response and prepends them in <think> tags.\n   * @param {object} message - The message object from the Bedrock response.\n   * @returns {string} The text response, potentially with reasoning prepended.\n   * @private\n   */\n  #parseReasoningFromResponse({ content = [] }) {\n    if (!content?.length) return \"\";\n\n    // Find the text block and grab the text\n    const textBlock = content.find((block) => block.text !== undefined);\n    let textResponse = textBlock?.text || \"\";\n\n    // Find the reasoning block and grab the reasoning text\n    const reasoningBlock = content.find(\n      (block) => block.reasoningContent?.reasoningText?.text\n    );\n    if (reasoningBlock) {\n      const reasoningText =\n        reasoningBlock.reasoningContent.reasoningText.text.trim();\n      if (reasoningText?.length)\n        textResponse = `<think>${reasoningText}</think>${textResponse}`;\n    }\n    return textResponse;\n  }\n\n  /**\n   * Sends a request for chat completion (non-streaming).\n   * @param {Array<object> | null} messages - Formatted message array from constructPrompt.\n   * @param {object} options - Request options.\n   * @param {number} options.temperature - Sampling temperature.\n   * @returns {Promise<object | null>} Response object with textResponse and metrics, or null.\n   * @throws {Error} If the API call fails or validation errors occur.\n   */\n  async getChatCompletion(messages = null, { temperature }) {\n    if (!messages?.length)\n      throw new Error(\n        \"AWSBedrock::getChatCompletion requires a non-empty messages array.\"\n      );\n\n    const hasSystem = messages[0]?.role === \"system\";\n    const systemBlock = hasSystem ? messages[0].content : undefined;\n    const history = hasSystem ? messages.slice(1) : messages;\n    const maxTokensToSend = this.getMaxOutputTokens();\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.bedrockClient\n        .send(\n          new ConverseCommand({\n            modelId: this.model,\n            messages: history,\n            inferenceConfig: {\n              maxTokens: maxTokensToSend,\n              temperature: temperature ?? this.defaultTemp,\n            },\n            system: systemBlock,\n          })\n        )\n        .catch((e) => {\n          this.#log(\n            `Bedrock Converse API Error (getChatCompletion): ${e.message}`,\n            e\n          );\n          AWSBedrockLLM.errorToHumanReadable(e, {\n            model: this.model,\n            maxTokens: maxTokensToSend,\n            method: \"getChatCompletion\",\n          });\n        })\n    );\n\n    const response = result.output;\n    if (!response?.output?.message) {\n      this.#log(\n        \"Bedrock response missing expected output.message structure.\",\n        response\n      );\n      return null;\n    }\n\n    const latencyMs = response?.metrics?.latencyMs;\n    const outputTokens = response?.usage?.outputTokens;\n    const outputTps =\n      latencyMs > 0 && outputTokens ? outputTokens / (latencyMs / 1000) : 0;\n\n    return {\n      textResponse: this.#parseReasoningFromResponse(response.output.message),\n      metrics: {\n        prompt_tokens: response?.usage?.inputTokens ?? 0,\n        completion_tokens: outputTokens ?? 0,\n        total_tokens: response?.usage?.totalTokens ?? 0,\n        outputTps: outputTps,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  /**\n   * Sends a request for streaming chat completion.\n   * @param {Array<object> | null} messages - Formatted message array from constructPrompt.\n   * @param {object} options - Request options.\n   * @param {number} [options.temperature] - Sampling temperature.\n   * @returns {Promise<import('../../helpers/chat/LLMPerformanceMonitor').MonitoredStream>} The monitored stream object.\n   * @throws {Error} If the API call setup fails or validation errors occur.\n   */\n  async streamGetChatCompletion(messages = null, { temperature }) {\n    if (!Array.isArray(messages) || messages.length === 0) {\n      throw new Error(\n        \"AWSBedrock::streamGetChatCompletion requires a non-empty messages array.\"\n      );\n    }\n\n    const hasSystem = messages[0]?.role === \"system\";\n    const systemBlock = hasSystem ? messages[0].content : undefined;\n    const history = hasSystem ? messages.slice(1) : messages;\n    const maxTokensToSend = this.getMaxOutputTokens();\n\n    try {\n      // Attempt to initiate the stream\n      const stream = await this.bedrockClient.send(\n        new ConverseStreamCommand({\n          modelId: this.model,\n          messages: history,\n          inferenceConfig: {\n            maxTokens: maxTokensToSend,\n            temperature: temperature ?? this.defaultTemp,\n          },\n          system: systemBlock,\n        })\n      );\n\n      // If successful, wrap the stream with performance monitoring\n      const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n        func: stream,\n        messages,\n        runPromptTokenCalculation: false,\n        modelTag: this.model,\n        provider: this.className,\n      });\n      return measuredStreamRequest;\n    } catch (e) {\n      // Catch errors during the initial .send() call (e.g., validation errors)\n      this.#log(\n        `Bedrock Converse API Error (streamGetChatCompletion setup): ${e.message}`,\n        e\n      );\n      AWSBedrockLLM.errorToHumanReadable(e, {\n        model: this.model,\n        maxTokens: maxTokensToSend,\n        method: \"streamGetChatCompletion\",\n      });\n    }\n  }\n\n  /**\n   * Handles the stream response from the AWS Bedrock API ConverseStreamCommand.\n   * Parses chunks, handles reasoning tags, and estimates token usage if not provided.\n   * @param {object} response - The HTTP response object to write chunks to.\n   * @param {import('../../helpers/chat/LLMPerformanceMonitor').MonitoredStream} stream - The monitored stream object from streamGetChatCompletion.\n   * @param {object} responseProps - Additional properties for the response chunks.\n   * @param {string} responseProps.uuid - Unique ID for the response.\n   * @param {Array} responseProps.sources - Source documents used (if any).\n   * @returns {Promise<string>} A promise that resolves with the complete text response when the stream ends.\n   */\n  handleStream(response, stream, responseProps) {\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n    let hasUsageMetrics = false;\n    let usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n      let reasoningText = \"\";\n\n      // Abort handler for client closing connection\n      const handleAbort = () => {\n        this.#log(`Client closed connection for stream ${uuid}. Aborting.`);\n        stream?.endMeasurement(usage); // Finalize metrics\n        clientAbortedHandler(resolve, fullText); // Resolve with partial text\n      };\n      response.on(\"close\", handleAbort);\n\n      try {\n        // Process stream chunks\n        for await (const chunk of stream.stream) {\n          if (!chunk) {\n            this.#log(\"Stream returned null/undefined chunk.\");\n            continue;\n          }\n          const action = Object.keys(chunk)[0];\n\n          switch (action) {\n            case \"metadata\": // Contains usage metrics at the end\n              if (chunk.metadata?.usage) {\n                hasUsageMetrics = true;\n                usage = {\n                  // Overwrite with final metrics\n                  prompt_tokens: chunk.metadata.usage.inputTokens ?? 0,\n                  completion_tokens: chunk.metadata.usage.outputTokens ?? 0,\n                  total_tokens: chunk.metadata.usage.totalTokens ?? 0,\n                };\n              }\n              break;\n            case \"contentBlockDelta\": {\n              // Contains text or reasoning deltas\n              const delta = chunk.contentBlockDelta?.delta;\n              if (!delta) break;\n              const token = delta.text;\n              const reasoningToken = delta.reasoningContent?.text;\n\n              if (reasoningToken) {\n                // Handle reasoning text\n                if (reasoningText.length === 0) {\n                  // Start of reasoning block\n                  const startTag = \"<think>\";\n                  writeResponseChunk(response, {\n                    uuid,\n                    sources,\n                    type: \"textResponseChunk\",\n                    textResponse: startTag + reasoningToken,\n                    close: false,\n                    error: false,\n                  });\n                  reasoningText += startTag + reasoningToken;\n                } else {\n                  // Continuation of reasoning block\n                  writeResponseChunk(response, {\n                    uuid,\n                    sources,\n                    type: \"textResponseChunk\",\n                    textResponse: reasoningToken,\n                    close: false,\n                    error: false,\n                  });\n                  reasoningText += reasoningToken;\n                }\n              } else if (token) {\n                // Handle regular text\n                if (reasoningText.length > 0) {\n                  // If reasoning was just output, close the tag\n                  const endTag = \"</think>\";\n                  writeResponseChunk(response, {\n                    uuid,\n                    sources,\n                    type: \"textResponseChunk\",\n                    textResponse: endTag,\n                    close: false,\n                    error: false,\n                  });\n                  fullText += reasoningText + endTag; // Add completed reasoning to final text\n                  reasoningText = \"\"; // Reset reasoning buffer\n                }\n                fullText += token; // Append regular text\n                if (!hasUsageMetrics) usage.completion_tokens++; // Estimate usage if no metrics yet\n                writeResponseChunk(response, {\n                  uuid,\n                  sources,\n                  type: \"textResponseChunk\",\n                  textResponse: token,\n                  close: false,\n                  error: false,\n                });\n              }\n              break;\n            }\n            case \"messageStop\": // End of message event\n              if (chunk.messageStop?.usage) {\n                // Check for final metrics here too\n                hasUsageMetrics = true;\n                usage = {\n                  // Overwrite with final metrics if available\n                  prompt_tokens:\n                    chunk.messageStop.usage.inputTokens ?? usage.prompt_tokens,\n                  completion_tokens:\n                    chunk.messageStop.usage.outputTokens ??\n                    usage.completion_tokens,\n                  total_tokens:\n                    chunk.messageStop.usage.totalTokens ?? usage.total_tokens,\n                };\n              }\n              // Ensure reasoning tag is closed if message stops mid-reasoning\n              if (reasoningText.length > 0) {\n                const endTag = \"</think>\";\n                writeResponseChunk(response, {\n                  uuid,\n                  sources,\n                  type: \"textResponseChunk\",\n                  textResponse: endTag,\n                  close: false,\n                  error: false,\n                });\n                fullText += reasoningText + endTag;\n                reasoningText = \"\";\n              }\n              break;\n            // Ignore other event types for now\n            case \"messageStart\":\n            case \"contentBlockStart\":\n            case \"contentBlockStop\":\n              break;\n            default:\n              this.#log(`Unhandled stream action: ${action}`, chunk);\n          }\n        } // End for await loop\n\n        // Final cleanup for reasoning tag in case stream ended abruptly\n        if (reasoningText.length > 0 && !fullText.endsWith(\"</think>\")) {\n          const endTag = \"</think>\";\n          if (!response.writableEnded) {\n            writeResponseChunk(response, {\n              uuid,\n              sources,\n              type: \"textResponseChunk\",\n              textResponse: endTag,\n              close: false,\n              error: false,\n            });\n          }\n          fullText += reasoningText + endTag;\n        }\n\n        // Send final closing chunk to signal end of stream\n        if (!response.writableEnded) {\n          writeResponseChunk(response, {\n            uuid,\n            sources,\n            type: \"textResponseChunk\",\n            textResponse: \"\",\n            close: true,\n            error: false,\n          });\n        }\n      } catch (error) {\n        // Handle errors during stream processing\n        this.#log(\n          `\\x1b[43m\\x1b[34m[STREAMING ERROR]\\x1b[0m ${error.message}`,\n          error\n        );\n        if (response && !response.writableEnded) {\n          writeResponseChunk(response, {\n            uuid,\n            type: \"abort\",\n            textResponse: null,\n            sources,\n            close: true,\n            error: `AWSBedrock:streaming - error. ${\n              error?.message ?? \"Unknown error\"\n            }`,\n          });\n        }\n      } finally {\n        response.removeListener(\"close\", handleAbort);\n        stream?.endMeasurement(usage);\n        resolve(fullText); // Resolve with the accumulated text\n      }\n    });\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n\n  static errorToHumanReadable(\n    error,\n    options = { method: \"chat\", model: \"unknown\", maxTokens: \"unknown\" }\n  ) {\n    if (\n      error.name === \"ValidationException\" &&\n      error.message.includes(\"maximum tokens\")\n    ) {\n      throw new Error(\n        `AWSBedrock::${options.method} failed during setup. Model ${options.model} rejected maxTokens value of ${options.maxTokens}. Check model documentation for its maximum output token limit and set AWS_BEDROCK_LLM_MAX_OUTPUT_TOKENS if needed. Original error: ${error.message}`\n      );\n    }\n\n    if (\n      error.name === \"CredentialsProviderError\" &&\n      error.message.includes(\"Could not load credentials from any providers\")\n    ) {\n      throw new Error(\n        `AWSBedrock::${options.method} authentication failed. AWS Bedrock requires a discoverable IAM credentials to be available in the environment (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY) or by resolving credentials from ~/.aws/credentials or ~/.aws/config files. Original error: ${error.message}`\n      );\n    }\n\n    // Generic error\n    throw new Error(\n      `AWSBedrock::${options.method} failed during setup. ${error.message}`\n    );\n  }\n}\n\nmodule.exports = {\n  AWSBedrockLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/bedrock/utils.js",
    "content": "const { BedrockRuntimeClient } = require(\"@aws-sdk/client-bedrock-runtime\");\nconst { fromStatic } = require(\"@aws-sdk/token-providers\");\nconst { ChatBedrockConverse } = require(\"@langchain/aws\");\n\n/** @typedef {'jpeg' | 'png' | 'gif' | 'webp'} */\nconst SUPPORTED_BEDROCK_IMAGE_FORMATS = [\"jpeg\", \"png\", \"gif\", \"webp\"];\n\n/** @type {number} */\nconst DEFAULT_MAX_OUTPUT_TOKENS = 4096;\n\n/** @type {number} */\nconst DEFAULT_CONTEXT_WINDOW_TOKENS = 8191;\n\n/** @type {'iam' | 'iam_role' | 'sessionToken' | 'apiKey'} */\nconst SUPPORTED_CONNECTION_METHODS = [\n  \"iam\",\n  \"iam_role\",\n  \"sessionToken\",\n  \"apiKey\",\n];\n\n/**\n * Gets the AWS Bedrock authentication method from the environment variables.\n * @returns {\"iam\" | \"iam_role\" | \"sessionToken\" | \"apiKey\"} The authentication method.\n */\nfunction getBedrockAuthMethod() {\n  const method = process.env.AWS_BEDROCK_LLM_CONNECTION_METHOD || \"iam\";\n  return SUPPORTED_CONNECTION_METHODS.includes(method) ? method : \"iam\";\n}\n\n/**\n * Creates the AWS Bedrock credentials object based on the authentication method.\n * @param {\"iam\" | \"iam_role\" | \"sessionToken\" | \"apiKey\"} authMethod - The authentication method.\n * @returns {object | undefined} The credentials object.\n */\nfunction createBedrockCredentials(authMethod) {\n  switch (authMethod) {\n    case \"iam\": // explicit credentials\n      return {\n        accessKeyId: process.env.AWS_BEDROCK_LLM_ACCESS_KEY_ID,\n        secretAccessKey: process.env.AWS_BEDROCK_LLM_ACCESS_KEY,\n      };\n    case \"sessionToken\": // Session token is used for temporary credentials\n      return {\n        accessKeyId: process.env.AWS_BEDROCK_LLM_ACCESS_KEY_ID,\n        secretAccessKey: process.env.AWS_BEDROCK_LLM_ACCESS_KEY,\n        sessionToken: process.env.AWS_BEDROCK_LLM_SESSION_TOKEN,\n      };\n    // IAM role is used for long-term credentials implied by system process\n    // is filled by the AWS SDK automatically if we pass in no credentials\n    // returning undefined will allow this to happen\n    case \"iam_role\":\n      return undefined;\n    case \"apiKey\":\n      return fromStatic({\n        token: { token: process.env.AWS_BEDROCK_LLM_API_KEY },\n      });\n    default:\n      return undefined;\n  }\n}\n\n/**\n * Creates the AWS Bedrock runtime client based on the authentication method.\n * @param {\"iam\" | \"iam_role\" | \"sessionToken\" | \"apiKey\"} authMethod - The authentication method.\n * @param {object | undefined} credentials - The credentials object.\n * @returns {BedrockRuntimeClient} The runtime client.\n */\nfunction createBedrockRuntimeClient(authMethod, credentials) {\n  const clientOpts = {\n    region: process.env.AWS_BEDROCK_LLM_REGION,\n  };\n\n  switch (authMethod) {\n    case \"apiKey\":\n      clientOpts.token = credentials;\n      clientOpts.authSchemePreference = [\"httpBearerAuth\"];\n      break;\n    default:\n      clientOpts.credentials = credentials;\n      break;\n  }\n  return new BedrockRuntimeClient(clientOpts);\n}\n\n/**\n * Creates the AWS Bedrock chat client based on the authentication method.\n * Used explicitly by the agent provider for the AWS Bedrock provider.\n * @param {object} config - The configuration object.\n * @param {\"iam\" | \"iam_role\" | \"sessionToken\" | \"apiKey\"} authMethod - The authentication method.\n * @param {object | undefined} credentials - The credentials object.\n * @param {string | null} model - The model to use.\n * @returns {ChatBedrockConverse} The chat client.\n */\nfunction createBedrockChatClient(config = {}, authMethod, credentials, model) {\n  authMethod ||= getBedrockAuthMethod();\n  credentials ||= createBedrockCredentials(authMethod);\n  model ||= process.env.AWS_BEDROCK_LLM_MODEL_PREFERENCE ?? null;\n  const client = createBedrockRuntimeClient(authMethod, credentials);\n  return new ChatBedrockConverse({\n    region: process.env.AWS_BEDROCK_LLM_REGION,\n    client,\n    model,\n    ...config,\n  });\n}\n\n/**\n * Parses a MIME type string (e.g., \"image/jpeg\") to extract and validate the image format\n * supported by Bedrock Converse. Handles 'image/jpg' as 'jpeg'.\n * @param {string | null | undefined} mimeType - The MIME type string.\n * @returns {string | null} The validated image format (e.g., \"jpeg\") or null if invalid/unsupported.\n */\nfunction getImageFormatFromMime(mimeType = \"\") {\n  if (!mimeType) return null;\n  const parts = mimeType.toLowerCase().split(\"/\");\n  if (parts?.[0] !== \"image\") return null;\n  let format = parts?.[1];\n  if (!format) return null;\n\n  // Remap jpg to jpeg\n  switch (format) {\n    case \"jpg\":\n      format = \"jpeg\";\n      break;\n    default:\n      break;\n  }\n\n  if (!SUPPORTED_BEDROCK_IMAGE_FORMATS.includes(format)) return null;\n  return format;\n}\n\n/**\n * Decodes a pure base64 string (without data URI prefix) into a Uint8Array using the atob method.\n * This approach matches the technique previously used by Langchain's implementation.\n * @param {string} base64String - The pure base64 encoded data.\n * @returns {Uint8Array | null} The resulting byte array or null on decoding error.\n */\nfunction base64ToUint8Array(base64String) {\n  try {\n    const binaryString = atob(base64String);\n    const len = binaryString.length;\n    const bytes = new Uint8Array(len);\n    for (let i = 0; i < len; i++) bytes[i] = binaryString.charCodeAt(i);\n    return bytes;\n  } catch (e) {\n    console.error(\n      `[AWSBedrock] Error decoding base64 string with atob: ${e.message}`\n    );\n    return null;\n  }\n}\n\nmodule.exports = {\n  SUPPORTED_CONNECTION_METHODS,\n  SUPPORTED_BEDROCK_IMAGE_FORMATS,\n  DEFAULT_MAX_OUTPUT_TOKENS,\n  DEFAULT_CONTEXT_WINDOW_TOKENS,\n  getImageFormatFromMime,\n  base64ToUint8Array,\n  getBedrockAuthMethod,\n  createBedrockCredentials,\n  createBedrockRuntimeClient,\n  createBedrockChatClient,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/cohere/index.js",
    "content": "const { v4 } = require(\"uuid\");\nconst { writeResponseChunk } = require(\"../../helpers/chat/responses\");\nconst { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst { MODEL_MAP } = require(\"../modelMap\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\n\nclass CohereLLM {\n  constructor(embedder = null) {\n    this.className = \"CohereLLM\";\n    const { CohereClient } = require(\"cohere-ai\");\n    if (!process.env.COHERE_API_KEY)\n      throw new Error(\"No Cohere API key was set.\");\n\n    const cohere = new CohereClient({\n      token: process.env.COHERE_API_KEY,\n    });\n\n    this.cohere = cohere;\n    this.model = process.env.COHERE_MODEL_PREF;\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.#log(\n      `Initialized with model ${this.model}. ctx: ${this.promptWindowLimit()}`\n    );\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[32m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  #convertChatHistoryCohere(chatHistory = []) {\n    let cohereHistory = [];\n    chatHistory.forEach((message) => {\n      switch (message.role) {\n        case \"system\":\n          cohereHistory.push({ role: \"SYSTEM\", message: message.content });\n          break;\n        case \"user\":\n          cohereHistory.push({ role: \"USER\", message: message.content });\n          break;\n        case \"assistant\":\n          cohereHistory.push({ role: \"CHATBOT\", message: message.content });\n          break;\n      }\n    });\n\n    return cohereHistory;\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    return MODEL_MAP.get(\"cohere\", modelName) ?? 4_096;\n  }\n\n  promptWindowLimit() {\n    return MODEL_MAP.get(\"cohere\", this.model) ?? 4_096;\n  }\n\n  async isValidChatCompletionModel() {\n    return true;\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [prompt, ...chatHistory, { role: \"user\", content: userPrompt }];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    const message = messages[messages.length - 1].content; // Get the last message\n    const cohereHistory = this.#convertChatHistoryCohere(messages.slice(0, -1)); // Remove the last message and convert to Cohere\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.cohere.chat({\n        model: this.model,\n        message: message,\n        chatHistory: cohereHistory,\n        temperature,\n      })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"text\") ||\n      result.output.text.length === 0\n    )\n      return null;\n\n    const promptTokens = result.output.meta?.tokens?.inputTokens || 0;\n    const completionTokens = result.output.meta?.tokens?.outputTokens || 0;\n    return {\n      textResponse: result.output.text,\n      metrics: {\n        prompt_tokens: promptTokens,\n        completion_tokens: completionTokens,\n        total_tokens: promptTokens + completionTokens,\n        outputTps: completionTokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    const message = messages[messages.length - 1].content; // Get the last message\n    const cohereHistory = this.#convertChatHistoryCohere(messages.slice(0, -1)); // Remove the last message and convert to Cohere\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.cohere.chatStream({\n        model: this.model,\n        message: message,\n        chatHistory: cohereHistory,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  /**\n   * Handles the stream response from the Cohere API.\n   * @param {Object} response - the response object\n   * @param {import('../../helpers/chat/LLMPerformanceMonitor').MonitoredStream} stream - the stream response from the Cohere API w/tracking\n   * @param {Object} responseProps - the response properties\n   * @returns {Promise<string>}\n   */\n  async handleStream(response, stream, responseProps) {\n    return new Promise(async (resolve) => {\n      const { uuid = v4(), sources = [] } = responseProps;\n      let fullText = \"\";\n      let usage = {\n        prompt_tokens: 0,\n        completion_tokens: 0,\n      };\n\n      const handleAbort = () => {\n        writeResponseChunk(response, {\n          uuid,\n          sources,\n          type: \"abort\",\n          textResponse: fullText,\n          close: true,\n          error: false,\n        });\n        response.removeListener(\"close\", handleAbort);\n        stream.endMeasurement(usage);\n        resolve(fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      try {\n        for await (const chat of stream) {\n          if (chat.eventType === \"stream-end\") {\n            const usageMetrics = chat?.response?.meta?.tokens || {};\n            usage.prompt_tokens = usageMetrics.inputTokens || 0;\n            usage.completion_tokens = usageMetrics.outputTokens || 0;\n          }\n\n          if (chat.eventType === \"text-generation\") {\n            const text = chat.text;\n            fullText += text;\n\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: text,\n              close: false,\n              error: false,\n            });\n          }\n        }\n\n        writeResponseChunk(response, {\n          uuid,\n          sources,\n          type: \"textResponseChunk\",\n          textResponse: \"\",\n          close: true,\n          error: false,\n        });\n        response.removeListener(\"close\", handleAbort);\n        stream.endMeasurement(usage);\n        resolve(fullText);\n      } catch (error) {\n        writeResponseChunk(response, {\n          uuid,\n          sources,\n          type: \"abort\",\n          textResponse: null,\n          close: true,\n          error: error.message,\n        });\n        response.removeListener(\"close\", handleAbort);\n        stream.endMeasurement(usage);\n        resolve(fullText);\n      }\n    });\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  CohereLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/cometapi/constants.js",
    "content": "// TODO: When CometAPI's model list is upgraded, this operation needs to be removed\n// Model filtering patterns from cometapi.md that are not supported by AnythingLLM\nmodule.exports.COMETAPI_IGNORE_PATTERNS = [\n  // Image generation models\n  \"dall-e\",\n  \"dalle\",\n  \"midjourney\",\n  \"mj_\",\n  \"stable-diffusion\",\n  \"sd-\",\n  \"flux-\",\n  \"playground-v\",\n  \"ideogram\",\n  \"recraft-\",\n  \"black-forest-labs\",\n  \"/recraft-v3\",\n  \"recraftv3\",\n  \"stability-ai/\",\n  \"sdxl\",\n  // Audio generation models\n  \"suno_\",\n  \"tts\",\n  \"whisper\",\n  // Video generation models\n  \"runway\",\n  \"luma_\",\n  \"luma-\",\n  \"veo\",\n  \"kling_\",\n  \"minimax_video\",\n  \"hunyuan-t1\",\n  // Utility models\n  \"embedding\",\n  \"search-gpts\",\n  \"files_retrieve\",\n  \"moderation\",\n  // Deepl\n  \"deepl\",\n];\n"
  },
  {
    "path": "server/utils/AiProviders/cometapi/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst {\n  writeResponseChunk,\n  clientAbortedHandler,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { safeJsonParse } = require(\"../../http\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst { COMETAPI_IGNORE_PATTERNS } = require(\"./constants\");\nconst cacheFolder = path.resolve(\n  process.env.STORAGE_DIR\n    ? path.resolve(process.env.STORAGE_DIR, \"models\", \"cometapi\")\n    : path.resolve(__dirname, `../../../storage/models/cometapi`)\n);\n\nclass CometApiLLM {\n  defaultTimeout = 3_000;\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.COMETAPI_LLM_API_KEY)\n      throw new Error(\"No CometAPI API key was set.\");\n\n    this.className = \"CometApiLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.basePath = \"https://api.cometapi.com/v1\";\n    this.openai = new OpenAIApi({\n      baseURL: this.basePath,\n      apiKey: process.env.COMETAPI_LLM_API_KEY ?? null,\n      defaultHeaders: {\n        \"HTTP-Referer\": \"https://anythingllm.com\",\n        \"X-CometAPI-Source\": \"anythingllm\",\n      },\n    });\n    this.model =\n      modelPreference || process.env.COMETAPI_LLM_MODEL_PREF || \"gpt-5-mini\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.timeout = this.#parseTimeout();\n\n    if (!fs.existsSync(cacheFolder))\n      fs.mkdirSync(cacheFolder, { recursive: true });\n    this.cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n    this.cacheAtPath = path.resolve(cacheFolder, \".cached_at\");\n\n    this.log(`Loaded with model: ${this.model}`);\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * CometAPI has various models that never return `finish_reasons` and thus leave the stream open\n   * which causes issues in subsequent messages. This timeout value forces us to close the stream after\n   * x milliseconds. This is a configurable value via the COMETAPI_LLM_TIMEOUT_MS value\n   * @returns {number} The timeout value in milliseconds (default: 3_000)\n   */\n  #parseTimeout() {\n    this.log(\n      `CometAPI timeout is set to ${process.env.COMETAPI_LLM_TIMEOUT_MS ?? this.defaultTimeout}ms`\n    );\n    if (isNaN(Number(process.env.COMETAPI_LLM_TIMEOUT_MS)))\n      return this.defaultTimeout;\n    const setValue = Number(process.env.COMETAPI_LLM_TIMEOUT_MS);\n    if (setValue < 500) return 500;\n    return setValue;\n  }\n\n  // This checks if the .cached_at file has a timestamp that is more than 1Week (in millis)\n  // from the current date. If it is, then we will refetch the API so that all the models are up\n  // to date.\n  #cacheIsStale() {\n    const MAX_STALE = 6.048e8; // 1 Week in MS\n    if (!fs.existsSync(this.cacheAtPath)) return true;\n    const now = Number(new Date());\n    const timestampMs = Number(fs.readFileSync(this.cacheAtPath));\n    return now - timestampMs > MAX_STALE;\n  }\n\n  // The CometAPI model API has a lot of models, so we cache this locally in the directory\n  // as if the cache directory JSON file is stale or does not exist we will fetch from API and store it.\n  // This might slow down the first request, but we need the proper token context window\n  // for each model and this is a constructor property - so we can really only get it if this cache exists.\n  // We used to have this as a chore, but given there is an API to get the info - this makes little sense.\n  async #syncModels() {\n    if (fs.existsSync(this.cacheModelPath) && !this.#cacheIsStale())\n      return false;\n\n    this.log(\n      \"Model cache is not present or stale. Fetching from CometAPI API.\"\n    );\n    await fetchCometApiModels();\n    return;\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  models() {\n    if (!fs.existsSync(this.cacheModelPath)) return {};\n    return safeJsonParse(\n      fs.readFileSync(this.cacheModelPath, { encoding: \"utf-8\" }),\n      {}\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    const cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n    const availableModels = fs.existsSync(cacheModelPath)\n      ? safeJsonParse(\n          fs.readFileSync(cacheModelPath, { encoding: \"utf-8\" }),\n          {}\n        )\n      : {};\n    return availableModels[modelName]?.maxLength || 4096;\n  }\n\n  promptWindowLimit() {\n    const availableModels = this.models();\n    return availableModels[this.model]?.maxLength || 4096;\n  }\n\n  async isValidChatCompletionModel(model = \"\") {\n    await this.#syncModels();\n    const availableModels = this.models();\n    return availableModels.hasOwnProperty(model);\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"auto\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `CometAPI chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `CometAPI chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  /**\n   * Handles the default stream response for a chat.\n   * @param {import(\"express\").Response} response\n   * @param {import('../../helpers/chat/LLMPerformanceMonitor').MonitoredStream} stream\n   * @param {Object} responseProps\n   * @returns {Promise<string>}\n   */\n  handleStream(response, stream, responseProps) {\n    const timeoutThresholdMs = this.timeout;\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n      let lastChunkTime = null; // null when first token is still not received.\n\n      // Establish listener to early-abort a streaming response\n      // in case things go sideways or the user does not like the response.\n      // We preserve the generated text but continue as if chat was completed\n      // to preserve previously generated content.\n      const handleAbort = () => {\n        stream?.endMeasurement({\n          completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n        });\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      // NOTICE: Not all CometAPI models will return a stop reason\n      // which keeps the connection open and so the model never finalizes the stream\n      // like the traditional OpenAI response schema does. So in the case the response stream\n      // never reaches a formal close state we maintain an interval timer that if we go >=timeoutThresholdMs with\n      // no new chunks then we kill the stream and assume it to be complete. CometAPI is quite fast\n      // so this threshold should permit most responses, but we can adjust `timeoutThresholdMs` if\n      // we find it is too aggressive.\n      const timeoutCheck = setInterval(() => {\n        if (lastChunkTime === null) return;\n\n        const now = Number(new Date());\n        const diffMs = now - lastChunkTime;\n        if (diffMs >= timeoutThresholdMs) {\n          this.log(\n            `CometAPI stream did not self-close and has been stale for >${timeoutThresholdMs}ms. Closing response stream.`\n          );\n          writeResponseChunk(response, {\n            uuid,\n            sources,\n            type: \"textResponseChunk\",\n            textResponse: \"\",\n            close: true,\n            error: false,\n          });\n          clearInterval(timeoutCheck);\n          response.removeListener(\"close\", handleAbort);\n          stream?.endMeasurement({\n            completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n          });\n          resolve(fullText);\n        }\n      }, 500);\n\n      try {\n        for await (const chunk of stream) {\n          const message = chunk?.choices?.[0];\n          const token = message?.delta?.content;\n          lastChunkTime = Number(new Date());\n\n          if (token) {\n            fullText += token;\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: token,\n              close: false,\n              error: false,\n            });\n          }\n\n          if (message.finish_reason !== null) {\n            writeResponseChunk(response, {\n              uuid,\n              sources,\n              type: \"textResponseChunk\",\n              textResponse: \"\",\n              close: true,\n              error: false,\n            });\n            response.removeListener(\"close\", handleAbort);\n            stream?.endMeasurement({\n              completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n            });\n            resolve(fullText);\n          }\n        }\n      } catch (e) {\n        writeResponseChunk(response, {\n          uuid,\n          sources,\n          type: \"abort\",\n          textResponse: null,\n          close: true,\n          error: e.message,\n        });\n        response.removeListener(\"close\", handleAbort);\n        stream?.endMeasurement({\n          completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n        });\n        resolve(fullText);\n      }\n    });\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\n/**\n * Fetches available models from CometAPI and filters out non-chat models\n * Based on cometapi.md specifications\n */\nasync function fetchCometApiModels() {\n  return await fetch(`https://api.cometapi.com/v1/models`, {\n    method: \"GET\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${process.env.COMETAPI_LLM_API_KEY}`,\n    },\n  })\n    .then((res) => res.json())\n    .then(({ data = [] }) => {\n      const models = {};\n\n      // Filter out non-chat models using patterns from cometapi.md\n      const chatModels = data.filter((model) => {\n        const modelId = model.id.toLowerCase();\n        return !COMETAPI_IGNORE_PATTERNS.some((pattern) =>\n          modelId.includes(pattern.toLowerCase())\n        );\n      });\n\n      chatModels.forEach((model) => {\n        models[model.id] = {\n          id: model.id,\n          name: model.id, // CometAPI has limited model info according to cometapi.md\n          organization:\n            model.id.split(\"/\")[0] || model.id.split(\"-\")[0] || \"CometAPI\",\n          maxLength: model.context_length || 4096, // Conservative default\n        };\n      });\n\n      // Cache all response information\n      if (!fs.existsSync(cacheFolder))\n        fs.mkdirSync(cacheFolder, { recursive: true });\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \"models.json\"),\n        JSON.stringify(models),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \".cached_at\"),\n        String(Number(new Date())),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n      return models;\n    })\n    .catch((e) => {\n      console.error(\"Error fetching CometAPI models:\", e);\n      return {};\n    });\n}\n\nmodule.exports = {\n  CometApiLLM,\n  fetchCometApiModels,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/deepseek/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst { MODEL_MAP } = require(\"../modelMap\");\nconst {\n  writeResponseChunk,\n  clientAbortedHandler,\n} = require(\"../../helpers/chat/responses\");\n\nclass DeepSeekLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.DEEPSEEK_API_KEY)\n      throw new Error(\"No DeepSeek API key was set.\");\n    this.className = \"DeepSeekLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n\n    this.openai = new OpenAIApi({\n      apiKey: process.env.DEEPSEEK_API_KEY,\n      baseURL: \"https://api.deepseek.com/v1\",\n    });\n    this.model =\n      modelPreference || process.env.DEEPSEEK_MODEL_PREF || \"deepseek-chat\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.log(\n      `Initialized ${this.model} with context window ${this.promptWindowLimit()}`\n    );\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    return MODEL_MAP.get(\"deepseek\", modelName) ?? 8192;\n  }\n\n  promptWindowLimit() {\n    return MODEL_MAP.get(\"deepseek\", this.model) ?? 8192;\n  }\n\n  async isValidChatCompletionModel(modelName = \"\") {\n    const models = await this.openai.models.list().catch(() => ({ data: [] }));\n    return models.data.some((model) => model.id === modelName);\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [prompt, ...chatHistory, { role: \"user\", content: userPrompt }];\n  }\n\n  /**\n   * Parses and prepends reasoning from the response and returns the full text response.\n   * @param {Object} response\n   * @returns {string}\n   */\n  #parseReasoningFromResponse({ message }) {\n    let textResponse = message?.content;\n    if (\n      !!message?.reasoning_content &&\n      message.reasoning_content.trim().length > 0\n    )\n      textResponse = `<think>${message.reasoning_content}</think>${textResponse}`;\n    return textResponse;\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `DeepSeek chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result?.output?.hasOwnProperty(\"choices\") ||\n      result?.output?.choices?.length === 0\n    )\n      throw new Error(\n        `Invalid response body returned from DeepSeek: ${JSON.stringify(result.output)}`\n      );\n\n    return {\n      textResponse: this.#parseReasoningFromResponse(result.output.choices[0]),\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `DeepSeek chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  // TODO: This is a copy of the generic handleStream function in responses.js\n  // to specifically handle the DeepSeek reasoning model `reasoning_content` field.\n  // When or if ever possible, we should refactor this to be in the generic function.\n  handleStream(response, stream, responseProps) {\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n    let hasUsageMetrics = false;\n    let usage = {\n      completion_tokens: 0,\n    };\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n      let reasoningText = \"\";\n\n      // Establish listener to early-abort a streaming response\n      // in case things go sideways or the user does not like the response.\n      // We preserve the generated text but continue as if chat was completed\n      // to preserve previously generated content.\n      const handleAbort = () => {\n        stream?.endMeasurement(usage);\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      try {\n        for await (const chunk of stream) {\n          const message = chunk?.choices?.[0];\n          const token = message?.delta?.content;\n          const reasoningToken = message?.delta?.reasoning_content;\n\n          if (\n            chunk.hasOwnProperty(\"usage\") && // exists\n            !!chunk.usage && // is not null\n            Object.values(chunk.usage).length > 0 // has values\n          ) {\n            if (chunk.usage.hasOwnProperty(\"prompt_tokens\")) {\n              usage.prompt_tokens = Number(chunk.usage.prompt_tokens);\n            }\n\n            if (chunk.usage.hasOwnProperty(\"completion_tokens\")) {\n              hasUsageMetrics = true; // to stop estimating counter\n              usage.completion_tokens = Number(chunk.usage.completion_tokens);\n            }\n          }\n\n          // Reasoning models will always return the reasoning text before the token text.\n          if (reasoningToken) {\n            // If the reasoning text is empty (''), we need to initialize it\n            // and send the first chunk of reasoning text.\n            if (reasoningText.length === 0) {\n              writeResponseChunk(response, {\n                uuid,\n                sources: [],\n                type: \"textResponseChunk\",\n                textResponse: `<think>${reasoningToken}`,\n                close: false,\n                error: false,\n              });\n              reasoningText += `<think>${reasoningToken}`;\n              continue;\n            } else {\n              writeResponseChunk(response, {\n                uuid,\n                sources: [],\n                type: \"textResponseChunk\",\n                textResponse: reasoningToken,\n                close: false,\n                error: false,\n              });\n              reasoningText += reasoningToken;\n            }\n          }\n\n          // If the reasoning text is not empty, but the reasoning token is empty\n          // and the token text is not empty we need to close the reasoning text and begin sending the token text.\n          if (!!reasoningText && !reasoningToken && token) {\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: `</think>`,\n              close: false,\n              error: false,\n            });\n            fullText += `${reasoningText}</think>`;\n            reasoningText = \"\";\n          }\n\n          if (token) {\n            fullText += token;\n            // If we never saw a usage metric, we can estimate them by number of completion chunks\n            if (!hasUsageMetrics) usage.completion_tokens++;\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: token,\n              close: false,\n              error: false,\n            });\n          }\n\n          // LocalAi returns '' and others return null on chunks - the last chunk is not \"\" or null.\n          // Either way, the key `finish_reason` must be present to determine ending chunk.\n          if (\n            message?.hasOwnProperty(\"finish_reason\") && // Got valid message and it is an object with finish_reason\n            message.finish_reason !== \"\" &&\n            message.finish_reason !== null\n          ) {\n            writeResponseChunk(response, {\n              uuid,\n              sources,\n              type: \"textResponseChunk\",\n              textResponse: \"\",\n              close: true,\n              error: false,\n            });\n            response.removeListener(\"close\", handleAbort);\n            stream?.endMeasurement(usage);\n            resolve(fullText);\n            break; // Break streaming when a valid finish_reason is first encountered\n          }\n        }\n      } catch (e) {\n        console.log(`\\x1b[43m\\x1b[34m[STREAMING ERROR]\\x1b[0m ${e.message}`);\n        writeResponseChunk(response, {\n          uuid,\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: e.message,\n        });\n        stream?.endMeasurement(usage);\n        resolve(fullText); // Return what we currently have - if anything.\n      }\n    });\n  }\n\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  DeepSeekLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/dellProAiStudio/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  handleDefaultStreamResponseV2,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\n\n//  hybrid of openAi LLM chat completion for Dell Pro AI Studio\nclass DellProAiStudioLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.DPAIS_LLM_BASE_PATH)\n      throw new Error(\"No Dell Pro AI Studio Base Path was set.\");\n\n    this.className = \"DellProAiStudioLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.dpais = new OpenAIApi({\n      baseURL: DellProAiStudioLLM.parseBasePath(),\n      apiKey: null,\n    });\n\n    this.model = modelPreference || process.env.DPAIS_LLM_MODEL_PREF;\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.log(\n      `Dell Pro AI Studio LLM initialized with ${this.model}. ctx: ${this.promptWindowLimit()}`\n    );\n  }\n\n  /**\n   * Parse the base path for the Dell Pro AI Studio API\n   * so we can use it for inference requests\n   * @param {string} providedBasePath\n   * @returns {string}\n   */\n  static parseBasePath(providedBasePath = process.env.DPAIS_LLM_BASE_PATH) {\n    try {\n      const baseURL = new URL(providedBasePath);\n      const basePath = `${baseURL.origin}/v1/openai`;\n      return basePath;\n    } catch {\n      return null;\n    }\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(_modelName) {\n    const limit = process.env.DPAIS_LLM_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No Dell Pro AI Studio token context limit was set.\");\n    return Number(limit);\n  }\n\n  // Ensure the user set a value for the token limit\n  // and if undefined - assume 4096 window.\n  promptWindowLimit() {\n    const limit = process.env.DPAIS_LLM_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No Dell Pro AI Studio token context limit was set.\");\n    return Number(limit);\n  }\n\n  async isValidChatCompletionModel(_ = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) return userPrompt;\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"auto\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    _attachments = [], // not used for Dell Pro AI Studio - `attachments` passed in is ignored\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, _attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        `Dell Pro AI Studio chat: ${this.model} is not valid or defined model for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.dpais.chat.completions.create({\n        model: this.model,\n        messages,\n        temperature,\n      })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage?.prompt_tokens || 0,\n        completion_tokens: result.output.usage?.completion_tokens || 0,\n        total_tokens: result.output.usage?.total_tokens || 0,\n        outputTps: result.output.usage?.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        `Dell Pro AI Studio chat: ${this.model} is not valid or defined model for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.dpais.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  DellProAiStudioLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/dockerModelRunner/index.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  handleDefaultStreamResponseV2,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst { OpenAI: OpenAIApi } = require(\"openai\");\nconst { humanFileSize } = require(\"../../helpers\");\nconst { safeJsonParse } = require(\"../../http\");\n\nclass DockerModelRunnerLLM {\n  static cacheTime = 1000 * 60 * 60 * 24; // 24 hours\n  static cacheFolder = path.resolve(\n    process.env.STORAGE_DIR\n      ? path.resolve(process.env.STORAGE_DIR, \"models\", \"docker-model-runner\")\n      : path.resolve(__dirname, `../../../storage/models/docker-model-runner`)\n  );\n\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.DOCKER_MODEL_RUNNER_BASE_PATH)\n      throw new Error(\"No Docker Model Runner API Base Path was set.\");\n    if (!process.env.DOCKER_MODEL_RUNNER_LLM_MODEL_PREF && !modelPreference)\n      throw new Error(\"No Docker Model Runner Model Pref was set.\");\n\n    this.className = \"DockerModelRunnerLLM\";\n    this.dmr = new OpenAIApi({\n      baseURL: parseDockerModelRunnerEndpoint(\n        process.env.DOCKER_MODEL_RUNNER_BASE_PATH\n      ),\n      apiKey: null,\n    });\n\n    this.model =\n      modelPreference || process.env.DOCKER_MODEL_RUNNER_LLM_MODEL_PREF;\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.#log(`initialized with model: ${this.model}`);\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[32m[Docker Model Runner]\\x1b[0m ${text}`, ...args);\n  }\n\n  static slog(text, ...args) {\n    console.log(`\\x1b[32m[Docker Model Runner]\\x1b[0m ${text}`, ...args);\n  }\n\n  async assertModelContextLimits() {\n    if (this.limits !== null) return;\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  /** DMR does not support curling the context window limit from the API, so we return the system defined limit. */\n  static promptWindowLimit(_) {\n    const systemDefinedLimit =\n      Number(process.env.DOCKER_MODEL_RUNNER_LLM_MODEL_TOKEN_LIMIT) || 8192;\n    return systemDefinedLimit;\n  }\n\n  promptWindowLimit() {\n    return this.constructor.promptWindowLimit(this.model);\n  }\n\n  async isValidChatCompletionModel(_ = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"auto\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        `Docker Model Runner chat: ${this.model} is not valid or defined model for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.dmr.chat.completions.create({\n        model: this.model,\n        messages,\n        temperature,\n      })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage?.prompt_tokens || 0,\n        completion_tokens: result.output.usage?.completion_tokens || 0,\n        total_tokens: result.output.usage?.total_tokens || 0,\n        outputTps: result.output.usage?.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        `Docker Model Runner chat: ${this.model} is not valid or defined model for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.dmr.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  /**\n   * Returns the capabilities of the model.\n   * Note: This is a heuristic approach to get the capabilities of the model based on the model metadata.\n   * It is not perfect, but works since every model metadata is different and may not have key values we rely on.\n   * There is no \"capabilities\" key in the metadata via any API endpoint - so we do this.\n   * @returns {Promise<{tools: 'unknown' | boolean, reasoning: 'unknown' | boolean, imageGeneration: 'unknown' | boolean, vision: 'unknown' | boolean}>}\n   */\n  async getModelCapabilities() {\n    try {\n      const endpoint = new URL(\n        parseDockerModelRunnerEndpoint(\n          process.env.DOCKER_MODEL_RUNNER_BASE_PATH,\n          \"dmr\"\n        )\n      );\n      // eg: /models/ai/qwen3:4B-UD-Q4_K_XL\n      endpoint.pathname = `/models/${this.model}`;\n      const response = await fetch(endpoint.toString());\n      const data = await response.text();\n\n      const tools = /tools|tool|tool_use|tool_call/.test(data);\n      const reasoning = /thinking|reason|reasoning|think/.test(data);\n      const imageGeneration = /diffusion/.test(data);\n      const vision = /vision|vllm|image/.test(data);\n      return {\n        tools: tools,\n        reasoning: reasoning,\n        imageGeneration: imageGeneration,\n        vision: vision,\n      };\n    } catch (error) {\n      console.error(\"Error getting model capabilities:\", error);\n      return {\n        tools: \"unknown\",\n        reasoning: \"unknown\",\n        imageGeneration: \"unknown\",\n        vision: \"unknown\",\n      };\n    }\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    await this.assertModelContextLimits();\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\n/**\n * Parse the base path of the Docker Model Runner endpoint and return the host and port.\n * @param {string} basePath - The base path of the Docker Model Runner endpoint.\n * @param {'openai' | 'dmr'} to - The provider to parse the endpoint for (internal DMR or openai-compatible)\n * @returns {string | null}\n */\nfunction parseDockerModelRunnerEndpoint(basePath = null, to = \"openai\") {\n  if (!basePath) return null;\n  try {\n    const url = new URL(basePath);\n    if (to === \"openai\") url.pathname = \"engines/v1\";\n    else if (to === \"ollama\") url.pathname = \"api\";\n    else if (to === \"dmr\") url.pathname = \"\";\n    return url.toString();\n  } catch {\n    return basePath;\n  }\n}\n\n/**\n * @typedef {Object} DockerRunnerInstalledModel\n * @property {string} id - The SHA256 identifier of the model layer/blob.\n * @property {string[]} tags - List of tags or aliases associated with this model (e.g., \"ai/qwen3:4B-UD-Q4_K_XL\").\n * @property {number} created - The Unix timestamp (seconds) when the model was created.\n * @property {string} config - The configuration of the model.\n * @property {string} config.format - The file format (e.g., \"gguf\").\n * @property {string} config.quantization - The quantization level (e.g., \"MOSTLY_Q4_K_M\", \"Q4_0\").\n * @property {string} config.parameters - The parameter count formatted as a string (e.g., \"4.02 B\").\n * @property {string} config.architecture - The base architecture of the model (e.g., \"qwen3\", \"llama\").\n * @property {string} config.size - The physical file size formatted as a string (e.g., \"2.37 GiB\").\n * @property {string} config?.gguf - Raw GGUF metadata headers containing tokenizer, architecture details, and licensing.\n * @property {string} config?.gguf['general.base_model.0.organization'] - The tokenizer of the model.\n * @property {string} config?.gguf['general.basename'] - The base name of the model (the real name of the model, not the tag)\n * @property {string} config?.gguf['*.context_length'] - The context length of the model. will be something like qwen3.context_length\n */\n\nfunction filterByTask(task = \"chat\", models = {}) {\n  const possibleEmbed = [{ pattern: /^all-mini/i }, { pattern: /embed/i }];\n  const isEmbedModel = (strTag) =>\n    possibleEmbed.some((p) => p.pattern.test(strTag));\n  const filteredModels = {};\n  for (const [modelName, tags] of Object.entries(models)) {\n    if (task === \"chat\") {\n      if (isEmbedModel(modelName)) continue;\n      filteredModels[modelName] = tags;\n    } else if (task === \"embedding\") {\n      if (!isEmbedModel(modelName)) continue;\n      filteredModels[modelName] = tags;\n    }\n  }\n  return filteredModels;\n}\n\n/**\n * Fetch the remote models from the Docker Hub and cache the results.\n * @param {'chat' | 'embedding'} task - The task to fetch the models for.\n * @returns {Promise<Record<string, {id: string, name: string, size: string, organization: string}[]>>}\n */\nasync function fetchRemoteModels(task = \"chat\") {\n  const cachePath = path.resolve(\n    DockerModelRunnerLLM.cacheFolder,\n    \"models.json\"\n  );\n  const cachedAtPath = path.resolve(\n    DockerModelRunnerLLM.cacheFolder,\n    \".cached_at\"\n  );\n  let cacheTime = 0;\n\n  if (fs.existsSync(cachePath) && fs.existsSync(cachedAtPath)) {\n    cacheTime = Number(fs.readFileSync(cachedAtPath, \"utf8\"));\n    if (Date.now() - cacheTime < DockerModelRunnerLLM.cacheTime)\n      return filterByTask(\n        task,\n        safeJsonParse(fs.readFileSync(cachePath, \"utf8\"))\n      );\n  }\n\n  DockerModelRunnerLLM.slog(`Refreshing remote models from Docker Hub`);\n  // Now hit the Docker Hub API to get the remote model namespace and root tags\n  const availableNamespaces = []; // array of strings like ai/mistral, ai/qwen3, etc\n  let nextPage =\n    \"https://hub.docker.com/v2/namespaces/ai/repositories?page_size=100&page=1\";\n  while (nextPage) {\n    const response = await fetch(nextPage)\n      .then((res) => res.json())\n      .then((data) => {\n        const namespaces = data.results\n          .filter(\n            (result) =>\n              result.namespace &&\n              result.name &&\n              result.content_types.includes(\"model\") &&\n              result.namespace === \"ai\"\n          )\n          .map((result) => result.namespace + \"/\" + result.name);\n        availableNamespaces.push(...namespaces);\n      })\n      .catch((e) => {\n        DockerModelRunnerLLM.slog(\n          `Error fetching remote models from Docker Hub`,\n          e\n        );\n        return [];\n      });\n    if (!response) break;\n    if (!response || !response.next) break;\n    nextPage = response.next;\n  }\n\n  const availableRemoteModels = {};\n  const BATCH_SIZE = 10;\n\n  // Run batch requests to avoid rate limiting but also\n  // improve the speed of the total request time.\n  for (let i = 0; i < availableNamespaces.length; i += BATCH_SIZE) {\n    const batch = availableNamespaces.slice(i, i + BATCH_SIZE);\n    DockerModelRunnerLLM.slog(\n      `Fetching tags for batch ${Math.floor(i / BATCH_SIZE) + 1} of ${Math.ceil(availableNamespaces.length / BATCH_SIZE)}`\n    );\n\n    await Promise.all(\n      batch.map(async (namespace) => {\n        const [organization, model] = namespace.split(\"/\");\n        const namespaceUrl = new URL(\n          \"https://hub.docker.com/v2/namespaces/ai/repositories/\" +\n            model +\n            \"/tags\"\n        );\n\n        DockerModelRunnerLLM.slog(\n          `Fetching tags for ${namespaceUrl.toString()}`\n        );\n        await fetch(namespaceUrl.toString())\n          .then((res) => res.json())\n          .then((data) => {\n            const tags = data.results.map((result) => {\n              return {\n                id: `${organization}/${model}:${result.name}`,\n                name: `${model}:${result.name}`,\n                size: humanFileSize(result.full_size),\n                organization: model,\n              };\n            });\n            availableRemoteModels[model] = tags;\n          })\n          .catch((e) => {\n            DockerModelRunnerLLM.slog(\n              `Error fetching tags for ${namespaceUrl.toString()}`,\n              e\n            );\n          });\n      })\n    );\n  }\n\n  if (Object.keys(availableRemoteModels).length === 0) {\n    DockerModelRunnerLLM.slog(\n      `No remote models found - API may be down or not available`\n    );\n    return {};\n  }\n\n  if (!fs.existsSync(DockerModelRunnerLLM.cacheFolder))\n    fs.mkdirSync(DockerModelRunnerLLM.cacheFolder, { recursive: true });\n  fs.writeFileSync(cachePath, JSON.stringify(availableRemoteModels), {\n    encoding: \"utf8\",\n  });\n  fs.writeFileSync(cachedAtPath, String(Number(new Date())), {\n    encoding: \"utf8\",\n  });\n  return filterByTask(task, availableRemoteModels);\n}\n\n/**\n * This function will fetch the remote models from the Docker Hub as well\n * as the local models installed on the system.\n * @param {string} basePath - The base path of the Docker Model Runner endpoint.\n * @param {'chat' | 'embedding'} task - The task to fetch the models for.\n */\nasync function getDockerModels(basePath = null, task = \"chat\") {\n  let availableModels = {};\n  /** @type {Array<DockerRunnerInstalledModel>} */\n  let installedModels = {};\n\n  try {\n    // Grab the locally installed models from the Docker Model Runner API\n    const dmrUrl = new URL(\n      parseDockerModelRunnerEndpoint(\n        basePath ?? process.env.DOCKER_MODEL_RUNNER_BASE_PATH,\n        \"dmr\"\n      )\n    );\n    dmrUrl.pathname = \"/models\";\n\n    await fetch(dmrUrl.toString())\n      .then((res) => res.json())\n      .then((data) => {\n        data?.forEach((model) => {\n          const id = model.tags.at(0);\n          // eg: ai/qwen3:latest -> qwen3\n          const tag =\n            id?.split(\"/\").pop()?.split(\":\")?.at(1) ??\n            id?.split(\":\").at(1) ??\n            \"latest\";\n          const organization = id?.split(\"/\").pop()?.split(\":\")?.at(0) ?? id;\n          installedModels[id] = {\n            id: id,\n            name: `${organization}:${tag}`,\n            size: model.config?.size ?? \"Unknown size\",\n            organization: organization,\n          };\n        });\n      });\n\n    // Now hit the Docker Hub API to get the remote model namespace and root tags\n    const remoteModels = await fetchRemoteModels(task);\n    for (const [modelName, tags] of Object.entries(remoteModels)) {\n      availableModels[modelName] = { tags: [] };\n      for (const tag of tags) {\n        if (!installedModels[tag.id])\n          availableModels[modelName].tags.push({ ...tag, downloaded: false });\n        else {\n          availableModels[modelName].tags.push({ ...tag, downloaded: true });\n          // remove the model from the installed models list so we dont double append it to the available models list\n          // when checking for custom models\n          delete installedModels[tag.id];\n        }\n      }\n    }\n\n    // For any models that are still in the installed models list, we need to append them to the available models list as downloaded\n    for (const model of Object.values(installedModels)) {\n      const organization = model.id.split(\"/\").pop();\n      const name = model.id.split(\"/\").pop();\n      if (!availableModels[organization])\n        availableModels[organization] = { tags: [] };\n      availableModels[organization].tags.push({\n        ...model,\n        downloaded: true,\n        name: name,\n      });\n    }\n  } catch (e) {\n    DockerModelRunnerLLM.slog(`Error getting Docker models`, e);\n  } finally {\n    // eslint-disable-next-line\n    return Object.values(availableModels).flatMap((m) => m.tags);\n  }\n}\n\nmodule.exports = {\n  DockerModelRunnerLLM,\n  parseDockerModelRunnerEndpoint,\n  getDockerModels,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/fireworksAi/index.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { safeJsonParse } = require(\"../../http\");\nconst { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst {\n  handleDefaultStreamResponseV2,\n} = require(\"../../helpers/chat/responses\");\n\nconst cacheFolder = path.resolve(\n  process.env.STORAGE_DIR\n    ? path.resolve(process.env.STORAGE_DIR, \"models\", \"fireworks\")\n    : path.resolve(__dirname, `../../../storage/models/fireworks`)\n);\n\nclass FireworksAiLLM {\n  constructor(embedder = null, modelPreference = null) {\n    this.className = \"FireworksAiLLM\";\n\n    if (!process.env.FIREWORKS_AI_LLM_API_KEY)\n      throw new Error(\"No FireworksAI API key was set.\");\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.openai = new OpenAIApi({\n      baseURL: \"https://api.fireworks.ai/inference/v1\",\n      apiKey: process.env.FIREWORKS_AI_LLM_API_KEY ?? null,\n    });\n    this.model = modelPreference || process.env.FIREWORKS_AI_LLM_MODEL_PREF;\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = !embedder ? new NativeEmbedder() : embedder;\n    this.defaultTemp = 0.7;\n\n    if (!fs.existsSync(cacheFolder))\n      fs.mkdirSync(cacheFolder, { recursive: true });\n    this.cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n    this.cacheAtPath = path.resolve(cacheFolder, \".cached_at\");\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  // This checks if the .cached_at file has a timestamp that is more than 1Week (in millis)\n  // from the current date. If it is, then we will refetch the API so that all the models are up\n  // to date.\n  #cacheIsStale() {\n    const MAX_STALE = 6.048e8; // 1 Week in MS\n    if (!fs.existsSync(this.cacheAtPath)) return true;\n    const now = Number(new Date());\n    const timestampMs = Number(fs.readFileSync(this.cacheAtPath));\n    return now - timestampMs > MAX_STALE;\n  }\n\n  // This function fetches the models from the ApiPie API and caches them locally.\n  // We do this because the ApiPie API has a lot of models, and we need to get the proper token context window\n  // for each model and this is a constructor property - so we can really only get it if this cache exists.\n  // We used to have this as a chore, but given there is an API to get the info - this makes little sense.\n  // This might slow down the first request, but we need the proper token context window\n  // for each model and this is a constructor property - so we can really only get it if this cache exists.\n  async #syncModels() {\n    if (fs.existsSync(this.cacheModelPath) && !this.#cacheIsStale())\n      return false;\n\n    this.log(\n      \"Model cache is not present or stale. Fetching from FireworksAI API.\"\n    );\n    await fireworksAiModels();\n    return;\n  }\n\n  models() {\n    if (!fs.existsSync(this.cacheModelPath)) return {};\n    return safeJsonParse(\n      fs.readFileSync(this.cacheModelPath, { encoding: \"utf-8\" }),\n      {}\n    );\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    const cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n    const availableModels = fs.existsSync(cacheModelPath)\n      ? safeJsonParse(\n          fs.readFileSync(cacheModelPath, { encoding: \"utf-8\" }),\n          {}\n        )\n      : {};\n    return availableModels[modelName]?.maxLength || 4096;\n  }\n\n  // Ensure the user set a value for the token limit\n  // and if undefined - assume 4096 window.\n  promptWindowLimit() {\n    const availableModels = this.models();\n    return availableModels[this.model]?.maxLength || 4096;\n  }\n\n  async isValidChatCompletionModel(model = \"\") {\n    await this.#syncModels();\n    const availableModels = this.models();\n    return availableModels.hasOwnProperty(model);\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [prompt, ...chatHistory, { role: \"user\", content: userPrompt }];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `FireworksAI chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions.create({\n        model: this.model,\n        messages,\n        temperature,\n      })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `FireworksAI chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nasync function fireworksAiModels(providedApiKey = null) {\n  const apiKey = providedApiKey || process.env.FIREWORKS_AI_LLM_API_KEY || null;\n  const { OpenAI: OpenAIApi } = require(\"openai\");\n  const client = new OpenAIApi({\n    baseURL: \"https://api.fireworks.ai/inference/v1\",\n    apiKey: apiKey,\n  });\n\n  return await client.models\n    .list()\n    .then((res) => res.data)\n    .then((models = []) => {\n      const validModels = {};\n      models.forEach((model) => {\n        // There are many models - the ones without a context length are not chat models\n        if (!model.hasOwnProperty(\"context_length\")) return;\n\n        validModels[model.id] = {\n          id: model.id,\n          name: model.id.split(\"/\").pop(),\n          organization: model.owned_by,\n          subtype: model.type,\n          maxLength: model.context_length ?? 4096,\n        };\n      });\n\n      if (Object.keys(validModels).length === 0) {\n        console.log(\"fireworksAi: No models found\");\n        return {};\n      }\n\n      // Cache all response information\n      if (!fs.existsSync(cacheFolder))\n        fs.mkdirSync(cacheFolder, { recursive: true });\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \"models.json\"),\n        JSON.stringify(validModels),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \".cached_at\"),\n        String(Number(new Date())),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n\n      return validModels;\n    })\n    .catch((e) => {\n      console.error(e);\n      return {};\n    });\n}\n\nmodule.exports = {\n  FireworksAiLLM,\n  fireworksAiModels,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/foundry/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst {\n  formatChatHistory,\n  writeResponseChunk,\n  clientAbortedHandler,\n} = require(\"../../helpers/chat/responses\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\n\nconst { OpenAI: OpenAIApi } = require(\"openai\");\n\nclass FoundryLLM {\n  /** @see FoundryLLM.cacheContextWindows */\n  static modelContextWindows = {};\n\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.FOUNDRY_BASE_PATH)\n      throw new Error(\"No Foundry Base Path was set.\");\n\n    this.className = \"FoundryLLM\";\n    this.model = modelPreference || process.env.FOUNDRY_MODEL_PREF;\n    this.openai = new OpenAIApi({\n      baseURL: parseFoundryBasePath(process.env.FOUNDRY_BASE_PATH),\n      apiKey: null,\n    });\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.limits = null;\n    FoundryLLM.cacheContextWindows(true);\n    this.#log(`Loaded with model: ${this.model}`);\n  }\n\n  static #slog(text, ...args) {\n    console.log(`\\x1b[36m[FoundryLLM]\\x1b[0m ${text}`, ...args);\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  async assertModelContextLimits() {\n    if (this.limits !== null) return;\n    await FoundryLLM.cacheContextWindows();\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  /**\n   * Cache the context windows for the Foundry models.\n   * This is done once and then cached for the lifetime of the server. This is absolutely necessary to ensure that the context windows are correct.\n   * Foundry Local has a weird behavior that when max_completion_tokens is unset it will only allow the output to be 1024 tokens.\n   *\n   * If you pass in too large of a max_completion_tokens, it will throw an error.\n   * If you pass in too little of a max_completion_tokens, you will get stubbed outputs before you reach a real \"stop\" token.\n   * So we need to cache the context windows and use them for the lifetime of the server.\n   * @param {boolean} force\n   * @returns\n   */\n  static async cacheContextWindows(force = false) {\n    try {\n      // Skip if we already have cached context windows and we're not forcing a refresh\n      if (Object.keys(FoundryLLM.modelContextWindows).length > 0 && !force)\n        return;\n\n      const openai = new OpenAIApi({\n        baseURL: parseFoundryBasePath(process.env.FOUNDRY_BASE_PATH),\n        apiKey: null,\n      });\n      (await openai.models.list().then((result) => result.data)).map(\n        (model) => {\n          const contextWindow =\n            Number(model.maxInputTokens) + Number(model.maxOutputTokens);\n          FoundryLLM.modelContextWindows[model.id] = contextWindow;\n        }\n      );\n      FoundryLLM.#slog(`Context windows cached for all models!`);\n    } catch (e) {\n      FoundryLLM.#slog(`Error caching context windows: ${e.message}`);\n      return;\n    }\n  }\n\n  /**\n   * Unload a model from the Foundry engine forcefully\n   * If the model is invalid, we just ignore the error. This is a util\n   * simply to have the foundry engine drop the resources for the model.\n   *\n   * @param {string} modelName\n   * @returns {Promise<boolean>}\n   */\n  static async unloadModelFromEngine(modelName) {\n    const basePath = parseFoundryBasePath(process.env.FOUNDRY_BASE_PATH);\n    const baseUrl = new URL(basePath);\n    baseUrl.pathname = `/openai/unload/${modelName}`;\n    baseUrl.searchParams.set(\"force\", \"true\");\n    return await fetch(baseUrl.toString())\n      .then((res) => res.json())\n      .catch(() => null);\n  }\n\n  static promptWindowLimit(modelName) {\n    if (Object.keys(FoundryLLM.modelContextWindows).length === 0) {\n      this.#slog(\n        \"No context windows cached - Context window may be inaccurately reported.\"\n      );\n      return process.env.FOUNDRY_MODEL_TOKEN_LIMIT || 4096;\n    }\n\n    let userDefinedLimit = null;\n    const systemDefinedLimit =\n      Number(this.modelContextWindows[modelName]) || 4096;\n\n    if (\n      process.env.FOUNDRY_MODEL_TOKEN_LIMIT &&\n      !isNaN(Number(process.env.FOUNDRY_MODEL_TOKEN_LIMIT)) &&\n      Number(process.env.FOUNDRY_MODEL_TOKEN_LIMIT) > 0\n    )\n      userDefinedLimit = Number(process.env.FOUNDRY_MODEL_TOKEN_LIMIT);\n\n    // The user defined limit is always higher priority than the context window limit, but it cannot be higher than the context window limit\n    // so we return the minimum of the two, if there is no user defined limit, we return the system defined limit as-is.\n    if (userDefinedLimit !== null)\n      return Math.min(userDefinedLimit, systemDefinedLimit);\n    return systemDefinedLimit;\n  }\n\n  promptWindowLimit() {\n    return this.constructor.promptWindowLimit(this.model);\n  }\n\n  async isValidChatCompletionModel(_ = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"auto\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        `Foundry chat: ${this.model} is not valid or defined model for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n          max_completion_tokens: this.promptWindowLimit(),\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        `Foundry chat: ${this.model} is not valid or defined model for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n        max_completion_tokens: this.promptWindowLimit(),\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  /**\n   * The timeout for the Foundry stream in milliseconds.\n   * This is because Foundry does not self-close the stream and so we need to timeout the stream after a certain amount of time.\n   * @returns {number}\n   */\n  get timeout() {\n    return 500;\n  }\n\n  /**\n   * Handles the default stream response for a chat.\n   * @param {import(\"express\").Response} response\n   * @param {import('../../helpers/chat/LLMPerformanceMonitor').MonitoredStream} stream\n   * @param {Object} responseProps\n   * @returns {Promise<string>}\n   */\n  handleStream(response, stream, responseProps) {\n    const timeoutThresholdMs = this.timeout;\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n      let reasoningText = \"\";\n      let lastChunkTime = null; // null when first token is still not received.\n\n      // Establish listener to early-abort a streaming response\n      // in case things go sideways or the user does not like the response.\n      // We preserve the generated text but continue as if chat was completed\n      // to preserve previously generated content.\n      const handleAbort = () => {\n        stream?.endMeasurement({\n          completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n        });\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      // NOTICE: As of Foundry 0.8.119 the stream will never return a finish_reason\n      // nor will it self-close or send a final chunk. So we need to maintain an interval timer that if we go >=timeoutThresholdMs with\n      // no new chunks then we kill the stream and assume it to be complete.\n      const timeoutCheck = setInterval(() => {\n        if (lastChunkTime === null) return;\n\n        const now = Number(new Date());\n        const diffMs = now - lastChunkTime;\n\n        if (diffMs >= timeoutThresholdMs) {\n          console.log(\n            `Foundry stream did not self-close and has been stale for >${timeoutThresholdMs}ms. Closing response stream.`\n          );\n          writeResponseChunk(response, {\n            uuid,\n            sources,\n            type: \"textResponseChunk\",\n            textResponse: \"\",\n            close: true,\n            error: false,\n          });\n          clearInterval(timeoutCheck);\n          response.removeListener(\"close\", handleAbort);\n          stream?.endMeasurement({\n            completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n          });\n          resolve(fullText);\n        }\n      }, 500);\n\n      try {\n        for await (const chunk of stream) {\n          // console.log(JSON.stringify(chunk, null, 2));\n          const message = chunk?.choices?.[0];\n          const token = message?.delta?.content;\n          const reasoningToken = message?.delta?.reasoning;\n          lastChunkTime = Number(new Date());\n\n          // Reasoning models will always return the reasoning text before the token text.\n          // can be null or ''\n          if (reasoningToken) {\n            // If the reasoning text is empty (''), we need to initialize it\n            // and send the first chunk of reasoning text.\n            if (reasoningText.length === 0) {\n              writeResponseChunk(response, {\n                uuid,\n                sources: [],\n                type: \"textResponseChunk\",\n                textResponse: `<think>${reasoningToken}`,\n                close: false,\n                error: false,\n              });\n              reasoningText += `<think>${reasoningToken}`;\n              continue;\n            } else {\n              // If the reasoning text is not empty, we need to append the reasoning text\n              // to the existing reasoning text.\n              writeResponseChunk(response, {\n                uuid,\n                sources: [],\n                type: \"textResponseChunk\",\n                textResponse: reasoningToken,\n                close: false,\n                error: false,\n              });\n              reasoningText += reasoningToken;\n            }\n          }\n\n          // If the reasoning text is not empty, but the reasoning token is empty\n          // and the token text is not empty we need to close the reasoning text and begin sending the token text.\n          if (!!reasoningText && !reasoningToken && token) {\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: `</think>`,\n              close: false,\n              error: false,\n            });\n            fullText += `${reasoningText}</think>`;\n            reasoningText = \"\";\n          }\n\n          if (token) {\n            fullText += token;\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: token,\n              close: false,\n              error: false,\n            });\n          }\n\n          // finish_reason can be \"stop\", \"length\", etc. when complete\n          // Must check for truthy value since undefined !== null is true\n          if (message?.finish_reason) {\n            writeResponseChunk(response, {\n              uuid,\n              sources,\n              type: \"textResponseChunk\",\n              textResponse: \"\",\n              close: true,\n              error: false,\n            });\n            response.removeListener(\"close\", handleAbort);\n            clearInterval(timeoutCheck);\n            stream?.endMeasurement({\n              completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n            });\n            resolve(fullText);\n            return; // Exit the loop after resolving\n          }\n        }\n      } catch (e) {\n        writeResponseChunk(response, {\n          uuid,\n          sources,\n          type: \"abort\",\n          textResponse: null,\n          close: true,\n          error: e.message,\n        });\n        response.removeListener(\"close\", handleAbort);\n        clearInterval(timeoutCheck);\n        stream?.endMeasurement({\n          completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n        });\n        resolve(fullText);\n      }\n    });\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    await this.assertModelContextLimits();\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\n/**\n * Parse the base path for the Foundry container API. Since the base path must end in /v1 and cannot have a trailing slash,\n * and the user can possibly set it to anything and likely incorrectly due to pasting behaviors, we need to ensure it is in the correct format.\n * @param {string} basePath\n * @returns {string}\n */\nfunction parseFoundryBasePath(providedBasePath = \"\") {\n  try {\n    const baseURL = new URL(providedBasePath);\n    const basePath = `${baseURL.origin}/v1`;\n    return basePath;\n  } catch {\n    return providedBasePath;\n  }\n}\n\nmodule.exports = {\n  FoundryLLM,\n  parseFoundryBasePath,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/gemini/defaultModels.js",
    "content": "const { MODEL_MAP } = require(\"../modelMap\");\n\nconst stableModels = [\n  // %STABLE_MODELS% - updated 2025-05-13T23:13:58.920Z\n  \"gemini-1.5-pro-001\",\n  \"gemini-1.5-pro-002\",\n  \"gemini-1.5-pro\",\n  \"gemini-1.5-flash-001\",\n  \"gemini-1.5-flash\",\n  \"gemini-1.5-flash-002\",\n  \"gemini-1.5-flash-8b\",\n  \"gemini-1.5-flash-8b-001\",\n  \"gemini-2.0-flash\",\n  \"gemini-2.0-flash-001\",\n  \"gemini-2.0-flash-lite-001\",\n  \"gemini-2.0-flash-lite\",\n  \"gemini-2.0-flash-preview-image-generation\",\n  // %EOC_STABLE_MODELS%\n];\n\n// There are some models that are only available in the v1beta API\n// and some models that are only available in the v1 API\n// generally, v1beta models have `exp` in the name, but not always\n// so we check for both against a static list as well via API.\nconst v1BetaModels = [\n  // %V1BETA_MODELS% - updated 2025-05-13T23:13:58.920Z\n  \"gemini-1.5-pro-latest\",\n  \"gemini-1.5-flash-latest\",\n  \"gemini-1.5-flash-8b-latest\",\n  \"gemini-1.5-flash-8b-exp-0827\",\n  \"gemini-1.5-flash-8b-exp-0924\",\n  \"gemini-2.5-pro-exp-03-25\",\n  \"gemini-2.5-pro-preview-03-25\",\n  \"gemini-2.5-flash-preview-04-17\",\n  \"gemini-2.5-flash-preview-04-17-thinking\",\n  \"gemini-2.5-pro-preview-05-06\",\n  \"gemini-2.0-flash-exp\",\n  \"gemini-2.0-flash-exp-image-generation\",\n  \"gemini-2.0-flash-lite-preview-02-05\",\n  \"gemini-2.0-flash-lite-preview\",\n  \"gemini-2.0-pro-exp\",\n  \"gemini-2.0-pro-exp-02-05\",\n  \"gemini-exp-1206\",\n  \"gemini-2.0-flash-thinking-exp-01-21\",\n  \"gemini-2.0-flash-thinking-exp\",\n  \"gemini-2.0-flash-thinking-exp-1219\",\n  \"learnlm-1.5-pro-experimental\",\n  \"learnlm-2.0-flash-experimental\",\n  \"gemma-3-1b-it\",\n  \"gemma-3-4b-it\",\n  \"gemma-3-12b-it\",\n  \"gemma-3-27b-it\",\n  // %EOC_V1BETA_MODELS%\n];\n\nconst defaultGeminiModels = () => [\n  ...stableModels.map((model) => ({\n    id: model,\n    name: model,\n    contextWindow: MODEL_MAP.get(\"gemini\", model),\n    experimental: false,\n  })),\n  ...v1BetaModels.map((model) => ({\n    id: model,\n    name: model,\n    contextWindow: MODEL_MAP.get(\"gemini\", model),\n    experimental: true,\n  })),\n];\n\nmodule.exports = {\n  defaultGeminiModels,\n  v1BetaModels,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/gemini/index.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst {\n  formatChatHistory,\n  handleDefaultStreamResponseV2,\n} = require(\"../../helpers/chat/responses\");\nconst { MODEL_MAP } = require(\"../modelMap\");\nconst { defaultGeminiModels, v1BetaModels } = require(\"./defaultModels\");\nconst { safeJsonParse } = require(\"../../http\");\nconst cacheFolder = path.resolve(\n  process.env.STORAGE_DIR\n    ? path.resolve(process.env.STORAGE_DIR, \"models\", \"gemini\")\n    : path.resolve(__dirname, `../../../storage/models/gemini`)\n);\n\nconst NO_SYSTEM_PROMPT_MODELS = [\n  \"gemma-3-1b-it\",\n  \"gemma-3-4b-it\",\n  \"gemma-3-12b-it\",\n  \"gemma-3-27b-it\",\n];\n\nclass GeminiLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.GEMINI_API_KEY)\n      throw new Error(\"No Gemini API key was set.\");\n\n    this.className = \"GeminiLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.model =\n      modelPreference ||\n      process.env.GEMINI_LLM_MODEL_PREF ||\n      \"gemini-2.0-flash-lite\";\n\n    const isExperimental = this.isExperimentalModel(this.model);\n    this.openai = new OpenAIApi({\n      apiKey: process.env.GEMINI_API_KEY,\n      // Even models that are v1 in gemini API can be used with v1beta/openai/ endpoint and nobody knows why.\n      baseURL: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n    });\n\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n\n    if (!fs.existsSync(cacheFolder))\n      fs.mkdirSync(cacheFolder, { recursive: true });\n    this.cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n    this.cacheAtPath = path.resolve(cacheFolder, \".cached_at\");\n    this.#log(\n      `Initialized with model: ${this.model} ${isExperimental ? \"[Experimental v1beta]\" : \"[Stable v1]\"} - ctx: ${this.promptWindowLimit()}`\n    );\n  }\n\n  /**\n   * Checks if the model supports system prompts\n   * This is a static list of models that are known to not support system prompts\n   * since this information is not available in the API model response.\n   * @returns {boolean}\n   */\n  get supportsSystemPrompt() {\n    return !NO_SYSTEM_PROMPT_MODELS.includes(this.model);\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[32m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  // This checks if the .cached_at file has a timestamp that is more than 1Week (in millis)\n  // from the current date. If it is, then we will refetch the API so that all the models are up\n  // to date.\n  static cacheIsStale() {\n    const MAX_STALE = 8.64e7; // 1 day in MS\n    if (!fs.existsSync(path.resolve(cacheFolder, \".cached_at\"))) return true;\n    const now = Number(new Date());\n    const timestampMs = Number(\n      fs.readFileSync(path.resolve(cacheFolder, \".cached_at\"))\n    );\n    return now - timestampMs > MAX_STALE;\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    try {\n      const cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n      if (!fs.existsSync(cacheModelPath))\n        return MODEL_MAP.get(\"gemini\", modelName) ?? 30_720;\n\n      const models = safeJsonParse(fs.readFileSync(cacheModelPath));\n      const model = models.find((model) => model.id === modelName);\n      if (!model)\n        throw new Error(\n          \"Model not found in cache - falling back to default model.\"\n        );\n      return model.contextWindow;\n    } catch (e) {\n      console.error(`GeminiLLM:promptWindowLimit`, e.message);\n      return MODEL_MAP.get(\"gemini\", modelName) ?? 30_720;\n    }\n  }\n\n  promptWindowLimit() {\n    try {\n      if (!fs.existsSync(this.cacheModelPath))\n        return MODEL_MAP.get(\"gemini\", this.model) ?? 30_720;\n      const models = safeJsonParse(fs.readFileSync(this.cacheModelPath));\n      const model = models.find((model) => model.id === this.model);\n      if (!model)\n        throw new Error(\n          \"Model not found in cache - falling back to default model.\"\n        );\n      return model.contextWindow;\n    } catch (e) {\n      console.error(`GeminiLLM:promptWindowLimit`, e.message);\n      return MODEL_MAP.get(\"gemini\", this.model) ?? 30_720;\n    }\n  }\n\n  /**\n   * Checks if a model is experimental by reading from the cache if available, otherwise it will perform\n   * a blind check against the v1BetaModels list - which is manually maintained and updated.\n   * @param {string} modelName - The name of the model to check\n   * @returns {boolean} A boolean indicating if the model is experimental\n   */\n  isExperimentalModel(modelName) {\n    if (\n      fs.existsSync(cacheFolder) &&\n      fs.existsSync(path.resolve(cacheFolder, \"models.json\"))\n    ) {\n      const models = safeJsonParse(\n        fs.readFileSync(path.resolve(cacheFolder, \"models.json\"))\n      );\n      const model = models.find((model) => model.id === modelName);\n      if (!model) return false;\n      return model.experimental;\n    }\n\n    return modelName.includes(\"exp\") || v1BetaModels.includes(modelName);\n  }\n\n  /**\n   * Fetches Gemini models from the Google Generative AI API\n   * @param {string} apiKey - The API key to use for the request\n   * @param {number} limit - The maximum number of models to fetch\n   * @param {string} pageToken - The page token to use for pagination\n   * @returns {Promise<[{id: string, name: string, contextWindow: number, experimental: boolean}]>} A promise that resolves to an array of Gemini models\n   */\n  static async fetchModels(apiKey, limit = 1_000, pageToken = null) {\n    if (!apiKey) return [];\n    if (fs.existsSync(cacheFolder) && !this.cacheIsStale()) {\n      console.log(\n        `\\x1b[32m[GeminiLLM]\\x1b[0m Using cached models API response.`\n      );\n      return safeJsonParse(\n        fs.readFileSync(path.resolve(cacheFolder, \"models.json\"))\n      );\n    }\n\n    const stableModels = [];\n    const allModels = [];\n\n    // Fetch from v1\n    try {\n      const url = new URL(\n        \"https://generativelanguage.googleapis.com/v1/models\"\n      );\n      url.searchParams.set(\"pageSize\", limit);\n      url.searchParams.set(\"key\", apiKey);\n      if (pageToken) url.searchParams.set(\"pageToken\", pageToken);\n      await fetch(url.toString(), {\n        method: \"GET\",\n        headers: { \"Content-Type\": \"application/json\" },\n      })\n        .then((res) => res.json())\n        .then((data) => {\n          if (data.error) throw new Error(data.error.message);\n          return data.models ?? [];\n        })\n        .then((models) => {\n          return models\n            .filter(\n              (model) => !model.displayName?.toLowerCase()?.includes(\"tuning\")\n            ) // remove tuning models\n            .filter(\n              (model) =>\n                !model.description?.toLowerCase()?.includes(\"deprecated\")\n            ) // remove deprecated models (in comment)\n            .filter((model) =>\n              //  Only generateContent is supported\n              model.supportedGenerationMethods.includes(\"generateContent\")\n            )\n            .map((model) => {\n              stableModels.push(model.name);\n              allModels.push({\n                id: model.name.split(\"/\").pop(),\n                name: model.displayName,\n                contextWindow: model.inputTokenLimit,\n                experimental: false,\n              });\n            });\n        })\n        .catch((e) => {\n          console.error(`Gemini:getGeminiModelsV1`, e.message);\n          return;\n        });\n    } catch (e) {\n      console.error(`Gemini:getGeminiModelsV1`, e.message);\n    }\n\n    // Fetch from v1beta\n    try {\n      const url = new URL(\n        \"https://generativelanguage.googleapis.com/v1beta/models\"\n      );\n      url.searchParams.set(\"pageSize\", limit);\n      url.searchParams.set(\"key\", apiKey);\n      if (pageToken) url.searchParams.set(\"pageToken\", pageToken);\n      await fetch(url.toString(), {\n        method: \"GET\",\n        headers: { \"Content-Type\": \"application/json\" },\n      })\n        .then((res) => res.json())\n        .then((data) => {\n          if (data.error) throw new Error(data.error.message);\n          return data.models ?? [];\n        })\n        .then((models) => {\n          return models\n            .filter((model) => !stableModels.includes(model.name)) // remove stable models that are already in the v1 list\n            .filter(\n              (model) => !model.displayName?.toLowerCase()?.includes(\"tuning\")\n            ) // remove tuning models\n            .filter(\n              (model) =>\n                !model.description?.toLowerCase()?.includes(\"deprecated\")\n            ) // remove deprecated models (in comment)\n            .filter((model) =>\n              //  Only generateContent is supported\n              model.supportedGenerationMethods.includes(\"generateContent\")\n            )\n            .map((model) => {\n              allModels.push({\n                id: model.name.split(\"/\").pop(),\n                name: model.displayName,\n                contextWindow: model.inputTokenLimit,\n                experimental: true,\n              });\n            });\n        })\n        .catch((e) => {\n          console.error(`Gemini:getGeminiModelsV1beta`, e.message);\n          return;\n        });\n    } catch (e) {\n      console.error(`Gemini:getGeminiModelsV1beta`, e.message);\n    }\n\n    if (allModels.length === 0) {\n      console.error(`Gemini:getGeminiModels - No models found`);\n      return defaultGeminiModels();\n    }\n\n    console.log(\n      `\\x1b[32m[GeminiLLM]\\x1b[0m Writing cached models API response to disk.`\n    );\n    if (!fs.existsSync(cacheFolder))\n      fs.mkdirSync(cacheFolder, { recursive: true });\n    fs.writeFileSync(\n      path.resolve(cacheFolder, \"models.json\"),\n      JSON.stringify(allModels)\n    );\n    fs.writeFileSync(\n      path.resolve(cacheFolder, \".cached_at\"),\n      new Date().getTime().toString()\n    );\n\n    return allModels;\n  }\n\n  /**\n   * Checks if a model is valid for chat completion (unused)\n   * @deprecated\n   * @param {string} modelName - The name of the model to check\n   * @returns {Promise<boolean>} A promise that resolves to a boolean indicating if the model is valid\n   */\n  async isValidChatCompletionModel(modelName = \"\") {\n    const models = await this.fetchModels(process.env.GEMINI_API_KEY);\n    return models.some((model) => model.id === modelName);\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) return userPrompt;\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"high\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [], // This is the specific attachment for only this prompt\n  }) {\n    let prompt = [];\n    if (this.supportsSystemPrompt) {\n      prompt.push({\n        role: \"system\",\n        content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n      });\n    } else {\n      this.#log(\n        `${this.model} - does not support system prompts - emulating...`\n      );\n      prompt.push(\n        {\n          role: \"user\",\n          content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n        },\n        {\n          role: \"assistant\",\n          content: \"Okay.\",\n        }\n      );\n    }\n\n    return [\n      ...prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature: temperature,\n        })\n        .catch((e) => {\n          console.error(e);\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature: temperature,\n        stream_options: {\n          include_usage: true,\n        },\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n}\n\nmodule.exports = {\n  GeminiLLM,\n  NO_SYSTEM_PROMPT_MODELS,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/gemini/syncStaticLists.mjs",
    "content": "/**\n * This is a script that syncs the static lists of models from the Gemini API\n * so that maintainers can keep the fallback lists up to date.\n * \n * To run, cd into this directory and run:\n * node syncStaticLists.mjs\n */\n\nimport fs from \"fs\";\nimport path from \"path\";\nimport dotenv from \"dotenv\";\n\ndotenv.config({ path: `../../../.env.development` });\nconst existingCachePath = path.resolve('../../../storage/models/gemini')\n\n// This will fetch all of the models from the Gemini API as well as post-process them\n// to remove any models that are deprecated or experimental.\nimport { GeminiLLM } from \"./index.js\";\n\nif (fs.existsSync(existingCachePath)) {\n  console.log(\"Removing existing cache so we can fetch fresh models from Gemini endpoints...\");\n  fs.rmSync(existingCachePath, { recursive: true, force: true });\n}\n\nconst models = await GeminiLLM.fetchModels(process.env.GEMINI_API_KEY);\n\nfunction updateDefaultModelsFile(models) {\n  const stableModelKeys = models.filter((model) => !model.experimental).map((model) => model.id);\n  const v1BetaModelKeys = models.filter((model) => model.experimental).map((model) => model.id);\n\n  let defaultModelFileContents = fs.readFileSync(path.join(\"./defaultModels.js\"), \"utf8\");\n\n  // Update the stable models between %STABLE_MODELS% and %EOC_STABLE_MODELS% comments\n  defaultModelFileContents = defaultModelFileContents.replace(\n    /%STABLE_MODELS%[\\s\\S]*?%EOC_STABLE_MODELS%/,\n    `%STABLE_MODELS% - updated ${new Date().toISOString()}\\n\"${stableModelKeys.join('\",\\n\"')}\",\\n// %EOC_STABLE_MODELS%`\n  );\n\n  // Update the v1beta models between %V1BETA_MODELS% and %EOC_V1BETA_MODELS% comments\n  defaultModelFileContents = defaultModelFileContents.replace(\n    /%V1BETA_MODELS%[\\s\\S]*?%EOC_V1BETA_MODELS%/,\n    `%V1BETA_MODELS% - updated ${new Date().toISOString()}\\n\"${v1BetaModelKeys.join('\",\\n\"')}\",\\n// %EOC_V1BETA_MODELS%`\n  );\n\n  fs.writeFileSync(path.join(\"./defaultModels.js\"), defaultModelFileContents);\n  console.log(\"Updated defaultModels.js. Dont forget to `yarn lint` and commit!\");\n}\nupdateDefaultModelsFile(models);\n"
  },
  {
    "path": "server/utils/AiProviders/genericOpenAi/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst {\n  formatChatHistory,\n  writeResponseChunk,\n  clientAbortedHandler,\n} = require(\"../../helpers/chat/responses\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst { toValidNumber } = require(\"../../http\");\nconst { getAnythingLLMUserAgent } = require(\"../../../endpoints/utils\");\n\nclass GenericOpenAiLLM {\n  constructor(embedder = null, modelPreference = null) {\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    if (!process.env.GENERIC_OPEN_AI_BASE_PATH)\n      throw new Error(\n        \"GenericOpenAI must have a valid base path to use for the api.\"\n      );\n\n    this.className = \"GenericOpenAiLLM\";\n    this.basePath = process.env.GENERIC_OPEN_AI_BASE_PATH;\n    this.openai = new OpenAIApi({\n      baseURL: this.basePath,\n      apiKey: process.env.GENERIC_OPEN_AI_API_KEY ?? null,\n      defaultHeaders: {\n        \"User-Agent\": getAnythingLLMUserAgent(),\n        ...GenericOpenAiLLM.parseCustomHeaders(),\n      },\n    });\n    this.model =\n      modelPreference ?? process.env.GENERIC_OPEN_AI_MODEL_PREF ?? null;\n    this.maxTokens = process.env.GENERIC_OPEN_AI_MAX_TOKENS\n      ? toValidNumber(process.env.GENERIC_OPEN_AI_MAX_TOKENS, 1024)\n      : 1024;\n    if (!this.model)\n      throw new Error(\"GenericOpenAI must have a valid model set.\");\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.log(`Inference API: ${this.basePath} Model: ${this.model}`);\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Parses custom headers from a CSV-formatted environment variable.\n   * Format: \"Header-Name:value,Another-Header:value2\"\n   * @returns {Object} Object with header key-value pairs\n   */\n  static parseCustomHeaders() {\n    const customHeadersEnv = process.env.GENERIC_OPEN_AI_CUSTOM_HEADERS;\n    if (!customHeadersEnv) return {};\n\n    const headers = {};\n    const pairs = customHeadersEnv.split(\",\");\n\n    for (const pair of pairs) {\n      const colonIndex = pair.indexOf(\":\"); // only split on first colon for key/value separation\n      if (colonIndex === -1) continue;\n\n      const key = pair.substring(0, colonIndex).trim();\n      const value = pair.substring(colonIndex + 1).trim();\n\n      if (key && value) headers[key] = value;\n    }\n\n    return headers;\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    if (process.env.GENERIC_OPENAI_STREAMING_DISABLED === \"true\") return false;\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(_modelName) {\n    const limit = process.env.GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No token context limit was set.\");\n    return Number(limit);\n  }\n\n  // Ensure the user set a value for the token limit\n  // and if undefined - assume 4096 window.\n  promptWindowLimit() {\n    const limit = process.env.GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No token context limit was set.\");\n    return Number(limit);\n  }\n\n  // Short circuit since we have no idea if the model is valid or not\n  // in pre-flight for generic endpoints\n  isValidChatCompletionModel(_modelName = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   *\n   * ## Developer Note\n   * This function assumes the generic OpenAI provider is _actually_ OpenAI compatible.\n   * For example, Ollama is \"OpenAI compatible\" but does not support images as a content array.\n   * The contentString also is the base64 string WITH `data:image/xxx;base64,` prefix, which may not be the case for all providers.\n   * If your provider does not work exactly this way, then attachments will not function or potentially break vision requests.\n   * If you encounter this issue, you are welcome to open an issue asking for your specific provider to be supported.\n   *\n   * This function will **not** be updated for providers that **do not** support images as a content array like OpenAI does.\n   * Do not open issues to update this function due to your specific provider not being compatible. Open an issue to request support for your specific provider.\n   * @param {Object} props\n   * @param {string} props.userPrompt - the user prompt to be sent to the model\n   * @param {import(\"../../helpers\").Attachment[]} props.attachments - the array of attachments to be sent to the model\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"high\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  /**\n   * Extracts accurate generation-only timing and token count from a llama.cpp\n   * response or streaming chunk. Mutates the provided usage object in place\n   * so it can be used by both streaming and non-streaming code paths.\n   * @param {Object} response - the API response or final streaming chunk\n   * @param {Object} usage - the usage object to mutate\n   */\n  #extractLlamaCppTimings(response, usage) {\n    if (!response || !response.timings) return;\n\n    if (response.timings.hasOwnProperty(\"predicted_n\"))\n      usage.completion_tokens = Number(response.timings.predicted_n);\n\n    if (response.timings.hasOwnProperty(\"predicted_ms\"))\n      usage.duration = Number(response.timings.predicted_ms) / 1000;\n  }\n\n  /**\n   * Parses and prepends reasoning from the response and returns the full text response.\n   * @param {Object} response\n   * @returns {string}\n   */\n  #parseReasoningFromResponse({ message }) {\n    let textResponse = message?.content;\n    if (\n      !!message?.reasoning_content &&\n      message.reasoning_content.trim().length > 0\n    )\n      textResponse = `<think>${message.reasoning_content}</think>${textResponse}`;\n    return textResponse;\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n          max_tokens: this.maxTokens,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    const usage = {\n      prompt_tokens: result.output?.usage?.prompt_tokens || 0,\n      completion_tokens: result.output?.usage?.completion_tokens || 0,\n      total_tokens: result.output?.usage?.total_tokens || 0,\n      duration: result.duration,\n    };\n    this.#extractLlamaCppTimings(result.output, usage);\n\n    return {\n      textResponse: this.#parseReasoningFromResponse(result.output.choices[0]),\n      metrics: {\n        ...usage,\n        outputTps: usage.completion_tokens / usage.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n        max_tokens: this.maxTokens,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  // TODO: This is a copy of the generic handleStream function in responses.js\n  // to specifically handle the DeepSeek reasoning model `reasoning_content` field.\n  // When or if ever possible, we should refactor this to be in the generic function.\n  handleStream(response, stream, responseProps) {\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n    let hasUsageMetrics = false;\n    let usage = {\n      completion_tokens: 0,\n    };\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n      let reasoningText = \"\";\n\n      // Establish listener to early-abort a streaming response\n      // in case things go sideways or the user does not like the response.\n      // We preserve the generated text but continue as if chat was completed\n      // to preserve previously generated content.\n      const handleAbort = () => {\n        stream?.endMeasurement(usage);\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      try {\n        for await (const chunk of stream) {\n          const message = chunk?.choices?.[0];\n          const token = message?.delta?.content;\n          const reasoningToken = message?.delta?.reasoning_content;\n\n          if (\n            chunk.hasOwnProperty(\"usage\") && // exists\n            !!chunk.usage && // is not null\n            Object.values(chunk.usage).length > 0 // has values\n          ) {\n            if (chunk.usage.hasOwnProperty(\"prompt_tokens\")) {\n              usage.prompt_tokens = Number(chunk.usage.prompt_tokens);\n            }\n\n            if (chunk.usage.hasOwnProperty(\"completion_tokens\")) {\n              hasUsageMetrics = true; // to stop estimating counter\n              usage.completion_tokens = Number(chunk.usage.completion_tokens);\n            }\n          }\n\n          // Reasoning models will always return the reasoning text before the token text.\n          if (reasoningToken) {\n            // If the reasoning text is empty (''), we need to initialize it\n            // and send the first chunk of reasoning text.\n            if (reasoningText.length === 0) {\n              writeResponseChunk(response, {\n                uuid,\n                sources: [],\n                type: \"textResponseChunk\",\n                textResponse: `<think>${reasoningToken}`,\n                close: false,\n                error: false,\n              });\n              reasoningText += `<think>${reasoningToken}`;\n              continue;\n            } else {\n              writeResponseChunk(response, {\n                uuid,\n                sources: [],\n                type: \"textResponseChunk\",\n                textResponse: reasoningToken,\n                close: false,\n                error: false,\n              });\n              reasoningText += reasoningToken;\n            }\n          }\n\n          // If the reasoning text is not empty, but the reasoning token is empty\n          // and the token text is not empty we need to close the reasoning text and begin sending the token text.\n          if (!!reasoningText && !reasoningToken && token) {\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: `</think>`,\n              close: false,\n              error: false,\n            });\n            fullText += `${reasoningText}</think>`;\n            reasoningText = \"\";\n          }\n\n          if (token) {\n            fullText += token;\n            // If we never saw a usage metric, we can estimate them by number of completion chunks\n            if (!hasUsageMetrics) usage.completion_tokens++;\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: token,\n              close: false,\n              error: false,\n            });\n          }\n\n          if (\n            message?.hasOwnProperty(\"finish_reason\") && // Got valid message and it is an object with finish_reason\n            message.finish_reason !== \"\" &&\n            message.finish_reason !== null\n          ) {\n            writeResponseChunk(response, {\n              uuid,\n              sources,\n              type: \"textResponseChunk\",\n              textResponse: \"\",\n              close: true,\n              error: false,\n            });\n            this.#extractLlamaCppTimings(chunk, usage);\n\n            response.removeListener(\"close\", handleAbort);\n            stream?.endMeasurement(usage);\n            resolve(fullText);\n            break; // Break streaming when a valid finish_reason is first encountered\n          }\n        }\n      } catch (e) {\n        console.log(`\\x1b[43m\\x1b[34m[STREAMING ERROR]\\x1b[0m ${e.message}`);\n        writeResponseChunk(response, {\n          uuid,\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: e.message,\n        });\n        stream?.endMeasurement(usage);\n        resolve(fullText);\n      }\n    });\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  GenericOpenAiLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/giteeai/index.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst { safeJsonParse, toValidNumber } = require(\"../../http\");\nconst LEGACY_MODEL_MAP = require(\"../modelMap/legacy\");\nconst { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst {\n  writeResponseChunk,\n  clientAbortedHandler,\n} = require(\"../../helpers/chat/responses\");\nconst cacheFolder = path.resolve(\n  process.env.STORAGE_DIR\n    ? path.resolve(process.env.STORAGE_DIR, \"models\", \"giteeai\")\n    : path.resolve(__dirname, `../../../storage/models/giteeai`)\n);\n\nclass GiteeAILLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.GITEE_AI_API_KEY)\n      throw new Error(\"No Gitee AI API key was set.\");\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n\n    this.className = \"GiteeAILLM\";\n    this.openai = new OpenAIApi({\n      apiKey: process.env.GITEE_AI_API_KEY,\n      baseURL: \"https://ai.gitee.com/v1\",\n    });\n    this.model = modelPreference || process.env.GITEE_AI_MODEL_PREF || \"\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n\n    if (!fs.existsSync(cacheFolder))\n      fs.mkdirSync(cacheFolder, { recursive: true });\n    this.cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n    this.cacheAtPath = path.resolve(cacheFolder, \".cached_at\");\n    this.log(\"Initialized with model:\", this.model);\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.constructor.name}]\\x1b[0m ${text}`, ...args);\n  }\n\n  models() {\n    if (!fs.existsSync(this.cacheModelPath)) return {};\n    return safeJsonParse(\n      fs.readFileSync(this.cacheModelPath, { encoding: \"utf-8\" }),\n      {}\n    );\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(model) {\n    return (\n      toValidNumber(process.env.GITEE_AI_MODEL_TOKEN_LIMIT) ||\n      LEGACY_MODEL_MAP.giteeai[model] ||\n      8192\n    );\n  }\n\n  promptWindowLimit() {\n    return (\n      toValidNumber(process.env.GITEE_AI_MODEL_TOKEN_LIMIT) ||\n      LEGACY_MODEL_MAP.giteeai[this.model] ||\n      8192\n    );\n  }\n\n  async isValidChatCompletionModel(_modelName = \"\") {\n    return true;\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [prompt, ...chatHistory, { role: \"user\", content: userPrompt }];\n  }\n\n  /**\n   * Parses and prepends reasoning from the response and returns the full text response.\n   * @param {Object} response\n   * @returns {string}\n   */\n  #parseReasoningFromResponse({ message }) {\n    let textResponse = message?.content;\n    if (\n      !!message?.reasoning_content &&\n      message.reasoning_content.trim().length > 0\n    )\n      textResponse = `<think>${message.reasoning_content}</think>${textResponse}`;\n    return textResponse;\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result?.output?.hasOwnProperty(\"choices\") ||\n      result?.output?.choices?.length === 0\n    )\n      throw new Error(\n        `Invalid response body returned from GiteeAI: ${JSON.stringify(result.output)}`\n      );\n\n    return {\n      textResponse: this.#parseReasoningFromResponse(result.output.choices[0]),\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  // TODO: This is a copy of the generic handleStream function in responses.js\n  // to specifically handle the GiteeAI reasoning model `reasoning_content` field.\n  // When or if ever possible, we should refactor this to be in the generic function.\n  handleStream(response, stream, responseProps) {\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n    let hasUsageMetrics = false;\n    let usage = {\n      completion_tokens: 0,\n    };\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n      let reasoningText = \"\";\n\n      // Establish listener to early-abort a streaming response\n      // in case things go sideways or the user does not like the response.\n      // We preserve the generated text but continue as if chat was completed\n      // to preserve previously generated content.\n      const handleAbort = () => {\n        stream?.endMeasurement(usage);\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      try {\n        for await (const chunk of stream) {\n          const message = chunk?.choices?.[0];\n          const token = message?.delta?.content;\n          const reasoningToken = message?.delta?.reasoning_content;\n\n          if (\n            chunk.hasOwnProperty(\"usage\") && // exists\n            !!chunk.usage && // is not null\n            Object.values(chunk.usage).length > 0 // has values\n          ) {\n            if (chunk.usage.hasOwnProperty(\"prompt_tokens\")) {\n              usage.prompt_tokens = Number(chunk.usage.prompt_tokens);\n            }\n\n            if (chunk.usage.hasOwnProperty(\"completion_tokens\")) {\n              hasUsageMetrics = true; // to stop estimating counter\n              usage.completion_tokens = Number(chunk.usage.completion_tokens);\n            }\n          }\n\n          // Reasoning models will always return the reasoning text before the token text.\n          if (reasoningToken) {\n            // If the reasoning text is empty (''), we need to initialize it\n            // and send the first chunk of reasoning text.\n            if (reasoningText.length === 0) {\n              writeResponseChunk(response, {\n                uuid,\n                sources: [],\n                type: \"textResponseChunk\",\n                textResponse: `<think>${reasoningToken}`,\n                close: false,\n                error: false,\n              });\n              reasoningText += `<think>${reasoningToken}`;\n              continue;\n            } else {\n              writeResponseChunk(response, {\n                uuid,\n                sources: [],\n                type: \"textResponseChunk\",\n                textResponse: reasoningToken,\n                close: false,\n                error: false,\n              });\n              reasoningText += reasoningToken;\n            }\n          }\n\n          // If the reasoning text is not empty, but the reasoning token is empty\n          // and the token text is not empty we need to close the reasoning text and begin sending the token text.\n          if (!!reasoningText && !reasoningToken && token) {\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: `</think>`,\n              close: false,\n              error: false,\n            });\n            fullText += `${reasoningText}</think>`;\n            reasoningText = \"\";\n          }\n\n          if (token) {\n            fullText += token;\n            // If we never saw a usage metric, we can estimate them by number of completion chunks\n            if (!hasUsageMetrics) usage.completion_tokens++;\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: token,\n              close: false,\n              error: false,\n            });\n          }\n\n          // LocalAi returns '' and others return null on chunks - the last chunk is not \"\" or null.\n          // Either way, the key `finish_reason` must be present to determine ending chunk.\n          if (\n            message?.hasOwnProperty(\"finish_reason\") && // Got valid message and it is an object with finish_reason\n            message.finish_reason !== \"\" &&\n            message.finish_reason !== null\n          ) {\n            writeResponseChunk(response, {\n              uuid,\n              sources,\n              type: \"textResponseChunk\",\n              textResponse: \"\",\n              close: true,\n              error: false,\n            });\n            response.removeListener(\"close\", handleAbort);\n            stream?.endMeasurement(usage);\n            resolve(fullText);\n            break; // Break streaming when a valid finish_reason is first encountered\n          }\n        }\n      } catch (e) {\n        console.log(`\\x1b[43m\\x1b[34m[STREAMING ERROR]\\x1b[0m ${e.message}`);\n        writeResponseChunk(response, {\n          uuid,\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: e.message,\n        });\n        stream?.endMeasurement(usage);\n        resolve(fullText); // Return what we currently have - if anything.\n      }\n    });\n  }\n\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nasync function giteeAiModels() {\n  const url = new URL(\"https://ai.gitee.com/v1/models\");\n  url.searchParams.set(\"type\", \"text2text\");\n  return await fetch(url.toString(), {\n    method: \"GET\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${process.env.GITEE_AI_API_KEY}`,\n    },\n  })\n    .then((res) => res.json())\n    .then(({ data = [] }) => data)\n    .then((models = []) => {\n      const validModels = {};\n      models.forEach(\n        (model) =>\n          (validModels[model.id] = {\n            id: model.id,\n            name: model.id,\n            organization: model.owned_by,\n          })\n      );\n      // Cache all response information\n      if (!fs.existsSync(cacheFolder))\n        fs.mkdirSync(cacheFolder, { recursive: true });\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \"models.json\"),\n        JSON.stringify(validModels),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \".cached_at\"),\n        String(Number(new Date())),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n\n      return validModels;\n    })\n    .catch((e) => {\n      console.error(e);\n      return {};\n    });\n}\n\nmodule.exports = {\n  GiteeAILLM,\n  giteeAiModels,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/groq/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst {\n  handleDefaultStreamResponseV2,\n} = require(\"../../helpers/chat/responses\");\nconst { MODEL_MAP } = require(\"../modelMap\");\n\nclass GroqLLM {\n  constructor(embedder = null, modelPreference = null) {\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    if (!process.env.GROQ_API_KEY) throw new Error(\"No Groq API key was set.\");\n    this.className = \"GroqLLM\";\n\n    this.openai = new OpenAIApi({\n      baseURL: \"https://api.groq.com/openai/v1\",\n      apiKey: process.env.GROQ_API_KEY,\n    });\n    this.model =\n      modelPreference || process.env.GROQ_MODEL_PREF || \"llama-3.1-8b-instant\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[32m[GroqAi]\\x1b[0m ${text}`, ...args);\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    return MODEL_MAP.get(\"groq\", modelName) ?? 8192;\n  }\n\n  promptWindowLimit() {\n    return MODEL_MAP.get(\"groq\", this.model) ?? 8192;\n  }\n\n  async isValidChatCompletionModel(modelName = \"\") {\n    return !!modelName; // name just needs to exist\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) return userPrompt;\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Last Updated: October 21, 2024\n   * According to https://console.groq.com/docs/vision\n   * the vision models supported all make a mess of prompting depending on the model.\n   * Currently the llama3.2 models are only in preview and subject to change and the llava model is deprecated - so we will not support attachments for that at all.\n   *\n   * Since we can only explicitly support the current models, this is a temporary solution.\n   * If the attachments are empty or the model is not a vision model, we will return the default prompt structure which will work for all models.\n   * If the attachments are present and the model is a vision model - we only return the user prompt with attachments - see comment at end of function for more.\n   *\n   * Historical attachments are also omitted from prompt chat history for the reasons above. (TDC: Dec 30, 2024)\n   */\n  #conditionalPromptStruct({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [], // This is the specific attachment for only this prompt\n  }) {\n    const VISION_MODELS = [\n      \"llama-3.2-90b-vision-preview\",\n      \"llama-3.2-11b-vision-preview\",\n    ];\n    const DEFAULT_PROMPT_STRUCT = [\n      {\n        role: \"system\",\n        content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n      },\n      ...chatHistory,\n      { role: \"user\", content: userPrompt },\n    ];\n\n    // If there are no attachments or model is not a vision model, return the default prompt structure\n    // as there is nothing to attach or do and no model limitations to consider\n    if (!attachments.length) return DEFAULT_PROMPT_STRUCT;\n    if (!VISION_MODELS.includes(this.model)) {\n      this.#log(\n        `${this.model} is not an explicitly supported vision model! Will omit attachments.`\n      );\n      return DEFAULT_PROMPT_STRUCT;\n    }\n\n    return [\n      // Why is the system prompt and history commented out?\n      // The current vision models for Groq perform VERY poorly with ANY history or text prior to the image.\n      // In order to not get LLM refusals for every single message, we will not include the \"system prompt\" or even the chat history.\n      // This is a temporary solution until Groq fixes their vision models to be more coherent and also handle context prior to the image.\n      // Note for the future:\n      // Groq vision models also do not support system prompts - which is why you see the user/assistant emulation used instead of \"system\".\n      // This means any vision call is assessed independently of the chat context prior to the image.\n      /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\n      // {\n      //   role: \"user\",\n      //   content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n      // },\n      // {\n      //   role: \"assistant\",\n      //   content: \"OK\",\n      // },\n      // ...chatHistory,\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [], // This is the specific attachment for only this prompt\n  }) {\n    // NOTICE: SEE GroqLLM.#conditionalPromptStruct for more information on how attachments are handled with Groq.\n    return this.#conditionalPromptStruct({\n      systemPrompt,\n      contextTexts,\n      chatHistory,\n      userPrompt,\n      attachments,\n    });\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `GroqAI:chatCompletion: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps:\n          result.output.usage.completion_tokens /\n          result.output.usage.completion_time,\n        duration: result.output.usage.total_time,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `GroqAI:streamChatCompletion: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  GroqLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/huggingface/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst {\n  handleDefaultStreamResponseV2,\n} = require(\"../../helpers/chat/responses\");\n\nclass HuggingFaceLLM {\n  constructor(embedder = null, _modelPreference = null) {\n    if (!process.env.HUGGING_FACE_LLM_ENDPOINT)\n      throw new Error(\"No HuggingFace Inference Endpoint was set.\");\n    if (!process.env.HUGGING_FACE_LLM_API_KEY)\n      throw new Error(\"No HuggingFace Access Token was set.\");\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n\n    this.className = \"HuggingFaceLLM\";\n    this.openai = new OpenAIApi({\n      baseURL: `${process.env.HUGGING_FACE_LLM_ENDPOINT}/v1`,\n      apiKey: process.env.HUGGING_FACE_LLM_API_KEY,\n    });\n    // When using HF inference server - the model param is not required so\n    // we can stub it here. HF Endpoints can only run one model at a time.\n    // We set to 'tgi' so that endpoint for HF can accept message format\n    this.model = \"tgi\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.2;\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(_modelName) {\n    const limit = process.env.HUGGING_FACE_LLM_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No HuggingFace token context limit was set.\");\n    return Number(limit);\n  }\n\n  promptWindowLimit() {\n    const limit = process.env.HUGGING_FACE_LLM_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No HuggingFace token context limit was set.\");\n    return Number(limit);\n  }\n\n  async isValidChatCompletionModel(_ = \"\") {\n    return true;\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n  }) {\n    // System prompt it not enabled for HF model chats\n    const prompt = {\n      role: \"user\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    const assistantResponse = {\n      role: \"assistant\",\n      content: \"Okay, I will follow those instructions\",\n    };\n    return [\n      prompt,\n      assistantResponse,\n      ...chatHistory,\n      { role: \"user\", content: userPrompt },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage?.prompt_tokens || 0,\n        completion_tokens: result.output.usage?.completion_tokens || 0,\n        total_tokens: result.output.usage?.total_tokens || 0,\n        outputTps:\n          (result.output.usage?.completion_tokens || 0) / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  HuggingFaceLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/koboldCPP/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  clientAbortedHandler,\n  writeResponseChunk,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst { v4: uuidv4 } = require(\"uuid\");\n\nclass KoboldCPPLLM {\n  constructor(embedder = null, modelPreference = null) {\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    if (!process.env.KOBOLD_CPP_BASE_PATH)\n      throw new Error(\n        \"KoboldCPP must have a valid base path to use for the api.\"\n      );\n\n    this.className = \"KoboldCPPLLM\";\n    this.basePath = process.env.KOBOLD_CPP_BASE_PATH;\n    this.openai = new OpenAIApi({\n      baseURL: this.basePath,\n      apiKey: null,\n    });\n    this.model = modelPreference ?? process.env.KOBOLD_CPP_MODEL_PREF ?? null;\n    if (!this.model) throw new Error(\"KoboldCPP must have a valid model set.\");\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.maxTokens = Number(process.env.KOBOLD_CPP_MAX_TOKENS) || 2048;\n    this.log(`Inference API: ${this.basePath} Model: ${this.model}`);\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(_modelName) {\n    const limit = process.env.KOBOLD_CPP_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No token context limit was set.\");\n    return Number(limit);\n  }\n\n  // Ensure the user set a value for the token limit\n  // and if undefined - assume 4096 window.\n  promptWindowLimit() {\n    const limit = process.env.KOBOLD_CPP_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No token context limit was set.\");\n    return Number(limit);\n  }\n\n  // Short circuit since we have no idea if the model is valid or not\n  // in pre-flight for generic endpoints\n  isValidChatCompletionModel(_modelName = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n          max_tokens: this.maxTokens,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    const promptTokens = LLMPerformanceMonitor.countTokens(messages);\n    const completionTokens = LLMPerformanceMonitor.countTokens([\n      { content: result.output.choices[0].message.content },\n    ]);\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: promptTokens,\n        completion_tokens: completionTokens,\n        total_tokens: promptTokens + completionTokens,\n        outputTps: completionTokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n        max_tokens: this.maxTokens,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n      let usage = {\n        prompt_tokens: LLMPerformanceMonitor.countTokens(stream.messages || []),\n        completion_tokens: 0,\n      };\n\n      const handleAbort = () => {\n        usage.completion_tokens = LLMPerformanceMonitor.countTokens([\n          { content: fullText },\n        ]);\n        stream?.endMeasurement(usage);\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      for await (const chunk of stream) {\n        const message = chunk?.choices?.[0];\n        const token = message?.delta?.content;\n\n        if (token) {\n          fullText += token;\n          writeResponseChunk(response, {\n            uuid,\n            sources: [],\n            type: \"textResponseChunk\",\n            textResponse: token,\n            close: false,\n            error: false,\n          });\n        }\n\n        // KoboldCPP finishes with \"length\" or \"stop\"\n        if (\n          message.finish_reason !== \"null\" &&\n          (message.finish_reason === \"length\" ||\n            message.finish_reason === \"stop\")\n        ) {\n          writeResponseChunk(response, {\n            uuid,\n            sources,\n            type: \"textResponseChunk\",\n            textResponse: \"\",\n            close: true,\n            error: false,\n          });\n          response.removeListener(\"close\", handleAbort);\n          usage.completion_tokens = LLMPerformanceMonitor.countTokens([\n            { content: fullText },\n          ]);\n          stream?.endMeasurement(usage);\n          resolve(fullText);\n        }\n      }\n    });\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  KoboldCPPLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/lemonade/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  handleDefaultStreamResponseV2,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst { OpenAI: OpenAIApi } = require(\"openai\");\nconst { humanFileSize } = require(\"../../helpers\");\n\nclass LemonadeLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.LEMONADE_LLM_BASE_PATH)\n      throw new Error(\"No Lemonade API Base Path was set.\");\n    if (!process.env.LEMONADE_LLM_MODEL_PREF && !modelPreference)\n      throw new Error(\"No Lemonade Model Pref was set.\");\n\n    this.className = \"LemonadeLLM\";\n    this.lemonade = new OpenAIApi({\n      baseURL: parseLemonadeServerEndpoint(\n        process.env.LEMONADE_LLM_BASE_PATH,\n        \"openai\"\n      ),\n      apiKey: null,\n    });\n\n    this.model = modelPreference || process.env.LEMONADE_LLM_MODEL_PREF;\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n\n    // We can establish here since we cannot dynamically curl the context window limit from the API.\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n    this.#log(`initialized with model: ${this.model}`);\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[32m[Lemonade]\\x1b[0m ${text}`, ...args);\n  }\n\n  static slog(text, ...args) {\n    console.log(`\\x1b[32m[Lemonade]\\x1b[0m ${text}`, ...args);\n  }\n\n  async assertModelContextLimits() {\n    if (this.limits !== null) return;\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n    this.#log(\n      `${this.model} is using a max context window of ${this.promptWindowLimit()} tokens.`\n    );\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  /** Lemonade does not support curling the context window limit from the API, so we return the system defined limit. */\n  static promptWindowLimit(_) {\n    return Number(process.env.LEMONADE_LLM_MODEL_TOKEN_LIMIT) || 8192;\n  }\n\n  promptWindowLimit() {\n    return this.constructor.promptWindowLimit(this.model);\n  }\n\n  async isValidChatCompletionModel(_ = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"auto\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    await LemonadeLLM.loadModel(this.model);\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.lemonade.chat.completions.create({\n        model: this.model,\n        messages,\n        temperature,\n      })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage?.prompt_tokens || 0,\n        completion_tokens: result.output.usage?.completion_tokens || 0,\n        total_tokens: result.output.usage?.total_tokens || 0,\n        outputTps: result.output.usage?.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    await LemonadeLLM.loadModel(this.model);\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.lemonade.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  /**\n   * Returns the capabilities of the model.\n   * Note: This is a heuristic approach to get the capabilities of the model based on the model metadata.\n   * It is not perfect, but works since every model metadata is different and may not have key values we rely on.\n   * There is no \"capabilities\" key in the metadata via any API endpoint - so we do this.\n   * @returns {Promise<{tools: 'unknown' | boolean, reasoning: 'unknown' | boolean, imageGeneration: 'unknown' | boolean, vision: 'unknown' | boolean}>}\n   */\n  async getModelCapabilities() {\n    try {\n      const client = new OpenAIApi({\n        baseURL: parseLemonadeServerEndpoint(\n          process.env.LEMONADE_LLM_BASE_PATH,\n          \"openai\"\n        ),\n        apiKey: null,\n      });\n\n      const { labels = [] } = await client.models.retrieve(this.model);\n      return {\n        tools: labels.includes(\"tool-calling\"),\n        reasoning: labels.includes(\"reasoning\"),\n        imageGeneration: \"unknown\",\n        vision: labels.includes(\"vision\"),\n      };\n    } catch (error) {\n      console.error(\"Error getting model capabilities:\", error);\n      return {\n        tools: \"unknown\",\n        reasoning: \"unknown\",\n        imageGeneration: \"unknown\",\n        vision: \"unknown\",\n      };\n    }\n  }\n\n  /**\n   * Utility function to load a model from the Lemonade server.\n   * Does not check if the model is already loaded or unloads any models.\n   * @param {*} model\n   */\n  static async loadModel(model, basePath = process.env.LEMONADE_LLM_BASE_PATH) {\n    try {\n      const endpoint = new URL(parseLemonadeServerEndpoint(basePath, \"openai\"));\n      endpoint.pathname += \"/load\";\n\n      console.log(endpoint.toString());\n\n      LemonadeLLM.slog(\n        `Loading model ${model} with context size ${this.promptWindowLimit()}`\n      );\n      await fetch(endpoint.toString(), {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        body: JSON.stringify({\n          model_name: String(model),\n          ctx_size: Number(this.promptWindowLimit()),\n        }),\n      })\n        .then((response) => {\n          if (!response.ok)\n            throw new Error(\n              `Failed to load model ${model}: ${response.statusText}`\n            );\n          return response.json();\n        })\n        .then((data) => {\n          if (data.status !== \"success\") throw new Error(data.message);\n          LemonadeLLM.slog(`Model ${model} loaded successfully`);\n          return true;\n        });\n    } catch (error) {\n      LemonadeLLM.slog(`Error loading model ${model}:`, error);\n      return false;\n    }\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    await this.assertModelContextLimits();\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\n/**\n * Extracts the model family/organization name from a model ID.\n * For example:\n * - \"Qwen3-VL-8B-Instruct-GGUF\" → \"Qwen\"\n * - \"SmolLM3-3B-GGUF\" → \"SmolLM\"\n * - \"Llama-3.2-8B\" → \"Llama\"\n * - \"DeepSeek-V3-GGUF\" → \"DeepSeek\"\n * @param {string} modelId - The model identifier\n * @returns {string} The organization/family name\n */\nfunction extractModelOrganization(modelId) {\n  const match = modelId.match(/^([A-Za-z]+)/);\n  return match ? match[1] : modelId;\n}\n\n/**\n * Parse the base path of the Docker Model Runner endpoint and return the host and port.\n * @param {string} basePath - The base path of the Lemonade server endpoint.\n * @param {'base' | 'openai' | 'ollama'} to - The provider to parse the endpoint for (internal DMR or openai-compatible)\n * @returns {string | null}\n */\nfunction parseLemonadeServerEndpoint(basePath = null, to = \"openai\") {\n  if (!basePath) return null;\n  try {\n    const url = new URL(basePath);\n    if (to === \"openai\") url.pathname = \"api/v1\";\n    else if (to === \"ollama\") url.pathname = \"api\";\n    else if (to === \"base\") url.pathname = \"\"; // only used for /live\n    return url.toString();\n  } catch {\n    return basePath;\n  }\n}\n\n/**\n * This function will fetch the remote models from the Lemonade server as well\n * as the local models installed on the system.\n * @param {string} basePath - The base path of the Lemonade server endpoint.\n * @param {'chat' | 'embedding' | 'reranking'} task - The task to fetch the models for.\n */\nasync function getAllLemonadeModels(basePath = null, task = \"chat\") {\n  const availableModels = {};\n\n  function isValidForTask(model) {\n    if (task === \"reranking\") return model.labels?.includes(\"reranking\");\n    if (task === \"embedding\") return model.labels?.includes(\"embeddings\");\n    if (task === \"chat\")\n      return ![\"embeddings\", \"reranking\"].some((label) =>\n        model.labels?.includes(label)\n      );\n    return true;\n  }\n\n  try {\n    // Grab the locally installed models from the Lemonade server API\n    const lemonadeUrl = new URL(\n      parseLemonadeServerEndpoint(\n        basePath ?? process.env.LEMONADE_LLM_BASE_PATH,\n        \"openai\"\n      )\n    );\n    lemonadeUrl.pathname += \"/models\";\n    lemonadeUrl.searchParams.append(\"show_all\", \"true\");\n    await fetch(lemonadeUrl.toString())\n      .then((res) => res.json())\n      .then(({ data }) => {\n        data?.forEach((model) => {\n          if (!isValidForTask(model)) return;\n\n          const organization = extractModelOrganization(model.id);\n          const modelData = {\n            id: model.id,\n            name: organization + \":\" + model.id,\n            // Reports in GB, convert to bytes\n            size: model?.size\n              ? humanFileSize(model.size * 1024 ** 3)\n              : \"Unknown size\",\n            downloaded: model?.downloaded ?? false,\n            organization,\n          };\n\n          if (!availableModels[organization])\n            availableModels[organization] = { tags: [] };\n          availableModels[organization].tags.push(modelData);\n        });\n      });\n  } catch (e) {\n    LemonadeLLM.slog(`Error getting Lemonade models`, e);\n  } finally {\n    // eslint-disable-next-line\n    return Object.values(availableModels).flatMap((m) => m.tags);\n  }\n}\n\nmodule.exports = {\n  LemonadeLLM,\n  parseLemonadeServerEndpoint,\n  getAllLemonadeModels,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/liteLLM/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst {\n  handleDefaultStreamResponseV2,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\n\nclass LiteLLM {\n  constructor(embedder = null, modelPreference = null) {\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    if (!process.env.LITE_LLM_BASE_PATH)\n      throw new Error(\n        \"LiteLLM must have a valid base path to use for the api.\"\n      );\n\n    this.className = \"LiteLLM\";\n    this.basePath = process.env.LITE_LLM_BASE_PATH;\n    this.openai = new OpenAIApi({\n      baseURL: this.basePath,\n      apiKey: process.env.LITE_LLM_API_KEY ?? null,\n    });\n    this.model = modelPreference ?? process.env.LITE_LLM_MODEL_PREF ?? null;\n\n    if (!this.model) throw new Error(\"LiteLLM must have a valid model set.\");\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.log(`Inference API: ${this.basePath} Model: ${this.model}`);\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(_modelName) {\n    const limit = process.env.LITE_LLM_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No token context limit was set.\");\n    return Number(limit);\n  }\n\n  // Ensure the user set a value for the token limit\n  // and if undefined - assume 4096 window.\n  promptWindowLimit() {\n    const limit = process.env.LITE_LLM_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No token context limit was set.\");\n    return Number(limit);\n  }\n\n  // Short circuit since we have no idea if the model is valid or not\n  // in pre-flight for generic endpoints\n  isValidChatCompletionModel(_modelName = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage?.prompt_tokens || 0,\n        completion_tokens: result.output.usage?.completion_tokens || 0,\n        total_tokens: result.output.usage?.total_tokens || 0,\n        outputTps:\n          (result.output.usage?.completion_tokens || 0) / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  LiteLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/lmStudio/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  handleDefaultStreamResponseV2,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst { OpenAI: OpenAIApi } = require(\"openai\");\n\n//  hybrid of openAi LLM chat completion for LMStudio\nclass LMStudioLLM {\n  /** @see LMStudioLLM.cacheContextWindows */\n  static modelContextWindows = {};\n\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.LMSTUDIO_BASE_PATH)\n      throw new Error(\"No LMStudio API Base Path was set.\");\n\n    this.className = \"LMStudioLLM\";\n    const apiKey = process.env.LMSTUDIO_AUTH_TOKEN ?? null;\n    this.lmstudio = new OpenAIApi({\n      baseURL: parseLMStudioBasePath(process.env.LMSTUDIO_BASE_PATH), // here is the URL to your LMStudio instance\n      apiKey,\n    });\n\n    // Prior to LMStudio 0.2.17 the `model` param was not required and you could pass anything\n    // into that field and it would work. On 0.2.17 LMStudio introduced multi-model chat\n    // which now has a bug that reports the server model id as \"Loaded from Chat UI\"\n    // and any other value will crash inferencing. So until this is patched we will\n    // try to fetch the `/models` and have the user set it, or just fallback to \"Loaded from Chat UI\"\n    // which will not impact users with <v0.2.17 and should work as well once the bug is fixed.\n    this.model = modelPreference || process.env.LMSTUDIO_MODEL_PREF;\n    if (!this.model) throw new Error(\"LMStudio must have a valid model set.\");\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n\n    // Lazy load the limits to avoid blocking the main thread on cacheContextWindows\n    this.limits = null;\n\n    LMStudioLLM.cacheContextWindows(true);\n    this.#log(`initialized with model: ${this.model}`);\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[32m[LMStudio]\\x1b[0m ${text}`, ...args);\n  }\n\n  static #slog(text, ...args) {\n    console.log(`\\x1b[32m[LMStudio]\\x1b[0m ${text}`, ...args);\n  }\n\n  async assertModelContextLimits() {\n    if (this.limits !== null) return;\n    await LMStudioLLM.cacheContextWindows();\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n  }\n\n  /**\n   * Cache the context windows for the LMStudio models.\n   * This is done once and then cached for the lifetime of the server. This is absolutely necessary to ensure that the context windows are correct.\n   *\n   * This is a convenience to ensure that the context windows are correct and that the user\n   * does not have to manually set the context window for each model.\n   * @param {boolean} force - Force the cache to be refreshed.\n   * @returns {Promise<void>} - A promise that resolves when the cache is refreshed.\n   */\n  static async cacheContextWindows(force = false) {\n    try {\n      // Skip if we already have cached context windows and we're not forcing a refresh\n      if (Object.keys(LMStudioLLM.modelContextWindows).length > 0 && !force)\n        return;\n\n      const apiKey = process.env.LMSTUDIO_AUTH_TOKEN ?? null;\n      const endpoint = new URL(\n        parseLMStudioBasePath(process.env.LMSTUDIO_BASE_PATH)\n      );\n      endpoint.pathname = \"/api/v0/models\";\n      await fetch(endpoint.toString(), {\n        headers: {\n          \"Content-Type\": \"application/json\",\n          ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),\n        },\n      })\n        .then((res) => {\n          if (!res.ok)\n            throw new Error(`LMStudio:cacheContextWindows - ${res.statusText}`);\n          return res.json();\n        })\n        .then(({ data: models }) => {\n          models.forEach((model) => {\n            if (model.type === \"embeddings\") return;\n            LMStudioLLM.modelContextWindows[model.id] =\n              model.max_context_length;\n          });\n        })\n        .catch((e) => {\n          LMStudioLLM.#slog(`Error caching context windows`, e);\n          return;\n        });\n\n      LMStudioLLM.#slog(`Context windows cached for all models!`);\n    } catch (e) {\n      LMStudioLLM.#slog(`Error caching context windows`, e);\n      return;\n    }\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    if (Object.keys(LMStudioLLM.modelContextWindows).length === 0) {\n      this.#slog(\n        \"No context windows cached - Context window may be inaccurately reported.\"\n      );\n      return process.env.LMSTUDIO_MODEL_TOKEN_LIMIT || 4096;\n    }\n\n    let userDefinedLimit = null;\n    const systemDefinedLimit =\n      Number(this.modelContextWindows[modelName]) || 4096;\n\n    if (\n      process.env.LMSTUDIO_MODEL_TOKEN_LIMIT &&\n      !isNaN(Number(process.env.LMSTUDIO_MODEL_TOKEN_LIMIT)) &&\n      Number(process.env.LMSTUDIO_MODEL_TOKEN_LIMIT) > 0\n    )\n      userDefinedLimit = Number(process.env.LMSTUDIO_MODEL_TOKEN_LIMIT);\n\n    // The user defined limit is always higher priority than the context window limit, but it cannot be higher than the context window limit\n    // so we return the minimum of the two, if there is no user defined limit, we return the system defined limit as-is.\n    if (userDefinedLimit !== null)\n      return Math.min(userDefinedLimit, systemDefinedLimit);\n    return systemDefinedLimit;\n  }\n\n  promptWindowLimit() {\n    return this.constructor.promptWindowLimit(this.model);\n  }\n\n  async isValidChatCompletionModel(_ = \"\") {\n    // LMStudio may be anything. The user must do it correctly.\n    // See comment about this.model declaration in constructor\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"auto\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        `LMStudio chat: ${this.model} is not valid or defined model for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.lmstudio.chat.completions.create({\n        model: this.model,\n        messages,\n        temperature,\n      })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage?.prompt_tokens || 0,\n        completion_tokens: result.output.usage?.completion_tokens || 0,\n        total_tokens: result.output.usage?.total_tokens || 0,\n        outputTps: result.output.usage?.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        `LMStudio chat: ${this.model} is not valid or defined model for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.lmstudio.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  /**\n   * Returns the capabilities of the model.\n   * This uses the new /api/v1 endpoint, which returns the model info in a different format.\n   * @returns {Promise<{tools: 'unknown' | boolean, reasoning: 'unknown' | boolean, imageGeneration: 'unknown' | boolean, vision: 'unknown' | boolean}>}\n   */\n  async getModelCapabilities() {\n    try {\n      const endpoint = new URL(\n        parseLMStudioBasePath(process.env.LMSTUDIO_BASE_PATH, \"v1\")\n      );\n      const apiKey = process.env.LMSTUDIO_AUTH_TOKEN ?? null;\n      endpoint.pathname += \"/models\";\n      const modelInfo =\n        (await fetch(endpoint.toString(), {\n          headers: {\n            \"Content-Type\": \"application/json\",\n            ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),\n          },\n        })\n          .then((res) => {\n            if (!res.ok)\n              throw new Error(\n                `LMStudio:getModelCapabilities - ${res.statusText}`\n              );\n            return res.json();\n          })\n          .then(({ models = [] }) =>\n            models.find((model) => model.key === this.model)\n          )) || {};\n\n      const capabilities = modelInfo.hasOwnProperty(\"capabilities\")\n        ? modelInfo.capabilities\n        : {\n            trained_for_tool_use: \"unknown\",\n            vision: \"unknown\",\n          };\n\n      return {\n        tools: capabilities.trained_for_tool_use,\n        reasoning: \"unknown\",\n        imageGeneration: \"unknown\", // LM Studio does not support image generation yet.\n        vision: capabilities.vision,\n      };\n    } catch (error) {\n      console.error(\"Error getting model capabilities:\", error);\n      return {\n        tools: \"unknown\",\n        reasoning: \"unknown\",\n        imageGeneration: \"unknown\",\n        vision: \"unknown\",\n      };\n    }\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    await this.assertModelContextLimits();\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\n/**\n * Parse the base path for the LMStudio API. Since the base path must end in /v1 and cannot have a trailing slash,\n * and the user can possibly set it to anything and likely incorrectly due to pasting behaviors, we need to ensure it is in the correct format.\n * @param {string} basePath\n * @param {'legacy' | 'v1'} apiVersion\n * @returns {string}\n */\nfunction parseLMStudioBasePath(providedBasePath = \"\", apiVersion = \"legacy\") {\n  try {\n    const baseURL = new URL(providedBasePath);\n    let basePath = `${baseURL.origin}`;\n    if (apiVersion === \"legacy\") basePath += `/v1`;\n    if (apiVersion === \"v1\") basePath += `/api/v1`;\n    return basePath;\n  } catch {\n    return providedBasePath;\n  }\n}\n\nmodule.exports = {\n  LMStudioLLM,\n  parseLMStudioBasePath,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/localAi/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst {\n  handleDefaultStreamResponseV2,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\n\nclass LocalAiLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.LOCAL_AI_BASE_PATH)\n      throw new Error(\"No LocalAI Base Path was set.\");\n\n    this.className = \"LocalAiLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.openai = new OpenAIApi({\n      baseURL: process.env.LOCAL_AI_BASE_PATH,\n      apiKey: process.env.LOCAL_AI_API_KEY ?? null,\n    });\n    this.model = modelPreference || process.env.LOCAL_AI_MODEL_PREF;\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(_modelName) {\n    const limit = process.env.LOCAL_AI_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No LocalAi token context limit was set.\");\n    return Number(limit);\n  }\n\n  // Ensure the user set a value for the token limit\n  // and if undefined - assume 4096 window.\n  promptWindowLimit() {\n    const limit = process.env.LOCAL_AI_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No LocalAi token context limit was set.\");\n    return Number(limit);\n  }\n\n  async isValidChatCompletionModel(_ = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `LocalAI chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions.create({\n        model: this.model,\n        messages,\n        temperature,\n      })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    const promptTokens = LLMPerformanceMonitor.countTokens(messages);\n    const completionTokens = LLMPerformanceMonitor.countTokens(\n      result.output.choices[0].message.content\n    );\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: promptTokens,\n        completion_tokens: completionTokens,\n        total_tokens: promptTokens + completionTokens,\n        outputTps: completionTokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `LocalAi chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  LocalAiLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/mistral/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst {\n  handleDefaultStreamResponseV2,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\n\nclass MistralLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.MISTRAL_API_KEY)\n      throw new Error(\"No Mistral API key was set.\");\n\n    this.className = \"MistralLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.openai = new OpenAIApi({\n      baseURL: \"https://api.mistral.ai/v1\",\n      apiKey: process.env.MISTRAL_API_KEY ?? null,\n    });\n    this.model =\n      modelPreference || process.env.MISTRAL_MODEL_PREF || \"mistral-tiny\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.0;\n    this.log(\"Initialized with model:\", this.model);\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit() {\n    return 32000;\n  }\n\n  promptWindowLimit() {\n    return 32000;\n  }\n\n  async isValidChatCompletionModel(_modelName = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) return userPrompt;\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: attachment.contentString,\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [], // This is the specific attachment for only this prompt\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `Mistral chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `Mistral chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  MistralLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/modelMap/index.js",
    "content": "const path = require(\"path\");\nconst fs = require(\"fs\");\nconst LEGACY_MODEL_MAP = require(\"./legacy\");\n\nclass ContextWindowFinder {\n  static instance = null;\n  static modelMap = LEGACY_MODEL_MAP;\n\n  /**\n   * Mapping for AnythingLLM provider <> LiteLLM provider\n   * @type {Record<string, string>}\n   */\n  static trackedProviders = {\n    anthropic: \"anthropic\",\n    openai: \"openai\",\n    cohere: \"cohere_chat\",\n    gemini: \"vertex_ai-language-models\",\n    groq: \"groq\",\n    xai: \"xai\",\n    deepseek: \"deepseek\",\n    moonshot: \"moonshot\",\n    zai: \"vercel_ai_gateway\", // Vercel has correct context windows for Z.AI models\n    sambanova: \"sambanova\",\n  };\n  static expiryMs = 1000 * 60 * 60 * 24 * 3; // 3 days\n  static remoteUrl =\n    \"https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json\";\n\n  cacheLocation = path.resolve(\n    process.env.STORAGE_DIR\n      ? path.resolve(process.env.STORAGE_DIR, \"models\", \"context-windows\")\n      : path.resolve(__dirname, `../../../storage/models/context-windows`)\n  );\n  cacheFilePath = path.resolve(this.cacheLocation, \"context-windows.json\");\n  cacheFileExpiryPath = path.resolve(this.cacheLocation, \".cached_at\");\n  seenStaleCacheWarning = false;\n\n  constructor() {\n    if (ContextWindowFinder.instance) return ContextWindowFinder.instance;\n    ContextWindowFinder.instance = this;\n    if (!fs.existsSync(this.cacheLocation))\n      fs.mkdirSync(this.cacheLocation, { recursive: true });\n\n    // If the cache is stale or not found at all, pull the model map from remote\n    if (this.isCacheStale || !fs.existsSync(this.cacheFilePath)) {\n      this.#pullRemoteModelMap().catch((err) =>\n        this.log(\"Background model map pull failed:\", err)\n      );\n    }\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[33m[ContextWindowFinder]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Checks if the cache is stale by checking if the cache file exists and if the cache file is older than the expiry time.\n   * @returns {boolean}\n   */\n  get isCacheStale() {\n    if (!fs.existsSync(this.cacheFileExpiryPath)) return true;\n    const cachedAt = fs.readFileSync(this.cacheFileExpiryPath, \"utf8\");\n    return Date.now() - cachedAt > ContextWindowFinder.expiryMs;\n  }\n\n  /**\n   * Gets the cached model map.\n   *\n   * Always returns the available model map - even if it is expired since re-pulling\n   * the model map only occurs on container start/system start.\n   * @returns {Record<string, Record<string, number>> | null} - The cached model map\n   */\n  get cachedModelMap() {\n    if (!fs.existsSync(this.cacheFilePath)) {\n      this.log(`\\x1b[33m\n--------------------------------\n[WARNING] Model map cache is not found!\nInvalid context windows will be returned leading to inaccurate model responses\nor smaller context windows than expected.\nYou can fix this by restarting AnythingLLM so the model map is re-pulled.\n--------------------------------\\x1b[0m`);\n      return null;\n    }\n\n    if (this.isCacheStale && !this.seenStaleCacheWarning) {\n      this.log(\n        \"Model map cache is stale - some model context windows may be incorrect. This is OK and the model map will be re-pulled on next boot.\"\n      );\n      this.seenStaleCacheWarning = true;\n    }\n\n    return JSON.parse(\n      fs.readFileSync(this.cacheFilePath, { encoding: \"utf8\" })\n    );\n  }\n\n  /**\n   * Pulls the remote model map from the remote URL, formats it and caches it.\n   * @returns {Record<string, Record<string, number>>} - The formatted model map\n   */\n  async #pullRemoteModelMap() {\n    try {\n      this.log(\"Pulling remote model map...\");\n      const response = await fetch(ContextWindowFinder.remoteUrl);\n      if (response.status !== 200) {\n        throw new Error(\n          \"Failed to fetch remote model map - non 200 status code\"\n        );\n      }\n\n      const data = await response.json();\n      const modelMap = this.#validateModelMap(this.#formatModelMap(data));\n      await Promise.all([\n        fs.promises.writeFile(\n          this.cacheFilePath,\n          JSON.stringify(modelMap, null, 2)\n        ),\n        fs.promises.writeFile(this.cacheFileExpiryPath, Date.now().toString()),\n      ]);\n\n      this.log(\"Remote model map synced and cached\");\n      return modelMap;\n    } catch (error) {\n      this.log(\"Error syncing remote model map\", error);\n      return null;\n    }\n  }\n\n  #validateModelMap(modelMap = {}) {\n    for (const [provider, models] of Object.entries(modelMap)) {\n      // If the models is null/falsey or has no keys, throw an error\n      if (typeof models !== \"object\")\n        throw new Error(\n          `Invalid model map for ${provider} - models is not an object`\n        );\n      if (!models || Object.keys(models).length === 0)\n        throw new Error(`Invalid model map for ${provider} - no models found!`);\n\n      // Validate that the context window is a number\n      for (const [model, contextWindow] of Object.entries(models)) {\n        if (isNaN(contextWindow) || contextWindow <= 0) {\n          this.log(\n            `${provider}:${model} - context window is not a positive number. Got ${contextWindow}.`\n          );\n          delete models[model];\n          continue;\n        }\n      }\n    }\n    return modelMap;\n  }\n\n  /**\n   * Formats the remote model map to a format that is compatible with how we store the model map\n   * for all providers who use it.\n   * @param {Record<string, any>} modelMap - The remote model map\n   * @returns {Record<string, Record<string, number>>} - The formatted model map\n   */\n  #formatModelMap(modelMap = {}) {\n    const formattedModelMap = {};\n\n    for (const [provider, liteLLMProviderTag] of Object.entries(\n      ContextWindowFinder.trackedProviders\n    )) {\n      formattedModelMap[provider] = {};\n      const matches = Object.entries(modelMap).filter(\n        ([_key, config]) => config.litellm_provider === liteLLMProviderTag\n      );\n      for (const [key, config] of matches) {\n        const contextWindow = Number(config.max_input_tokens);\n        if (isNaN(contextWindow)) continue;\n\n        // Some models have a provider/model-tag format, so we need to get the last part since we dont do paths\n        // for names with the exception of some router-providers like OpenRouter or Together.\n        const modelName = key.split(\"/\").pop();\n        formattedModelMap[provider][modelName] = contextWindow;\n      }\n    }\n    return formattedModelMap;\n  }\n\n  /**\n   * Gets the context window for a given provider and model.\n   *\n   * If the provider is not found, null is returned.\n   * If the model is not found, the provider's entire model map is returned.\n   *\n   * if both provider and model are provided, the context window for the given model is returned.\n   * @param {string|null} provider - The provider to get the context window for\n   * @param {string|null} model - The model to get the context window for\n   * @returns {number|null} - The context window for the given provider and model\n   */\n  get(provider = null, model = null) {\n    if (!provider || !this.cachedModelMap || !this.cachedModelMap[provider])\n      return null;\n    if (!model) return this.cachedModelMap[provider];\n\n    const modelContextWindow = this.cachedModelMap[provider][model];\n    if (!modelContextWindow) {\n      this.log(\"Invalid access to model context window - not found in cache\", {\n        provider,\n        model,\n      });\n      return null;\n    }\n    return Number(modelContextWindow);\n  }\n}\n\nmodule.exports = { MODEL_MAP: new ContextWindowFinder() };\n"
  },
  {
    "path": "server/utils/AiProviders/modelMap/legacy.js",
    "content": "const LEGACY_MODEL_MAP = {\n  anthropic: {\n    \"claude-instant-1.2\": 100000,\n    \"claude-2.0\": 100000,\n    \"claude-2.1\": 200000,\n    \"claude-3-haiku-20240307\": 200000,\n    \"claude-3-sonnet-20240229\": 200000,\n    \"claude-3-opus-20240229\": 200000,\n    \"claude-3-opus-latest\": 200000,\n    \"claude-3-5-haiku-latest\": 200000,\n    \"claude-3-5-haiku-20241022\": 200000,\n    \"claude-3-5-sonnet-latest\": 200000,\n    \"claude-3-5-sonnet-20241022\": 200000,\n    \"claude-3-5-sonnet-20240620\": 200000,\n    \"claude-3-7-sonnet-20250219\": 200000,\n    \"claude-3-7-sonnet-latest\": 200000,\n  },\n  cohere: {\n    \"command-r\": 128000,\n    \"command-r-plus\": 128000,\n    command: 4096,\n    \"command-light\": 4096,\n    \"command-nightly\": 8192,\n    \"command-light-nightly\": 8192,\n    \"command-r-plus-08-2024\": 132096,\n    \"command-a-03-2025\": 288000,\n    \"c4ai-aya-vision-32b\": 16384,\n    \"command-a-reasoning-08-2025\": 288768,\n    \"command-r-08-2024\": 132096,\n    \"c4ai-aya-vision-8b\": 16384,\n    \"command-r7b-12-2024\": 132000,\n    \"command-r7b-arabic-02-2025\": 128000,\n    \"command-a-vision-07-2025\": 128000,\n    \"c4ai-aya-expanse-8b\": 8192,\n    \"c4ai-aya-expanse-32b\": 128000,\n    \"command-a-translate-08-2025\": 8992,\n  },\n  gemini: {\n    \"gemini-1.5-pro-001\": 2000000,\n    \"gemini-1.5-pro-002\": 2000000,\n    \"gemini-1.5-pro\": 2000000,\n    \"gemini-1.5-flash-001\": 1000000,\n    \"gemini-1.5-flash\": 1000000,\n    \"gemini-1.5-flash-002\": 1000000,\n    \"gemini-1.5-flash-8b\": 1000000,\n    \"gemini-1.5-flash-8b-001\": 1000000,\n    \"gemini-2.0-flash\": 1048576,\n    \"gemini-2.0-flash-001\": 1048576,\n    \"gemini-2.0-flash-lite-001\": 1048576,\n    \"gemini-2.0-flash-lite\": 1048576,\n    \"gemini-1.5-pro-latest\": 2000000,\n    \"gemini-1.5-flash-latest\": 1000000,\n    \"gemini-1.5-flash-8b-latest\": 1000000,\n    \"gemini-1.5-flash-8b-exp-0827\": 1000000,\n    \"gemini-1.5-flash-8b-exp-0924\": 1000000,\n    \"gemini-2.5-pro-exp-03-25\": 1048576,\n    \"gemini-2.5-pro-preview-03-25\": 1048576,\n    \"gemini-2.0-flash-exp\": 1048576,\n    \"gemini-2.0-flash-exp-image-generation\": 1048576,\n    \"gemini-2.0-flash-lite-preview-02-05\": 1048576,\n    \"gemini-2.0-flash-lite-preview\": 1048576,\n    \"gemini-2.0-pro-exp\": 1048576,\n    \"gemini-2.0-pro-exp-02-05\": 1048576,\n    \"gemini-exp-1206\": 1048576,\n    \"gemini-2.0-flash-thinking-exp-01-21\": 1048576,\n    \"gemini-2.0-flash-thinking-exp\": 1048576,\n    \"gemini-2.0-flash-thinking-exp-1219\": 1048576,\n    \"learnlm-1.5-pro-experimental\": 32767,\n    \"gemma-3-1b-it\": 32768,\n    \"gemma-3-4b-it\": 32768,\n    \"gemma-3-12b-it\": 32768,\n    \"gemma-3-27b-it\": 131072,\n  },\n  groq: {\n    \"gemma2-9b-it\": 8192,\n    \"gemma-7b-it\": 8192,\n    \"llama3-70b-8192\": 8192,\n    \"llama3-8b-8192\": 8192,\n    \"llama-3.1-70b-versatile\": 8000,\n    \"llama-3.1-8b-instant\": 8000,\n    \"mixtral-8x7b-32768\": 32768,\n  },\n  openai: {\n    \"gpt-3.5-turbo\": 16385,\n    \"gpt-3.5-turbo-1106\": 16385,\n    \"gpt-4o\": 128000,\n    \"gpt-4o-2024-08-06\": 128000,\n    \"gpt-4o-2024-05-13\": 128000,\n    \"gpt-4o-mini\": 128000,\n    \"gpt-4o-mini-2024-07-18\": 128000,\n    \"gpt-4-turbo\": 128000,\n    \"gpt-4-1106-preview\": 128000,\n    \"gpt-4-turbo-preview\": 128000,\n    \"gpt-4\": 8192,\n    \"gpt-4-32k\": 32000,\n    \"gpt-4.1\": 1047576,\n    \"gpt-4.1-2025-04-14\": 1047576,\n    \"gpt-4.1-mini\": 1047576,\n    \"gpt-4.1-mini-2025-04-14\": 1047576,\n    \"gpt-4.1-nano\": 1047576,\n    \"gpt-4.1-nano-2025-04-14\": 1047576,\n    \"gpt-4.5-preview\": 128000,\n    \"gpt-4.5-preview-2025-02-27\": 128000,\n    \"o1-preview\": 128000,\n    \"o1-preview-2024-09-12\": 128000,\n    \"o1-mini\": 128000,\n    \"o1-mini-2024-09-12\": 128000,\n    o1: 200000,\n    \"o1-2024-12-17\": 200000,\n    \"o1-pro\": 200000,\n    \"o1-pro-2025-03-19\": 200000,\n    \"o3-mini\": 200000,\n    \"o3-mini-2025-01-31\": 200000,\n  },\n  deepseek: {\n    \"deepseek-chat\": 128000,\n    \"deepseek-coder\": 128000,\n    \"deepseek-reasoner\": 128000,\n  },\n  xai: {\n    \"grok-beta\": 131072,\n  },\n  giteeai: {\n    \"Qwen2.5-72B-Instruct\": 16_384,\n    \"Qwen2.5-14B-Instruct\": 24_576,\n    \"Qwen2-7B-Instruct\": 24_576,\n    \"Qwen2.5-32B-Instruct\": 32_768,\n    \"Qwen2-72B-Instruct\": 32_768,\n    \"Qwen2-VL-72B\": 32_768,\n    \"QwQ-32B-Preview\": 32_768,\n    \"Yi-34B-Chat\": 4_096,\n    \"glm-4-9b-chat\": 32_768,\n    \"deepseek-coder-33B-instruct\": 8_192,\n    \"codegeex4-all-9b\": 32_768,\n    \"InternVL2-8B\": 32_768,\n    \"InternVL2.5-26B\": 32_768,\n    \"InternVL2.5-78B\": 32_768,\n    \"DeepSeek-R1-Distill-Qwen-32B\": 32_768,\n    \"DeepSeek-R1-Distill-Qwen-1.5B\": 32_768,\n    \"DeepSeek-R1-Distill-Qwen-14B\": 32_768,\n    \"DeepSeek-R1-Distill-Qwen-7B\": 32_768,\n    \"DeepSeek-V3\": 32_768,\n    \"DeepSeek-R1\": 32_768,\n  },\n};\nmodule.exports = LEGACY_MODEL_MAP;\n"
  },
  {
    "path": "server/utils/AiProviders/moonshotAi/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst {\n  handleDefaultStreamResponseV2,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst { MODEL_MAP } = require(\"../modelMap\");\n\nclass MoonshotAiLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.MOONSHOT_AI_API_KEY)\n      throw new Error(\"No Moonshot AI API key was set.\");\n    this.className = \"MoonshotAiLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n\n    this.openai = new OpenAIApi({\n      baseURL: \"https://api.moonshot.ai/v1\",\n      apiKey: process.env.MOONSHOT_AI_API_KEY,\n    });\n    this.model =\n      modelPreference ||\n      process.env.MOONSHOT_AI_MODEL_PREF ||\n      \"moonshot-v1-32k\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.log(\n      `Initialized ${this.model} with context window ${this.promptWindowLimit()}`\n    );\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  streamingEnabled() {\n    return true;\n  }\n\n  promptWindowLimit() {\n    return MODEL_MAP.get(\"moonshot\", this.model) ?? 8_192;\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !Object.prototype.hasOwnProperty.call(result.output, \"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n}\n\nmodule.exports = { MoonshotAiLLM };\n"
  },
  {
    "path": "server/utils/AiProviders/novita/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst {\n  writeResponseChunk,\n  clientAbortedHandler,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { safeJsonParse } = require(\"../../http\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst cacheFolder = path.resolve(\n  process.env.STORAGE_DIR\n    ? path.resolve(process.env.STORAGE_DIR, \"models\", \"novita\")\n    : path.resolve(__dirname, `../../../storage/models/novita`)\n);\n\nclass NovitaLLM {\n  defaultTimeout = 3_000;\n\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.NOVITA_LLM_API_KEY)\n      throw new Error(\"No Novita API key was set.\");\n\n    this.className = \"NovitaLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.basePath = \"https://api.novita.ai/v3/openai\";\n    this.openai = new OpenAIApi({\n      baseURL: this.basePath,\n      apiKey: process.env.NOVITA_LLM_API_KEY ?? null,\n      defaultHeaders: {\n        \"HTTP-Referer\": \"https://anythingllm.com\",\n        \"X-Novita-Source\": \"anythingllm\",\n      },\n    });\n    this.model =\n      modelPreference ||\n      process.env.NOVITA_LLM_MODEL_PREF ||\n      \"deepseek/deepseek-r1\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.timeout = this.#parseTimeout();\n\n    if (!fs.existsSync(cacheFolder))\n      fs.mkdirSync(cacheFolder, { recursive: true });\n    this.cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n    this.cacheAtPath = path.resolve(cacheFolder, \".cached_at\");\n\n    this.log(`Loaded with model: ${this.model}`);\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Novita has various models that never return `finish_reasons` and thus leave the stream open\n   * which causes issues in subsequent messages. This timeout value forces us to close the stream after\n   * x milliseconds. This is a configurable value via the NOVITA_LLM_TIMEOUT_MS value\n   * @returns {number} The timeout value in milliseconds (default: 3_000)\n   */\n  #parseTimeout() {\n    this.log(\n      `Novita timeout is set to ${process.env.NOVITA_LLM_TIMEOUT_MS ?? this.defaultTimeout}ms`\n    );\n    if (isNaN(Number(process.env.NOVITA_LLM_TIMEOUT_MS)))\n      return this.defaultTimeout;\n    const setValue = Number(process.env.NOVITA_LLM_TIMEOUT_MS);\n    if (setValue < 500) return 500; // 500ms is the minimum timeout\n    return setValue;\n  }\n\n  // This checks if the .cached_at file has a timestamp that is more than 1Week (in millis)\n  // from the current date. If it is, then we will refetch the API so that all the models are up\n  // to date.\n  #cacheIsStale() {\n    const MAX_STALE = 2.592e8; // 3 days in MS\n    if (!fs.existsSync(this.cacheAtPath)) return true;\n    const now = Number(new Date());\n    const timestampMs = Number(fs.readFileSync(this.cacheAtPath));\n    return now - timestampMs > MAX_STALE;\n  }\n\n  // The Novita model API has a lot of models, so we cache this locally in the directory\n  // as if the cache directory JSON file is stale or does not exist we will fetch from API and store it.\n  // This might slow down the first request, but we need the proper token context window\n  // for each model and this is a constructor property - so we can really only get it if this cache exists.\n  // We used to have this as a chore, but given there is an API to get the info - this makes little sense.\n  async #syncModels() {\n    if (fs.existsSync(this.cacheModelPath) && !this.#cacheIsStale())\n      return false;\n\n    this.log(\"Model cache is not present or stale. Fetching from Novita API.\");\n    await fetchNovitaModels();\n    return;\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  models() {\n    if (!fs.existsSync(this.cacheModelPath)) return {};\n    return safeJsonParse(\n      fs.readFileSync(this.cacheModelPath, { encoding: \"utf-8\" }),\n      {}\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    const cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n    const availableModels = fs.existsSync(cacheModelPath)\n      ? safeJsonParse(\n          fs.readFileSync(cacheModelPath, { encoding: \"utf-8\" }),\n          {}\n        )\n      : {};\n    return availableModels[modelName]?.maxLength || 4096;\n  }\n\n  promptWindowLimit() {\n    const availableModels = this.models();\n    return availableModels[this.model]?.maxLength || 4096;\n  }\n\n  /**\n   * Get the capabilities of a model from the Novita API.\n   * @returns {Promise<{tools: 'unknown' | boolean, reasoning: 'unknown' | boolean, imageGeneration: 'unknown' | boolean, vision: 'unknown' | boolean}>}\n   */\n  async getModelCapabilities() {\n    try {\n      await this.#syncModels();\n      const availableModels = this.models();\n      const modelInfo = availableModels[this.model];\n      return {\n        tools: modelInfo.features.includes(\"function-calling\"),\n        reasoning: modelInfo.features.includes(\"reasoning\"),\n        imageGeneration: false, // no image generation capabilities for Novita yet.\n        vision: modelInfo.input_modalities.includes(\"image\"),\n      };\n    } catch (error) {\n      console.error(\"Error getting model capabilities:\", error);\n      return {\n        tools: \"unknown\",\n        reasoning: \"unknown\",\n        imageGeneration: \"unknown\",\n        vision: \"unknown\",\n      };\n    }\n  }\n\n  async isValidChatCompletionModel(model = \"\") {\n    await this.#syncModels();\n    const availableModels = this.models();\n    return availableModels.hasOwnProperty(model);\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"auto\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `Novita chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `Novita chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  /**\n   * Handles the default stream response for a chat.\n   * @param {import(\"express\").Response} response\n   * @param {import('../../helpers/chat/LLMPerformanceMonitor').MonitoredStream} stream\n   * @param {Object} responseProps\n   * @returns {Promise<string>}\n   */\n  handleStream(response, stream, responseProps) {\n    const timeoutThresholdMs = this.timeout;\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n      let lastChunkTime = null; // null when first token is still not received.\n\n      // Establish listener to early-abort a streaming response\n      // in case things go sideways or the user does not like the response.\n      // We preserve the generated text but continue as if chat was completed\n      // to preserve previously generated content.\n      const handleAbort = () => {\n        stream?.endMeasurement({\n          completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n        });\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      // NOTICE: Not all Novita models will return a stop reason\n      // which keeps the connection open and so the model never finalizes the stream\n      // like the traditional OpenAI response schema does. So in the case the response stream\n      // never reaches a formal close state we maintain an interval timer that if we go >=timeoutThresholdMs with\n      // no new chunks then we kill the stream and assume it to be complete. Novita is quite fast\n      // so this threshold should permit most responses, but we can adjust `timeoutThresholdMs` if\n      // we find it is too aggressive.\n      const timeoutCheck = setInterval(() => {\n        if (lastChunkTime === null) return;\n\n        const now = Number(new Date());\n        const diffMs = now - lastChunkTime;\n        if (diffMs >= timeoutThresholdMs) {\n          this.log(\n            `Novita stream did not self-close and has been stale for >${timeoutThresholdMs}ms. Closing response stream.`\n          );\n          writeResponseChunk(response, {\n            uuid,\n            sources,\n            type: \"textResponseChunk\",\n            textResponse: \"\",\n            close: true,\n            error: false,\n          });\n          clearInterval(timeoutCheck);\n          response.removeListener(\"close\", handleAbort);\n          stream?.endMeasurement({\n            completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n          });\n          resolve(fullText);\n        }\n      }, 500);\n\n      try {\n        for await (const chunk of stream) {\n          const message = chunk?.choices?.[0];\n          const token = message?.delta?.content;\n          lastChunkTime = Number(new Date());\n\n          if (token) {\n            fullText += token;\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: token,\n              close: false,\n              error: false,\n            });\n          }\n\n          if (message?.finish_reason !== null) {\n            writeResponseChunk(response, {\n              uuid,\n              sources,\n              type: \"textResponseChunk\",\n              textResponse: \"\",\n              close: true,\n              error: false,\n            });\n            response.removeListener(\"close\", handleAbort);\n            stream?.endMeasurement({\n              completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n            });\n            resolve(fullText);\n          }\n        }\n      } catch (e) {\n        writeResponseChunk(response, {\n          uuid,\n          sources,\n          type: \"abort\",\n          textResponse: null,\n          close: true,\n          error: e.message,\n        });\n        response.removeListener(\"close\", handleAbort);\n        stream?.endMeasurement({\n          completion_tokens: LLMPerformanceMonitor.countTokens(fullText),\n        });\n        resolve(fullText);\n      }\n    });\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nasync function fetchNovitaModels() {\n  return await fetch(`https://api.novita.ai/v3/openai/models`, {\n    method: \"GET\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n  })\n    .then((res) => res.json())\n    .then(({ data = [] }) => {\n      const models = {};\n      data.forEach((model) => {\n        models[model.id] = {\n          id: model.id,\n          name: model.title,\n          organization:\n            model.id.split(\"/\")[0].charAt(0).toUpperCase() +\n            model.id.split(\"/\")[0].slice(1),\n          maxLength: model.context_size,\n          features: model.features ?? [],\n          input_modalities: model.input_modalities ?? [],\n        };\n      });\n\n      // Cache all response information\n      if (!fs.existsSync(cacheFolder))\n        fs.mkdirSync(cacheFolder, { recursive: true });\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \"models.json\"),\n        JSON.stringify(models),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \".cached_at\"),\n        String(Number(new Date())),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n      return models;\n    })\n    .catch((e) => {\n      console.error(e);\n      return {};\n    });\n}\n\nmodule.exports = {\n  NovitaLLM,\n  fetchNovitaModels,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/nvidiaNim/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst {\n  handleDefaultStreamResponseV2,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\n\nclass NvidiaNimLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.NVIDIA_NIM_LLM_BASE_PATH)\n      throw new Error(\"No NVIDIA NIM API Base Path was set.\");\n\n    this.className = \"NvidiaNimLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.nvidiaNim = new OpenAIApi({\n      baseURL: parseNvidiaNimBasePath(process.env.NVIDIA_NIM_LLM_BASE_PATH),\n      apiKey: null,\n    });\n\n    this.model = modelPreference || process.env.NVIDIA_NIM_LLM_MODEL_PREF;\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.#log(\n      `Loaded with model: ${this.model} with context window: ${this.promptWindowLimit()}`\n    );\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  /**\n   * Set the model token limit `NVIDIA_NIM_LLM_MODEL_TOKEN_LIMIT` for the given model ID\n   * @param {string} modelId\n   * @param {string} basePath\n   * @returns {Promise<void>}\n   */\n  static async setModelTokenLimit(modelId, basePath = null) {\n    if (!modelId) return;\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    const openai = new OpenAIApi({\n      baseURL: parseNvidiaNimBasePath(\n        basePath || process.env.NVIDIA_NIM_LLM_BASE_PATH\n      ),\n      apiKey: null,\n    });\n    const model = await openai.models\n      .list()\n      .then((results) => results.data)\n      .catch(() => {\n        return [];\n      });\n\n    if (!model.length) return;\n    const modelInfo = model.find((model) => model.id === modelId);\n    if (!modelInfo) return;\n    process.env.NVIDIA_NIM_LLM_MODEL_TOKEN_LIMIT = Number(\n      modelInfo.max_model_len || 4096\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(_modelName) {\n    const limit = process.env.NVIDIA_NIM_LLM_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No NVIDIA NIM token context limit was set.\");\n    return Number(limit);\n  }\n\n  // Ensure the user set a value for the token limit\n  // and if undefined - assume 4096 window.\n  promptWindowLimit() {\n    const limit = process.env.NVIDIA_NIM_LLM_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No NVIDIA NIM token context limit was set.\");\n    return Number(limit);\n  }\n\n  async isValidChatCompletionModel(_ = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"auto\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        `NVIDIA NIM chat: ${this.model} is not valid or defined model for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.nvidiaNim.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        `NVIDIA NIM chat: ${this.model} is not valid or defined model for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.nvidiaNim.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\n/**\n * Parse the base path for the Nvidia NIM container API. Since the base path must end in /v1 and cannot have a trailing slash,\n * and the user can possibly set it to anything and likely incorrectly due to pasting behaviors, we need to ensure it is in the correct format.\n * @param {string} basePath\n * @returns {string}\n */\nfunction parseNvidiaNimBasePath(providedBasePath = \"\") {\n  try {\n    const baseURL = new URL(providedBasePath);\n    const basePath = `${baseURL.origin}/v1`;\n    return basePath;\n  } catch {\n    return providedBasePath;\n  }\n}\n\nmodule.exports = {\n  NvidiaNimLLM,\n  parseNvidiaNimBasePath,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/ollama/index.js",
    "content": "const {\n  writeResponseChunk,\n  clientAbortedHandler,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst { Ollama } = require(\"ollama\");\nconst { v4: uuidv4 } = require(\"uuid\");\n\n// Docs: https://github.com/jmorganca/ollama/blob/main/docs/api.md\nclass OllamaAILLM {\n  /** @see OllamaAILLM.cacheContextWindows */\n  static modelContextWindows = {};\n\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.OLLAMA_BASE_PATH)\n      throw new Error(\"No Ollama Base Path was set.\");\n\n    this.className = \"OllamaAILLM\";\n    this.authToken = process.env.OLLAMA_AUTH_TOKEN;\n    this.basePath = process.env.OLLAMA_BASE_PATH;\n    this.model = modelPreference || process.env.OLLAMA_MODEL_PREF;\n    this.keepAlive = process.env.OLLAMA_KEEP_ALIVE_TIMEOUT\n      ? Number(process.env.OLLAMA_KEEP_ALIVE_TIMEOUT)\n      : 300; // Default 5-minute timeout for Ollama model loading.\n\n    const headers = this.authToken\n      ? { Authorization: `Bearer ${this.authToken}` }\n      : {};\n    this.client = new Ollama({\n      host: this.basePath,\n      headers: headers,\n      fetch: OllamaAILLM.applyOllamaFetch(),\n    });\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n\n    // Lazy load the limits to avoid blocking the main thread on cacheContextWindows\n    this.limits = null;\n\n    OllamaAILLM.cacheContextWindows(true);\n    this.#log(`initialized with model: ${this.model}`);\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[32m[Ollama]\\x1b[0m ${text}`, ...args);\n  }\n\n  static #slog(text, ...args) {\n    console.log(`\\x1b[32m[Ollama]\\x1b[0m ${text}`, ...args);\n  }\n\n  async assertModelContextLimits() {\n    if (this.limits !== null) return;\n    await OllamaAILLM.cacheContextWindows();\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n    this.#log(\n      `model ${this.model} is using a max context window of ${this.promptWindowLimit()}/${OllamaAILLM.maxContextWindow(this.model)} tokens.`\n    );\n  }\n\n  /**\n   * Cache the context windows for the Ollama models.\n   * This is done once and then cached for the lifetime of the server. This is absolutely necessary to ensure that the context windows are correct.\n   *\n   * This is a convenience to ensure that the context windows are correct and that the user\n   * does not have to manually set the context window for each model.\n   * @param {boolean} force - Force the cache to be refreshed.\n   * @returns {Promise<void>} - A promise that resolves when the cache is refreshed.\n   */\n  static async cacheContextWindows(force = false) {\n    try {\n      // Skip if we already have cached context windows and we're not forcing a refresh\n      if (Object.keys(OllamaAILLM.modelContextWindows).length > 0 && !force)\n        return;\n\n      const authToken = process.env.OLLAMA_AUTH_TOKEN;\n      const basePath = process.env.OLLAMA_BASE_PATH;\n      const client = new Ollama({\n        host: basePath,\n        headers: authToken ? { Authorization: `Bearer ${authToken}` } : {},\n      });\n\n      const { models } = await client.list().catch(() => ({ models: [] }));\n      if (!models.length) return;\n\n      const infoPromises = models.map((model) =>\n        client\n          .show({ model: model.name })\n          .then((info) => ({ name: model.name, ...info }))\n      );\n      const infos = await Promise.all(infoPromises);\n      infos.forEach((showInfo) => {\n        if (showInfo.capabilities.includes(\"embedding\")) return;\n        const contextWindowKey = Object.keys(showInfo.model_info).find((key) =>\n          key.endsWith(\".context_length\")\n        );\n        if (!contextWindowKey)\n          return (OllamaAILLM.modelContextWindows[showInfo.name] = 4096);\n        OllamaAILLM.modelContextWindows[showInfo.name] =\n          showInfo.model_info[contextWindowKey];\n      });\n      OllamaAILLM.#slog(`Context windows cached for all models!`);\n    } catch (e) {\n      OllamaAILLM.#slog(`Error caching context windows`, e);\n      return;\n    }\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  /**\n   * Apply a custom fetch function to the Ollama client.\n   * This is useful when we want to bypass the default 5m timeout for global fetch\n   * for machines which run responses very slowly.\n   * @returns {Function} The custom fetch function.\n   */\n  static applyOllamaFetch() {\n    try {\n      if (!(\"OLLAMA_RESPONSE_TIMEOUT\" in process.env)) return fetch;\n      const { Agent } = require(\"undici\");\n      const moment = require(\"moment\");\n      let timeout = process.env.OLLAMA_RESPONSE_TIMEOUT;\n\n      if (!timeout || isNaN(Number(timeout)) || Number(timeout) <= 5 * 60_000) {\n        OllamaAILLM.#slog(\n          \"Timeout option was not set, is not a number, or is less than 5 minutes in ms - falling back to default\",\n          { timeout }\n        );\n        return fetch;\n      } else timeout = Number(timeout);\n\n      const noTimeoutFetch = (input, init = {}) => {\n        return fetch(input, {\n          ...init,\n          dispatcher: new Agent({ headersTimeout: timeout }),\n        });\n      };\n\n      const humanDiff = moment.duration(timeout).humanize();\n      OllamaAILLM.#slog(`Applying custom fetch w/timeout of ${humanDiff}.`);\n      return noTimeoutFetch;\n    } catch (error) {\n      OllamaAILLM.#slog(\n        \"Error applying custom fetch - using default fetch\",\n        error\n      );\n      return fetch;\n    }\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    if (Object.keys(OllamaAILLM.modelContextWindows).length === 0) {\n      this.#slog(\n        \"No context windows cached - Context window may be inaccurately reported.\"\n      );\n      return Number(process.env.OLLAMA_MODEL_TOKEN_LIMIT) || 4096;\n    }\n\n    let userDefinedLimit = null;\n    const systemDefinedLimit = OllamaAILLM.maxContextWindow(modelName);\n\n    if (\n      process.env.OLLAMA_MODEL_TOKEN_LIMIT &&\n      !isNaN(Number(process.env.OLLAMA_MODEL_TOKEN_LIMIT)) &&\n      Number(process.env.OLLAMA_MODEL_TOKEN_LIMIT) > 0\n    )\n      userDefinedLimit = Number(process.env.OLLAMA_MODEL_TOKEN_LIMIT);\n\n    // The user defined limit is always higher priority than the context window limit, but it cannot be higher than the context window limit\n    // so we return the minimum of the two, if there is no user defined limit, we return the system defined limit as-is.\n    if (userDefinedLimit !== null)\n      return Math.min(userDefinedLimit, systemDefinedLimit);\n\n    // Cap the context window limit to 16,384 tokens if the model supports more than that and no value is specified by the user.\n    // This prevents super-large context windows from being used if the user does not specify a value\n    // as well as also having smaller context windows use the full context window limit.\n    return Math.min(systemDefinedLimit, 16384);\n  }\n\n  promptWindowLimit() {\n    return this.constructor.promptWindowLimit(this.model);\n  }\n\n  static maxContextWindow(modelName = null) {\n    if (Object.keys(OllamaAILLM.modelContextWindows).length === 0 || !modelName)\n      return 4096;\n    return Number(OllamaAILLM.modelContextWindows[modelName]) || 16384;\n  }\n\n  async isValidChatCompletionModel(_ = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {{content: string, images: string[]}}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) return { content: userPrompt };\n    const images = attachments.map(\n      (attachment) => attachment.contentString.split(\"base64,\").slice(-1)[0]\n    );\n    return { content: userPrompt, images };\n  }\n\n  /**\n   * Handles errors from the Ollama API to make them more user friendly.\n   * @param {Error} e\n   */\n  #errorHandler(e) {\n    switch (e.message) {\n      case \"fetch failed\":\n        throw new Error(\n          \"Your Ollama instance could not be reached or is not responding. Please make sure it is running the API server and your connection information is correct in AnythingLLM.\"\n        );\n      default:\n        return e;\n    }\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent, \"spread\"),\n      {\n        role: \"user\",\n        ...this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.client\n        .chat({\n          model: this.model,\n          stream: false,\n          messages,\n          keep_alive: this.keepAlive,\n          options: {\n            temperature,\n            num_ctx: this.promptWindowLimit(),\n          },\n        })\n        .then((res) => {\n          let content = res.message.content;\n          if (res.message.thinking)\n            content = `<think>${res.message.thinking}</think>${content}`;\n          return {\n            content,\n            usage: {\n              prompt_tokens: res.prompt_eval_count,\n              completion_tokens: res.eval_count,\n              total_tokens: res.prompt_eval_count + res.eval_count,\n              duration: res.eval_duration / 1e9,\n            },\n          };\n        })\n        .catch((e) => {\n          throw new Error(\n            `Ollama::getChatCompletion failed to communicate with Ollama. ${this.#errorHandler(e).message}`\n          );\n        })\n    );\n\n    if (!result.output.content || !result.output.content.length)\n      throw new Error(`Ollama::getChatCompletion text response was empty.`);\n\n    return {\n      textResponse: result.output.content,\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens,\n        completion_tokens: result.output.usage.completion_tokens,\n        total_tokens: result.output.usage.total_tokens,\n        outputTps:\n          result.output.usage.completion_tokens / result.output.usage.duration,\n        duration: result.output.usage.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.client.chat({\n        model: this.model,\n        stream: true,\n        messages,\n        keep_alive: this.keepAlive,\n        options: {\n          temperature,\n          num_ctx: this.promptWindowLimit(),\n        },\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    }).catch((e) => {\n      throw this.#errorHandler(e);\n    });\n    return measuredStreamRequest;\n  }\n\n  /**\n   * Handles streaming responses from Ollama.\n   * @param {import(\"express\").Response} response\n   * @param {import(\"../../helpers/chat/LLMPerformanceMonitor\").MonitoredStream} stream\n   * @param {import(\"express\").Request} request\n   * @returns {Promise<string>}\n   */\n  handleStream(response, stream, responseProps) {\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n      let reasoningText = \"\";\n      let usage = {\n        prompt_tokens: 0,\n        completion_tokens: 0,\n      };\n\n      // Establish listener to early-abort a streaming response\n      // in case things go sideways or the user does not like the response.\n      // We preserve the generated text but continue as if chat was completed\n      // to preserve previously generated content.\n      const handleAbort = () => {\n        stream?.endMeasurement(usage);\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      try {\n        for await (const chunk of stream) {\n          if (chunk === undefined)\n            throw new Error(\n              \"Stream returned undefined chunk. Aborting reply - check model provider logs.\"\n            );\n\n          if (chunk.done) {\n            usage.prompt_tokens = chunk.prompt_eval_count;\n            usage.completion_tokens = chunk.eval_count;\n            usage.duration = chunk.eval_duration / 1e9;\n            writeResponseChunk(response, {\n              uuid,\n              sources,\n              type: \"textResponseChunk\",\n              textResponse: \"\",\n              close: true,\n              error: false,\n            });\n            response.removeListener(\"close\", handleAbort);\n            stream?.endMeasurement(usage);\n            resolve(fullText);\n            break;\n          }\n\n          if (chunk.hasOwnProperty(\"message\")) {\n            // As of Ollama v0.9.0+, thinking content comes in a separate property\n            // in the response object. If it exists, we need to handle it separately by wrapping it in <think> tags.\n            const content = chunk.message.content;\n            const reasoningToken = chunk.message.thinking;\n\n            if (reasoningToken) {\n              if (reasoningText.length === 0) {\n                const startTag = \"<think>\";\n                writeResponseChunk(response, {\n                  uuid,\n                  sources,\n                  type: \"textResponseChunk\",\n                  textResponse: startTag + reasoningToken,\n                  close: false,\n                  error: false,\n                });\n                reasoningText += startTag + reasoningToken;\n              } else {\n                writeResponseChunk(response, {\n                  uuid,\n                  sources,\n                  type: \"textResponseChunk\",\n                  textResponse: reasoningToken,\n                  close: false,\n                  error: false,\n                });\n                reasoningText += reasoningToken;\n              }\n            } else if (content.length > 0) {\n              // If we have reasoning text, we need to close the reasoning tag and then append the content.\n              if (reasoningText.length > 0) {\n                const endTag = \"</think>\";\n                writeResponseChunk(response, {\n                  uuid,\n                  sources,\n                  type: \"textResponseChunk\",\n                  textResponse: endTag,\n                  close: false,\n                  error: false,\n                });\n                fullText += reasoningText + endTag;\n                reasoningText = \"\"; // Reset reasoning buffer\n              }\n              fullText += content; // Append regular text\n              writeResponseChunk(response, {\n                uuid,\n                sources,\n                type: \"textResponseChunk\",\n                textResponse: content,\n                close: false,\n                error: false,\n              });\n            }\n          }\n        }\n      } catch (error) {\n        writeResponseChunk(response, {\n          uuid,\n          sources: [],\n          type: \"textResponseChunk\",\n          textResponse: \"\",\n          close: true,\n          error: `Ollama:streaming - could not stream chat. ${\n            error?.cause ?? error.message\n          }`,\n        });\n        response.removeListener(\"close\", handleAbort);\n        stream?.endMeasurement(usage);\n        resolve(fullText);\n      }\n    });\n  }\n\n  /**\n   * Returns the capabilities of the model.\n   * @returns {Promise<{tools: 'unknown' | boolean, reasoning: 'unknown' | boolean, imageGeneration: 'unknown' | boolean, vision: 'unknown' | boolean}>}\n   */\n  async getModelCapabilities() {\n    try {\n      const { capabilities = [] } = await this.client.show({\n        model: this.model,\n      });\n      return {\n        tools: capabilities.includes(\"tools\") ? true : false,\n        reasoning: capabilities.includes(\"thinking\") ? true : false,\n        imageGeneration: false, // we dont have any image generation capabilities for Ollama or anywhere right now.\n        vision: capabilities.includes(\"vision\") ? true : false,\n      };\n    } catch (error) {\n      console.error(\"Error getting model capabilities:\", error);\n      return {\n        tools: \"unknown\",\n        reasoning: \"unknown\",\n        imageGeneration: \"unknown\",\n        vision: \"unknown\",\n      };\n    }\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    await this.assertModelContextLimits();\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  OllamaAILLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/openAi/index.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  formatChatHistory,\n  writeResponseChunk,\n  clientAbortedHandler,\n} = require(\"../../helpers/chat/responses\");\nconst { MODEL_MAP } = require(\"../modelMap\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\n\nclass OpenAiLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.OPEN_AI_KEY) throw new Error(\"No OpenAI API key was set.\");\n    this.className = \"OpenAiLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n\n    this.openai = new OpenAIApi({\n      apiKey: process.env.OPEN_AI_KEY,\n    });\n    this.model = modelPreference || process.env.OPEN_MODEL_PREF || \"gpt-4o\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.log(\n      `Initialized ${this.model} with context window ${this.promptWindowLimit()}`\n    );\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    return MODEL_MAP.get(\"openai\", modelName) ?? 4_096;\n  }\n\n  promptWindowLimit() {\n    return MODEL_MAP.get(\"openai\", this.model) ?? 4_096;\n  }\n\n  // Short circuit if name has 'gpt' since we now fetch models from OpenAI API\n  // via the user API key, so the model must be relevant and real.\n  // and if somehow it is not, chat will fail but that is caught.\n  // we don't want to hit the OpenAI api every chat because it will get spammed\n  // and introduce latency for no reason.\n  async isValidChatCompletionModel(modelName = \"\") {\n    const isPreset =\n      modelName.toLowerCase().includes(\"gpt\") ||\n      modelName.toLowerCase().startsWith(\"o\");\n    if (isPreset) return true;\n\n    const model = await this.openai.models\n      .retrieve(modelName)\n      .then((modelObj) => modelObj)\n      .catch(() => null);\n    return !!model;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"input_text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"input_image\",\n        image_url: attachment.contentString,\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [], // This is the specific attachment for only this prompt\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  /**\n   * Determine the appropriate temperature for the model.\n   * @param {string} modelName\n   * @param {number} temperature\n   * @returns {number}\n   */\n  #temperature(modelName, temperature) {\n    // For models that don't support temperature\n    // OpenAI accepts temperature 1\n    const NO_TEMP_MODELS = [\"o\", \"gpt-5\"];\n\n    if (NO_TEMP_MODELS.some((prefix) => modelName.startsWith(prefix))) {\n      return 1;\n    }\n\n    return temperature;\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `OpenAI chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.responses\n        .create({\n          model: this.model,\n          input: messages,\n          store: false,\n          temperature: this.#temperature(this.model, temperature),\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (!result.output.hasOwnProperty(\"output_text\")) return null;\n\n    const usage = result.output.usage || {};\n    return {\n      textResponse: result.output.output_text,\n      metrics: {\n        prompt_tokens: usage.input_tokens || 0,\n        completion_tokens: usage.output_tokens || 0,\n        total_tokens: usage.total_tokens || 0,\n        outputTps: usage.output_tokens\n          ? usage.output_tokens / result.duration\n          : 0,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `OpenAI chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.responses.create({\n        model: this.model,\n        stream: true,\n        input: messages,\n        store: false,\n        temperature: this.#temperature(this.model, temperature),\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n\n    let hasUsageMetrics = false;\n    let usage = {\n      completion_tokens: 0,\n    };\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n\n      const handleAbort = () => {\n        stream?.endMeasurement(usage);\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      try {\n        for await (const chunk of stream) {\n          if (chunk.type === \"response.output_text.delta\") {\n            const token = chunk.delta;\n            if (token) {\n              fullText += token;\n              if (!hasUsageMetrics) usage.completion_tokens++;\n\n              writeResponseChunk(response, {\n                uuid,\n                sources: [],\n                type: \"textResponseChunk\",\n                textResponse: token,\n                close: false,\n                error: false,\n              });\n            }\n          } else if (chunk.type === \"response.completed\") {\n            const { response: res } = chunk;\n            if (res.hasOwnProperty(\"usage\") && !!res.usage) {\n              hasUsageMetrics = true;\n              usage = {\n                ...usage,\n                prompt_tokens: res.usage?.input_tokens || 0,\n                completion_tokens: res.usage?.output_tokens || 0,\n                total_tokens: res.usage?.total_tokens || 0,\n              };\n            }\n\n            writeResponseChunk(response, {\n              uuid,\n              sources,\n              type: \"textResponseChunk\",\n              textResponse: \"\",\n              close: true,\n              error: false,\n            });\n            response.removeListener(\"close\", handleAbort);\n            stream?.endMeasurement(usage);\n            resolve(fullText);\n            break;\n          }\n        }\n      } catch (e) {\n        console.log(`\\x1b[43m\\x1b[34m[STREAMING ERROR]\\x1b[0m ${e.message}`);\n        writeResponseChunk(response, {\n          uuid,\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: e.message,\n        });\n        stream?.endMeasurement(usage);\n        resolve(fullText);\n      }\n    });\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  OpenAiLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/openRouter/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst {\n  writeResponseChunk,\n  clientAbortedHandler,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { safeJsonParse } = require(\"../../http\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst cacheFolder = path.resolve(\n  process.env.STORAGE_DIR\n    ? path.resolve(process.env.STORAGE_DIR, \"models\", \"openrouter\")\n    : path.resolve(__dirname, `../../../storage/models/openrouter`)\n);\n\nclass OpenRouterLLM {\n  /**\n   * Some openrouter models never send a finish_reason and thus leave the stream open in the UI.\n   * However, because OR is a middleware it can also wait an inordinately long time between chunks so we need\n   * to ensure that we dont accidentally close the stream too early. If the time between chunks is greater than this timeout\n   * we will close the stream and assume it to be complete. This is common for free models or slow providers they can\n   * possibly delegate to during invocation.\n   * @type {number}\n   */\n  defaultTimeout = 3_000;\n\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.OPENROUTER_API_KEY)\n      throw new Error(\"No OpenRouter API key was set.\");\n\n    this.className = \"OpenRouterLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.basePath = \"https://openrouter.ai/api/v1\";\n    this.openai = new OpenAIApi({\n      baseURL: this.basePath,\n      apiKey: process.env.OPENROUTER_API_KEY ?? null,\n      defaultHeaders: {\n        \"HTTP-Referer\": \"https://anythingllm.com\",\n        \"X-Title\": \"AnythingLLM\",\n      },\n    });\n    this.model =\n      modelPreference || process.env.OPENROUTER_MODEL_PREF || \"openrouter/auto\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.timeout = this.#parseTimeout();\n\n    if (!fs.existsSync(cacheFolder))\n      fs.mkdirSync(cacheFolder, { recursive: true });\n    this.cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n    this.cacheAtPath = path.resolve(cacheFolder, \".cached_at\");\n    this.log(\"Initialized with model:\", this.model);\n  }\n\n  /**\n   * Returns true if the model is a Perplexity model.\n   * OpenRouter has support for a lot of models and we have some special handling for Perplexity models\n   * that support in-line citations.\n   * @returns {boolean}\n   */\n  get isPerplexityModel() {\n    return this.model.startsWith(\"perplexity/\");\n  }\n\n  /**\n   * Generic formatting of a token for the following use cases:\n   * - Perplexity models that return inline citations in the token text\n   * @param {{token: string, citations: string[]}} options - The token text and citations.\n   * @returns {string} - The formatted token text.\n   */\n  enrichToken({ token, citations = [] }) {\n    if (!Array.isArray(citations) || citations.length === 0) return token;\n    return token.replace(/\\[(\\d+)\\]/g, (match, index) => {\n      const citationIndex = parseInt(index) - 1;\n      return citations[citationIndex]\n        ? `[[${index}](${citations[citationIndex]})]`\n        : match;\n    });\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * OpenRouter has various models that never return `finish_reasons` and thus leave the stream open\n   * which causes issues in subsequent messages. This timeout value forces us to close the stream after\n   * x milliseconds. This is a configurable value via the OPENROUTER_TIMEOUT_MS value\n   * @returns {number} The timeout value in milliseconds (default: 3_000)\n   */\n  #parseTimeout() {\n    this.log(\n      `OpenRouter timeout is set to ${process.env.OPENROUTER_TIMEOUT_MS ?? this.defaultTimeout}ms`\n    );\n    if (isNaN(Number(process.env.OPENROUTER_TIMEOUT_MS)))\n      return this.defaultTimeout;\n    const setValue = Number(process.env.OPENROUTER_TIMEOUT_MS);\n    if (setValue < 500) return 500; // 500ms is the minimum timeout\n    return setValue;\n  }\n\n  // This checks if the .cached_at file has a timestamp that is more than 1Week (in millis)\n  // from the current date. If it is, then we will refetch the API so that all the models are up\n  // to date.\n  #cacheIsStale() {\n    const MAX_STALE = 6.048e8; // 1 Week in MS\n    if (!fs.existsSync(this.cacheAtPath)) return true;\n    const now = Number(new Date());\n    const timestampMs = Number(fs.readFileSync(this.cacheAtPath));\n    return now - timestampMs > MAX_STALE;\n  }\n\n  // The OpenRouter model API has a lot of models, so we cache this locally in the directory\n  // as if the cache directory JSON file is stale or does not exist we will fetch from API and store it.\n  // This might slow down the first request, but we need the proper token context window\n  // for each model and this is a constructor property - so we can really only get it if this cache exists.\n  // We used to have this as a chore, but given there is an API to get the info - this makes little sense.\n  async #syncModels() {\n    if (fs.existsSync(this.cacheModelPath) && !this.#cacheIsStale())\n      return false;\n\n    this.log(\n      \"Model cache is not present or stale. Fetching from OpenRouter API.\"\n    );\n    await fetchOpenRouterModels();\n    return;\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  models() {\n    if (!fs.existsSync(this.cacheModelPath)) return {};\n    return safeJsonParse(\n      fs.readFileSync(this.cacheModelPath, { encoding: \"utf-8\" }),\n      {}\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    const cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n    const availableModels = fs.existsSync(cacheModelPath)\n      ? safeJsonParse(\n          fs.readFileSync(cacheModelPath, { encoding: \"utf-8\" }),\n          {}\n        )\n      : {};\n    return availableModels[modelName]?.maxLength || 4096;\n  }\n\n  promptWindowLimit() {\n    const availableModels = this.models();\n    return availableModels[this.model]?.maxLength || 4096;\n  }\n\n  async isValidChatCompletionModel(model = \"\") {\n    await this.#syncModels();\n    const availableModels = this.models();\n    return availableModels.hasOwnProperty(model);\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"auto\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Parses and prepends reasoning from the response and returns the full text response.\n   * @param {Object} response\n   * @returns {string}\n   */\n  #parseReasoningFromResponse({ message }) {\n    let textResponse = message?.content;\n    if (!!message?.reasoning && message.reasoning.trim().length > 0)\n      textResponse = `<think>${message.reasoning}</think>${textResponse}`;\n    return textResponse;\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7, user = null }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `OpenRouter chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n          // This is an OpenRouter specific option that allows us to get the reasoning text\n          // before the token text.\n          include_reasoning: true,\n          user: user?.id ? `user_${user.id}` : \"\",\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result?.output?.hasOwnProperty(\"choices\") ||\n      result?.output?.choices?.length === 0\n    )\n      throw new Error(\n        `Invalid response body returned from OpenRouter: ${result.output?.error?.message || \"Unknown error\"} ${result.output?.error?.code || \"Unknown code\"}`\n      );\n\n    return {\n      textResponse: this.#parseReasoningFromResponse(result.output.choices[0]),\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(\n    messages = null,\n    { temperature = 0.7, user = null }\n  ) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `OpenRouter chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n        // This is an OpenRouter specific option that allows us to get the reasoning text\n        // before the token text.\n        include_reasoning: true,\n        user: user?.id ? `user_${user.id}` : \"\",\n      }),\n      messages,\n      // OpenRouter returns the usage in the stream as the very last chunk **after** the finish reason.\n      // so we don't need to run the prompt token calculation.\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  /**\n   * Handles the default stream response for a chat.\n   * - Handle weird OR timeout behavior where the stream never self-closes.\n   * - Handle the usage metrics being returned in the stream as the very last chunk **after** the finish reason.\n   * @param {import(\"express\").Response} response\n   * @param {import('../../helpers/chat/LLMPerformanceMonitor').MonitoredStream} stream\n   * @param {Object} responseProps\n   * @returns {Promise<string>}\n   */\n  handleStream(response, stream, responseProps) {\n    const timeoutThresholdMs = this.timeout;\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n    let hasUsageMetrics = false;\n    let usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n      let reasoningText = \"\";\n      let lastChunkTime = null; // null when first token is still not received.\n      let pplxCitations = []; // Array of inline citations for Perplexity models (if applicable)\n      let isPerplexity = this.isPerplexityModel;\n\n      const handleAbort = () => {\n        stream?.endMeasurement(usage);\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      // NOTICE: Not all OpenRouter models will return a stop reason\n      // which keeps the connection open and so the model never finalizes the stream\n      // like the traditional OpenAI response schema does. So in the case the response stream\n      // never reaches a formal close state we maintain an interval timer that if we go >=timeoutThresholdMs with\n      // no new chunks then we kill the stream and assume it to be complete. OpenRouter is quite fast\n      // so this threshold should permit most responses, but we can adjust `timeoutThresholdMs` if\n      // we find it is too aggressive.\n      const timeoutCheck = setInterval(() => {\n        if (lastChunkTime === null) return;\n\n        const now = Number(new Date());\n        const diffMs = now - lastChunkTime;\n\n        if (diffMs >= timeoutThresholdMs) {\n          console.log(\n            `OpenRouter stream did not self-close and has been stale for >${timeoutThresholdMs}ms. Closing response stream.`\n          );\n          writeResponseChunk(response, {\n            uuid,\n            sources,\n            type: \"textResponseChunk\",\n            textResponse: \"\",\n            close: true,\n            error: false,\n          });\n          clearInterval(timeoutCheck);\n          response.removeListener(\"close\", handleAbort);\n          stream?.endMeasurement(usage);\n          resolve(fullText);\n        }\n      }, 500);\n\n      try {\n        for await (const chunk of stream) {\n          const message = chunk?.choices?.[0];\n          const token = message?.delta?.content;\n          const reasoningToken = message?.delta?.reasoning;\n          lastChunkTime = Number(new Date());\n\n          if (chunk.hasOwnProperty(\"usage\") && !hasUsageMetrics) {\n            hasUsageMetrics = true;\n            usage = {\n              prompt_tokens: chunk.usage.prompt_tokens,\n              completion_tokens: chunk.usage.completion_tokens,\n              total_tokens: chunk.usage.total_tokens,\n            };\n          }\n\n          // Some models will return citations (e.g. Perplexity) - we should preserve them for inline citations if applicable.\n          if (\n            isPerplexity &&\n            Array.isArray(chunk?.citations) &&\n            chunk?.citations?.length !== 0\n          )\n            pplxCitations.push(...chunk.citations);\n\n          // Reasoning models will always return the reasoning text before the token text.\n          // can be null or ''\n          if (reasoningToken) {\n            const formattedReasoningToken = this.enrichToken({\n              token: reasoningToken,\n              citations: pplxCitations,\n            });\n\n            // If the reasoning text is empty (''), we need to initialize it\n            // and send the first chunk of reasoning text.\n            if (reasoningText.length === 0) {\n              writeResponseChunk(response, {\n                uuid,\n                sources: [],\n                type: \"textResponseChunk\",\n                textResponse: `<think>${formattedReasoningToken}`,\n                close: false,\n                error: false,\n              });\n              reasoningText += `<think>${formattedReasoningToken}`;\n              continue;\n            } else {\n              // If the reasoning text is not empty, we need to append the reasoning text\n              // to the existing reasoning text.\n              writeResponseChunk(response, {\n                uuid,\n                sources: [],\n                type: \"textResponseChunk\",\n                textResponse: formattedReasoningToken,\n                close: false,\n                error: false,\n              });\n              reasoningText += formattedReasoningToken;\n            }\n          }\n\n          // If the reasoning text is not empty, but the reasoning token is empty\n          // and the token text is not empty we need to close the reasoning text and begin sending the token text.\n          if (!!reasoningText && !reasoningToken && token) {\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: `</think>`,\n              close: false,\n              error: false,\n            });\n            fullText += `${reasoningText}</think>`;\n            reasoningText = \"\";\n          }\n\n          if (token) {\n            const formattedToken = this.enrichToken({\n              token,\n              citations: pplxCitations,\n            });\n            fullText += formattedToken;\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: formattedToken,\n              close: false,\n              error: false,\n            });\n          }\n\n          if (message?.finish_reason) {\n            writeResponseChunk(response, {\n              uuid,\n              sources,\n              type: \"textResponseChunk\",\n              textResponse: \"\",\n              close: true,\n              error: false,\n            });\n          }\n        }\n\n        // Stream completed naturally - resolve with final metrics\n        response.removeListener(\"close\", handleAbort);\n        clearInterval(timeoutCheck);\n        stream?.endMeasurement(usage);\n        resolve(fullText);\n      } catch (e) {\n        writeResponseChunk(response, {\n          uuid,\n          sources,\n          type: \"abort\",\n          textResponse: null,\n          close: true,\n          error: e.message,\n        });\n        response.removeListener(\"close\", handleAbort);\n        clearInterval(timeoutCheck);\n        stream?.endMeasurement(usage);\n        resolve(fullText);\n      }\n    });\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nasync function fetchOpenRouterModels() {\n  return await fetch(`https://openrouter.ai/api/v1/models`, {\n    method: \"GET\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n    },\n  })\n    .then((res) => res.json())\n    .then(({ data = [] }) => {\n      const models = {};\n      data.forEach((model) => {\n        models[model.id] = {\n          id: model.id,\n          name: model.name,\n          organization:\n            model.id.split(\"/\")[0].charAt(0).toUpperCase() +\n            model.id.split(\"/\")[0].slice(1),\n          maxLength: model.context_length,\n        };\n      });\n\n      // Cache all response information\n      if (!fs.existsSync(cacheFolder))\n        fs.mkdirSync(cacheFolder, { recursive: true });\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \"models.json\"),\n        JSON.stringify(models),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \".cached_at\"),\n        String(Number(new Date())),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n\n      return models;\n    })\n    .catch((e) => {\n      console.error(e);\n      return {};\n    });\n}\n\nmodule.exports = {\n  OpenRouterLLM,\n  fetchOpenRouterModels,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/perplexity/index.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  writeResponseChunk,\n  clientAbortedHandler,\n} = require(\"../../helpers/chat/responses\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\n\nfunction perplexityModels() {\n  const { MODELS } = require(\"./models.js\");\n  return MODELS || {};\n}\n\nclass PerplexityLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.PERPLEXITY_API_KEY)\n      throw new Error(\"No Perplexity API key was set.\");\n\n    this.className = \"PerplexityLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.openai = new OpenAIApi({\n      baseURL: \"https://api.perplexity.ai\",\n      apiKey: process.env.PERPLEXITY_API_KEY ?? null,\n    });\n    this.model =\n      modelPreference ||\n      process.env.PERPLEXITY_MODEL_PREF ||\n      \"llama-3-sonar-large-32k-online\"; // Give at least a unique model to the provider as last fallback.\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  allModelInformation() {\n    return perplexityModels();\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    const availableModels = perplexityModels();\n    return availableModels[modelName]?.maxLength || 4096;\n  }\n\n  promptWindowLimit() {\n    const availableModels = this.allModelInformation();\n    return availableModels[this.model]?.maxLength || 4096;\n  }\n\n  async isValidChatCompletionModel(model = \"\") {\n    const availableModels = this.allModelInformation();\n    return availableModels.hasOwnProperty(model);\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [prompt, ...chatHistory, { role: \"user\", content: userPrompt }];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `Perplexity chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage?.prompt_tokens || 0,\n        completion_tokens: result.output.usage?.completion_tokens || 0,\n        total_tokens: result.output.usage?.total_tokens || 0,\n        outputTps: result.output.usage?.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `Perplexity chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  /**\n   * Enrich a token with citations if available for in-line citations.\n   * @param {string} token - The token to enrich.\n   * @param {Array} citations - The citations to enrich the token with.\n   * @returns {string} The enriched token.\n   */\n  enrichToken(token, citations) {\n    if (!Array.isArray(citations) || citations.length === 0) return token;\n    return token.replace(/\\[(\\d+)\\]/g, (match, index) => {\n      const citationIndex = parseInt(index) - 1;\n      return citations[citationIndex]\n        ? `[[${index}](${citations[citationIndex]})]`\n        : match;\n    });\n  }\n\n  handleStream(response, stream, responseProps) {\n    const timeoutThresholdMs = 800;\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n    let hasUsageMetrics = false;\n    let pplxCitations = []; // Array of links\n    let usage = {\n      completion_tokens: 0,\n    };\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n      let lastChunkTime = null;\n\n      const handleAbort = () => {\n        stream?.endMeasurement(usage);\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      const timeoutCheck = setInterval(() => {\n        if (lastChunkTime === null) return;\n\n        const now = Number(new Date());\n        const diffMs = now - lastChunkTime;\n        if (diffMs >= timeoutThresholdMs) {\n          console.log(\n            `Perplexity stream did not self-close and has been stale for >${timeoutThresholdMs}ms. Closing response stream.`\n          );\n          writeResponseChunk(response, {\n            uuid,\n            sources,\n            type: \"textResponseChunk\",\n            textResponse: \"\",\n            close: true,\n            error: false,\n          });\n          clearInterval(timeoutCheck);\n          response.removeListener(\"close\", handleAbort);\n          stream?.endMeasurement(usage);\n          resolve(fullText);\n        }\n      }, 500);\n\n      // Now handle the chunks from the streamed response and append to fullText.\n      try {\n        for await (const chunk of stream) {\n          lastChunkTime = Number(new Date());\n          const message = chunk?.choices?.[0];\n          const token = message?.delta?.content;\n\n          if (Array.isArray(chunk.citations) && chunk.citations.length !== 0) {\n            pplxCitations = chunk.citations;\n          }\n\n          // If we see usage metrics in the chunk, we can use them directly\n          // instead of estimating them, but we only want to assign values if\n          // the response object is the exact same key:value pair we expect.\n          if (\n            chunk.hasOwnProperty(\"usage\") && // exists\n            !!chunk.usage && // is not null\n            Object.values(chunk.usage).length > 0 // has values\n          ) {\n            if (chunk.usage.hasOwnProperty(\"prompt_tokens\")) {\n              usage.prompt_tokens = Number(chunk.usage.prompt_tokens);\n            }\n\n            if (chunk.usage.hasOwnProperty(\"completion_tokens\")) {\n              hasUsageMetrics = true; // to stop estimating counter\n              usage.completion_tokens = Number(chunk.usage.completion_tokens);\n            }\n          }\n\n          if (token) {\n            let enrichedToken = this.enrichToken(token, pplxCitations);\n            fullText += enrichedToken;\n            if (!hasUsageMetrics) usage.completion_tokens++;\n\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: enrichedToken,\n              close: false,\n              error: false,\n            });\n          }\n\n          if (message?.finish_reason) {\n            console.log(\"closing\");\n            writeResponseChunk(response, {\n              uuid,\n              sources,\n              type: \"textResponseChunk\",\n              textResponse: \"\",\n              close: true,\n              error: false,\n            });\n            response.removeListener(\"close\", handleAbort);\n            stream?.endMeasurement(usage);\n            clearInterval(timeoutCheck);\n            resolve(fullText);\n            break; // Break streaming when a valid finish_reason is first encountered\n          }\n        }\n      } catch (e) {\n        console.log(`\\x1b[43m\\x1b[34m[STREAMING ERROR]\\x1b[0m ${e.message}`);\n        writeResponseChunk(response, {\n          uuid,\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: e.message,\n        });\n        stream?.endMeasurement(usage);\n        clearInterval(timeoutCheck);\n        resolve(fullText); // Return what we currently have - if anything.\n      }\n    });\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  PerplexityLLM,\n  perplexityModels,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/perplexity/models.js",
    "content": "const MODELS = {\n  \"sonar-reasoning-pro\": {\n    id: \"sonar-reasoning-pro\",\n    name: \"sonar-reasoning-pro\",\n    maxLength: 127072,\n  },\n  \"sonar-reasoning\": {\n    id: \"sonar-reasoning\",\n    name: \"sonar-reasoning\",\n    maxLength: 127072,\n  },\n  \"sonar-pro\": {\n    id: \"sonar-pro\",\n    name: \"sonar-pro\",\n    maxLength: 200000,\n  },\n  sonar: {\n    id: \"sonar\",\n    name: \"sonar\",\n    maxLength: 127072,\n  },\n};\n\nmodule.exports.MODELS = MODELS;\n"
  },
  {
    "path": "server/utils/AiProviders/perplexity/scripts/.gitignore",
    "content": "*.json"
  },
  {
    "path": "server/utils/AiProviders/perplexity/scripts/chat_models.txt",
    "content": "| Model                               | Parameter Count | Context Length | Model Type      |\n| :---------------------------------- | :-------------- | :------------- | :-------------- |\n| `sonar-reasoning-pro` | 8B              | 127,072       | Chat Completion |\n| `sonar-reasoning` | 8B             | 127,072        | Chat Completion |\n| `sonar-pro` | 8B              | 200,000       | Chat Completion |\n| `sonar` | 8B             | 127,072        | Chat Completion |"
  },
  {
    "path": "server/utils/AiProviders/perplexity/scripts/parse.mjs",
    "content": "// Perplexity does not provide a simple REST API to get models,\n// so we have a table which we copy from their documentation\n// https://docs.perplexity.ai/edit/model-cards that we can\n// then parse and get all models from in a format that makes sense\n// Why this does not exist is so bizarre, but whatever.\n\n// To run, cd into this directory and run `node parse.mjs`\n// copy outputs into the export in ../models.js\n\n// Update the date below if you run this again because Perplexity added new models.\n// Last Collected: Jan 23, 2025\n\n// UPDATE: Jan 23, 2025\n// The table is no longer available on the website, but Perplexity has deprecated the\n// old models so now we can just update the chat_models.txt file with the new models\n// manually and then run this script to get the new models.\n\nimport fs from \"fs\";\n\nfunction parseChatModels() {\n  const models = {};\n  const tableString = fs.readFileSync(\"chat_models.txt\", { encoding: \"utf-8\" });\n  const rows = tableString.split(\"\\n\").slice(2);\n\n  rows.forEach((row) => {\n    let [model, _, contextLength] = row\n      .split(\"|\")\n      .slice(1, -1)\n      .map((text) => text.trim());\n    model = model.replace(/`|\\s*\\[\\d+\\]\\s*/g, \"\");\n    const maxLength = Number(contextLength.replace(/[^\\d]/g, \"\"));\n    if (model && maxLength) {\n      models[model] = {\n        id: model,\n        name: model,\n        maxLength: maxLength,\n      };\n    }\n  });\n\n  fs.writeFileSync(\n    \"chat_models.json\",\n    JSON.stringify(models, null, 2),\n    \"utf-8\"\n  );\n  return models;\n}\n\nparseChatModels();\n"
  },
  {
    "path": "server/utils/AiProviders/ppio/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  handleDefaultStreamResponseV2,\n} = require(\"../../helpers/chat/responses\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { safeJsonParse } = require(\"../../http\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst cacheFolder = path.resolve(\n  process.env.STORAGE_DIR\n    ? path.resolve(process.env.STORAGE_DIR, \"models\", \"ppio\")\n    : path.resolve(__dirname, `../../../storage/models/ppio`)\n);\n\nclass PPIOLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.PPIO_API_KEY) throw new Error(\"No PPIO API key was set.\");\n\n    this.className = \"PPIOLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.basePath = \"https://api.ppinfra.com/v3/openai/\";\n    this.openai = new OpenAIApi({\n      baseURL: this.basePath,\n      apiKey: process.env.PPIO_API_KEY ?? null,\n      defaultHeaders: {\n        \"HTTP-Referer\": \"https://anythingllm.com\",\n        \"X-API-Source\": \"anythingllm\",\n      },\n    });\n    this.model =\n      modelPreference ||\n      process.env.PPIO_MODEL_PREF ||\n      \"qwen/qwen2.5-32b-instruct\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n\n    if (!fs.existsSync(cacheFolder))\n      fs.mkdirSync(cacheFolder, { recursive: true });\n    this.cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n    this.cacheAtPath = path.resolve(cacheFolder, \".cached_at\");\n\n    this.log(`Loaded with model: ${this.model}`);\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  async #syncModels() {\n    if (fs.existsSync(this.cacheModelPath) && !this.#cacheIsStale())\n      return false;\n\n    this.log(\"Model cache is not present or stale. Fetching from PPIO API.\");\n    await fetchPPIOModels();\n    return;\n  }\n\n  #cacheIsStale() {\n    const MAX_STALE = 6.048e8; // 1 Week in MS\n    if (!fs.existsSync(this.cacheAtPath)) return true;\n    const now = Number(new Date());\n    const timestampMs = Number(fs.readFileSync(this.cacheAtPath));\n    return now - timestampMs > MAX_STALE;\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  models() {\n    if (!fs.existsSync(this.cacheModelPath)) return {};\n    return safeJsonParse(\n      fs.readFileSync(this.cacheModelPath, { encoding: \"utf-8\" }),\n      {}\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  promptWindowLimit() {\n    const model = this.models()[this.model];\n    if (!model) return 4096; // Default to 4096 if we cannot find the model\n    return model?.maxLength || 4096;\n  }\n\n  async isValidChatCompletionModel(model = \"\") {\n    await this.#syncModels();\n    const availableModels = this.models();\n    return Object.prototype.hasOwnProperty.call(availableModels, model);\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  //eslint-disable-next-line\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"auto\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    // attachments = [], - not supported\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [prompt, ...chatHistory, { role: \"user\", content: userPrompt }];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `PPIO chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !Object.prototype.hasOwnProperty.call(result.output, \"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `PPIO chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nasync function fetchPPIOModels() {\n  return await fetch(`https://api.ppinfra.com/v3/openai/models`, {\n    method: \"GET\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${process.env.PPIO_API_KEY}`,\n    },\n  })\n    .then((res) => res.json())\n    .then(({ data = [] }) => {\n      const models = {};\n      data.forEach((model) => {\n        const organization = model.id?.split(\"/\")?.[0] || \"PPIO\";\n        models[model.id] = {\n          id: model.id,\n          name: model.display_name || model.title || model.id,\n          organization,\n          maxLength: model.context_size || 4096,\n        };\n      });\n\n      if (!fs.existsSync(cacheFolder))\n        fs.mkdirSync(cacheFolder, { recursive: true });\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \"models.json\"),\n        JSON.stringify(models),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n      fs.writeFileSync(\n        path.resolve(cacheFolder, \".cached_at\"),\n        String(Number(new Date())),\n        {\n          encoding: \"utf-8\",\n        }\n      );\n      return models;\n    })\n    .catch((e) => {\n      console.error(e);\n      return {};\n    });\n}\n\nmodule.exports = {\n  PPIOLLM,\n  fetchPPIOModels,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/privatemode/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  handleDefaultStreamResponseV2,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\n\nclass PrivatemodeLLM {\n  static contextWindows = {\n    \"leon-se/gemma-3-27b-it-fp8-dynamic\": 128000,\n    \"gemma-3-27b\": 128000,\n    \"qwen3-coder-30b-a3b\": 128000,\n    \"gpt-oss-120b\": 128000,\n    \"openai/gpt-oss-120b\": 128000,\n  };\n\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.PRIVATEMODE_LLM_BASE_PATH)\n      throw new Error(\"No Privatemode Base Path was set.\");\n\n    this.className = \"PrivatemodeLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.client = new OpenAIApi({\n      baseURL: PrivatemodeLLM.parseBasePath(),\n      apiKey: null,\n    });\n\n    this.model = modelPreference || process.env.PRIVATEMODE_LLM_MODEL_PREF;\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.log(\n      `Privatemode LLM initialized with ${this.model}. ctx: ${this.promptWindowLimit()}`\n    );\n  }\n\n  /**\n   * Parse the base path for the Privatemode API\n   * so we can use it for inference requests\n   * @param {string} providedBasePath\n   * @returns {string}\n   */\n  static parseBasePath(\n    providedBasePath = process.env.PRIVATEMODE_LLM_BASE_PATH\n  ) {\n    try {\n      const baseURL = new URL(providedBasePath);\n      const basePath = `${baseURL.origin}/v1`;\n      return basePath;\n    } catch {\n      return null;\n    }\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(_modelName) {\n    const limit = PrivatemodeLLM.contextWindows[_modelName] || 16384;\n    return Number(limit);\n  }\n\n  promptWindowLimit() {\n    const limit = PrivatemodeLLM.contextWindows[this.model] || 16384;\n    return Number(limit);\n  }\n\n  async isValidChatCompletionModel(_ = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) return userPrompt;\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"auto\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        `Privatemode chat: ${this.model} is not valid or defined model for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.client.chat.completions.create({\n        model: this.model,\n        messages,\n        temperature,\n      })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage?.prompt_tokens || 0,\n        completion_tokens: result.output.usage?.completion_tokens || 0,\n        total_tokens: result.output.usage?.total_tokens || 0,\n        outputTps: result.output.usage?.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.model)\n      throw new Error(\n        `Privatemode chat: ${this.model} is not valid or defined model for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.client.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  PrivatemodeLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/sambanova/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst {\n  writeResponseChunk,\n  clientAbortedHandler,\n} = require(\"../../helpers/chat/responses\");\nconst { MODEL_MAP } = require(\"../modelMap\");\n\nclass SambaNovaLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.SAMBANOVA_LLM_API_KEY)\n      throw new Error(\"No SambaNova API key was set.\");\n    this.className = \"SambaNovaLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n\n    this.openai = new OpenAIApi({\n      baseURL: \"https://api.sambanova.ai/v1\",\n      apiKey: process.env.SAMBANOVA_LLM_API_KEY,\n    });\n    this.model = modelPreference || process.env.SAMBANOVA_LLM_MODEL_PREF;\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.log(\n      `Initialized ${this.model} with context window ${this.promptWindowLimit()}`\n    );\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    return MODEL_MAP.get(\"sambanova\", modelName) ?? 131072;\n  }\n\n  promptWindowLimit() {\n    return MODEL_MAP.get(\"sambanova\", this.model) ?? 131072;\n  }\n\n  async isValidChatCompletionModel(modelName = \"\") {\n    return !!modelName; // name just needs to exist\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) return userPrompt;\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...chatHistory,\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage?.prompt_tokens || 0,\n        completion_tokens: result.output.usage?.completion_tokens || 0,\n        total_tokens: result.output.usage?.total_tokens || 0,\n        outputTps: result.output.usage?.total_tokens_per_sec || 0,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n        stream_options: {\n          include_usage: true,\n        },\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    const { uuid = uuidv4(), sources = [] } = responseProps;\n    let hasUsageMetrics = false;\n    let usage = {\n      prompt_tokens: 0,\n      total_tokens: 0,\n      outputTps: 0,\n      completion_tokens: 0,\n    };\n\n    return new Promise(async (resolve) => {\n      let fullText = \"\";\n      const handleAbort = () => {\n        stream?.endMeasurement(usage);\n        clientAbortedHandler(resolve, fullText);\n      };\n      response.on(\"close\", handleAbort);\n\n      try {\n        for await (const chunk of stream) {\n          const message = chunk?.choices?.[0];\n          const token = message?.delta?.content;\n\n          if (\n            chunk.hasOwnProperty(\"usage\") && // exists\n            !!chunk.usage &&\n            Object.values(chunk.usage).length > 0\n          ) {\n            if (chunk.usage.hasOwnProperty(\"prompt_tokens\"))\n              usage.prompt_tokens = Number(chunk.usage.prompt_tokens);\n            if (chunk.usage.hasOwnProperty(\"completion_tokens\"))\n              usage.completion_tokens = Number(chunk.usage.completion_tokens);\n            if (chunk.usage.hasOwnProperty(\"total_tokens\"))\n              usage.total_tokens = Number(chunk.usage.total_tokens);\n            if (chunk.usage.hasOwnProperty(\"total_tokens_per_sec\"))\n              usage.outputTps = Number(chunk.usage.total_tokens_per_sec);\n            hasUsageMetrics = true;\n          }\n\n          if (token) {\n            fullText += token;\n            if (!hasUsageMetrics) usage.completion_tokens++;\n            writeResponseChunk(response, {\n              uuid,\n              sources: [],\n              type: \"textResponseChunk\",\n              textResponse: token,\n              close: false,\n              error: false,\n            });\n          }\n\n          if (\n            message?.hasOwnProperty(\"finish_reason\") &&\n            message.finish_reason !== \"\" &&\n            message.finish_reason !== null\n          ) {\n            writeResponseChunk(response, {\n              uuid,\n              sources,\n              type: \"textResponseChunk\",\n              textResponse: \"\",\n              close: true,\n              error: false,\n            });\n          }\n        }\n\n        response.removeListener(\"close\", handleAbort);\n        stream?.endMeasurement(usage);\n        resolve(fullText);\n      } catch (e) {\n        this.log(`\\x1b[43m\\x1b[34m[STREAMING ERROR]\\x1b[0m ${e.message}`);\n        writeResponseChunk(response, {\n          uuid,\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: e.message,\n        });\n        stream?.endMeasurement(usage);\n        resolve(fullText);\n      }\n    });\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  SambaNovaLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/textGenWebUI/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  handleDefaultStreamResponseV2,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\n\nclass TextGenWebUILLM {\n  constructor(embedder = null) {\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    if (!process.env.TEXT_GEN_WEB_UI_BASE_PATH)\n      throw new Error(\n        \"TextGenWebUI must have a valid base path to use for the api.\"\n      );\n\n    this.className = \"TextGenWebUILLM\";\n    this.basePath = process.env.TEXT_GEN_WEB_UI_BASE_PATH;\n    this.openai = new OpenAIApi({\n      baseURL: this.basePath,\n      apiKey: process.env.TEXT_GEN_WEB_UI_API_KEY ?? null,\n    });\n    this.model = null;\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.log(`Inference API: ${this.basePath} Model: ${this.model}`);\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(_modelName) {\n    const limit = process.env.TEXT_GEN_WEB_UI_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No token context limit was set.\");\n    return Number(limit);\n  }\n\n  // Ensure the user set a value for the token limit\n  // and if undefined - assume 4096 window.\n  promptWindowLimit() {\n    const limit = process.env.TEXT_GEN_WEB_UI_MODEL_TOKEN_LIMIT || 4096;\n    if (!limit || isNaN(Number(limit)))\n      throw new Error(\"No token context limit was set.\");\n    return Number(limit);\n  }\n\n  // Short circuit since we have no idea if the model is valid or not\n  // in pre-flight for generic endpoints\n  isValidChatCompletionModel(_modelName = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage?.prompt_tokens || 0,\n        completion_tokens: result.output.usage?.completion_tokens || 0,\n        total_tokens: result.output.usage?.total_tokens || 0,\n        outputTps: result.output.usage?.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: true,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  TextGenWebUILLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/togetherAi/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  handleDefaultStreamResponseV2,\n} = require(\"../../helpers/chat/responses\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { safeJsonParse } = require(\"../../http\");\n\nconst cacheFolder = path.resolve(\n  process.env.STORAGE_DIR\n    ? path.resolve(process.env.STORAGE_DIR, \"models\", \"togetherAi\")\n    : path.resolve(__dirname, `../../../storage/models/togetherAi`)\n);\n\nasync function togetherAiModels(apiKey = null) {\n  const cacheModelPath = path.resolve(cacheFolder, \"models.json\");\n  const cacheAtPath = path.resolve(cacheFolder, \".cached_at\");\n\n  // If cache exists and is less than 1 week old, use it\n  if (fs.existsSync(cacheModelPath) && fs.existsSync(cacheAtPath)) {\n    const now = Number(new Date());\n    const timestampMs = Number(fs.readFileSync(cacheAtPath));\n    if (now - timestampMs <= 6.048e8) {\n      // 1 Week in MS\n      return safeJsonParse(\n        fs.readFileSync(cacheModelPath, { encoding: \"utf-8\" }),\n        []\n      );\n    }\n  }\n\n  try {\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    const openai = new OpenAIApi({\n      baseURL: \"https://api.together.xyz/v1\",\n      apiKey: apiKey || process.env.TOGETHER_AI_API_KEY || null,\n    });\n\n    const response = await openai.models.list();\n\n    // Filter and transform models into the expected format\n    // Only include chat models\n    const validModels = response.body\n      .filter((model) => [\"chat\"].includes(model.type))\n      .map((model) => ({\n        id: model.id,\n        name: model.display_name || model.id,\n        organization: model.organization || \"Unknown\",\n        type: model.type,\n        maxLength: model.context_length || 4096,\n      }));\n\n    // Cache the results\n    if (!fs.existsSync(cacheFolder))\n      fs.mkdirSync(cacheFolder, { recursive: true });\n    fs.writeFileSync(cacheModelPath, JSON.stringify(validModels), {\n      encoding: \"utf-8\",\n    });\n    fs.writeFileSync(cacheAtPath, String(Number(new Date())), {\n      encoding: \"utf-8\",\n    });\n\n    return validModels;\n  } catch (error) {\n    console.error(\"Error fetching Together AI models:\", error);\n    // If cache exists but is stale, still use it as fallback\n    if (fs.existsSync(cacheModelPath)) {\n      return safeJsonParse(\n        fs.readFileSync(cacheModelPath, { encoding: \"utf-8\" }),\n        []\n      );\n    }\n    return [];\n  }\n}\n\nclass TogetherAiLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.TOGETHER_AI_API_KEY)\n      throw new Error(\"No TogetherAI API key was set.\");\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.className = \"TogetherAiLLM\";\n    this.openai = new OpenAIApi({\n      baseURL: \"https://api.together.xyz/v1\",\n      apiKey: process.env.TOGETHER_AI_API_KEY ?? null,\n    });\n    this.model = modelPreference || process.env.TOGETHER_AI_MODEL_PREF;\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = !embedder ? new NativeEmbedder() : embedder;\n    this.defaultTemp = 0.7;\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  async allModelInformation() {\n    const models = await togetherAiModels();\n    return models.reduce((acc, model) => {\n      acc[model.id] = model;\n      return acc;\n    }, {});\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static async promptWindowLimit(modelName) {\n    const models = await togetherAiModels();\n    const model = models.find((m) => m.id === modelName);\n    return model?.maxLength || 4096;\n  }\n\n  async promptWindowLimit() {\n    const models = await togetherAiModels();\n    const model = models.find((m) => m.id === this.model);\n    return model?.maxLength || 4096;\n  }\n\n  async isValidChatCompletionModel(model = \"\") {\n    const models = await togetherAiModels();\n    const foundModel = models.find((m) => m.id === model);\n    return foundModel && foundModel.type === \"chat\";\n  }\n\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...chatHistory,\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `TogetherAI chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage?.prompt_tokens || 0,\n        completion_tokens: result.output.usage?.completion_tokens || 0,\n        total_tokens: result.output.usage?.total_tokens || 0,\n        outputTps: result.output.usage?.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!(await this.isValidChatCompletionModel(this.model)))\n      throw new Error(\n        `TogetherAI chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  TogetherAiLLM,\n  togetherAiModels,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/xai/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst {\n  handleDefaultStreamResponseV2,\n  formatChatHistory,\n} = require(\"../../helpers/chat/responses\");\nconst { MODEL_MAP } = require(\"../modelMap\");\n\nclass XAiLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.XAI_LLM_API_KEY)\n      throw new Error(\"No xAI API key was set.\");\n    this.className = \"XAiLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n\n    this.openai = new OpenAIApi({\n      baseURL: \"https://api.x.ai/v1\",\n      apiKey: process.env.XAI_LLM_API_KEY,\n    });\n    this.model =\n      modelPreference || process.env.XAI_LLM_MODEL_PREF || \"grok-beta\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.log(\n      `Initialized ${this.model} with context window ${this.promptWindowLimit()}`\n    );\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    return MODEL_MAP.get(\"xai\", modelName) ?? 131_072;\n  }\n\n  promptWindowLimit() {\n    return MODEL_MAP.get(\"xai\", this.model) ?? 131_072;\n  }\n\n  isValidChatCompletionModel(_modelName = \"\") {\n    return true;\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) {\n      return userPrompt;\n    }\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n          detail: \"high\",\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [], // This is the specific attachment for only this prompt\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...formatChatHistory(chatHistory, this.#generateContent),\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.isValidChatCompletionModel(this.model))\n      throw new Error(\n        `xAI chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage.prompt_tokens || 0,\n        completion_tokens: result.output.usage.completion_tokens || 0,\n        total_tokens: result.output.usage.total_tokens || 0,\n        outputTps: result.output.usage.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    if (!this.isValidChatCompletionModel(this.model))\n      throw new Error(\n        `xAI chat: ${this.model} is not valid for chat completion!`\n      );\n\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  XAiLLM,\n};\n"
  },
  {
    "path": "server/utils/AiProviders/zai/index.js",
    "content": "const { NativeEmbedder } = require(\"../../EmbeddingEngines/native\");\nconst {\n  LLMPerformanceMonitor,\n} = require(\"../../helpers/chat/LLMPerformanceMonitor\");\nconst {\n  handleDefaultStreamResponseV2,\n} = require(\"../../helpers/chat/responses\");\nconst { MODEL_MAP } = require(\"../modelMap\");\n\nclass ZAiLLM {\n  constructor(embedder = null, modelPreference = null) {\n    if (!process.env.ZAI_API_KEY) throw new Error(\"No Z.AI API key was set.\");\n    this.className = \"ZAiLLM\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n\n    this.openai = new OpenAIApi({\n      baseURL: \"https://api.z.ai/api/paas/v4\",\n      apiKey: process.env.ZAI_API_KEY,\n    });\n    this.model = modelPreference || process.env.ZAI_MODEL_PREF || \"glm-4.5\";\n    this.limits = {\n      history: this.promptWindowLimit() * 0.15,\n      system: this.promptWindowLimit() * 0.15,\n      user: this.promptWindowLimit() * 0.7,\n    };\n\n    this.embedder = embedder ?? new NativeEmbedder();\n    this.defaultTemp = 0.7;\n    this.log(\n      `Initialized ${this.model} with context window ${this.promptWindowLimit()}`\n    );\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #appendContext(contextTexts = []) {\n    if (!contextTexts || !contextTexts.length) return \"\";\n    return (\n      \"\\nContext:\\n\" +\n      contextTexts\n        .map((text, i) => {\n          return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n        })\n        .join(\"\")\n    );\n  }\n\n  streamingEnabled() {\n    return \"streamGetChatCompletion\" in this;\n  }\n\n  static promptWindowLimit(modelName) {\n    return MODEL_MAP.get(\"zai\", modelName) ?? 131072;\n  }\n\n  promptWindowLimit() {\n    return MODEL_MAP.get(\"zai\", this.model) ?? 131072;\n  }\n\n  async isValidChatCompletionModel(modelName = \"\") {\n    return !!modelName; // name just needs to exist\n  }\n\n  /**\n   * Generates appropriate content array for a message + attachments.\n   * @param {{userPrompt:string, attachments: import(\"../../helpers\").Attachment[]}}\n   * @returns {string|object[]}\n   */\n  #generateContent({ userPrompt, attachments = [] }) {\n    if (!attachments.length) return userPrompt;\n\n    const content = [{ type: \"text\", text: userPrompt }];\n    for (let attachment of attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n        },\n      });\n    }\n    return content.flat();\n  }\n\n  /**\n   * Construct the user prompt for this model.\n   * @param {{attachments: import(\"../../helpers\").Attachment[]}} param0\n   * @returns\n   */\n  constructPrompt({\n    systemPrompt = \"\",\n    contextTexts = [],\n    chatHistory = [],\n    userPrompt = \"\",\n    attachments = [],\n  }) {\n    const prompt = {\n      role: \"system\",\n      content: `${systemPrompt}${this.#appendContext(contextTexts)}`,\n    };\n    return [\n      prompt,\n      ...chatHistory,\n      {\n        role: \"user\",\n        content: this.#generateContent({ userPrompt, attachments }),\n      },\n    ];\n  }\n\n  async getChatCompletion(messages = null, { temperature = 0.7 }) {\n    const result = await LLMPerformanceMonitor.measureAsyncFunction(\n      this.openai.chat.completions\n        .create({\n          model: this.model,\n          messages,\n          temperature,\n        })\n        .catch((e) => {\n          throw new Error(e.message);\n        })\n    );\n\n    if (\n      !result.output.hasOwnProperty(\"choices\") ||\n      result.output.choices.length === 0\n    )\n      return null;\n\n    return {\n      textResponse: result.output.choices[0].message.content,\n      metrics: {\n        prompt_tokens: result.output.usage?.prompt_tokens || 0,\n        completion_tokens: result.output.usage?.completion_tokens || 0,\n        total_tokens: result.output.usage?.total_tokens || 0,\n        outputTps: result.output.usage?.completion_tokens / result.duration,\n        duration: result.duration,\n        model: this.model,\n        provider: this.className,\n        timestamp: new Date(),\n      },\n    };\n  }\n\n  async streamGetChatCompletion(messages = null, { temperature = 0.7 }) {\n    const measuredStreamRequest = await LLMPerformanceMonitor.measureStream({\n      func: this.openai.chat.completions.create({\n        model: this.model,\n        stream: true,\n        messages,\n        temperature,\n      }),\n      messages,\n      runPromptTokenCalculation: false,\n      modelTag: this.model,\n      provider: this.className,\n    });\n\n    return measuredStreamRequest;\n  }\n\n  handleStream(response, stream, responseProps) {\n    return handleDefaultStreamResponseV2(response, stream, responseProps);\n  }\n\n  // Simple wrapper for dynamic embedder & normalize interface for all LLM implementations\n  async embedTextInput(textInput) {\n    return await this.embedder.embedTextInput(textInput);\n  }\n  async embedChunks(textChunks = []) {\n    return await this.embedder.embedChunks(textChunks);\n  }\n\n  async compressMessages(promptArgs = {}, rawHistory = []) {\n    const { messageArrayCompressor } = require(\"../../helpers/chat\");\n    const messageArray = this.constructPrompt(promptArgs);\n    return await messageArrayCompressor(this, messageArray, rawHistory);\n  }\n}\n\nmodule.exports = {\n  ZAiLLM,\n};\n"
  },
  {
    "path": "server/utils/BackgroundWorkers/index.js",
    "content": "const path = require(\"path\");\nconst Graceful = require(\"@ladjs/graceful\");\nconst Bree = require(\"@mintplex-labs/bree\");\nconst setLogger = require(\"../logger\");\n\nclass BackgroundService {\n  name = \"BackgroundWorkerService\";\n  static _instance = null;\n  documentSyncEnabled = false;\n  #root = path.resolve(__dirname, \"../../jobs\");\n\n  #alwaysRunJobs = [\n    {\n      name: \"cleanup-orphan-documents\",\n      timeout: \"1m\",\n      interval: \"12hr\",\n    },\n  ];\n\n  #documentSyncJobs = [\n    // Job for auto-sync of documents\n    // https://github.com/breejs/bree\n    {\n      name: \"sync-watched-documents\",\n      interval: \"1hr\",\n    },\n  ];\n\n  constructor() {\n    if (BackgroundService._instance) {\n      this.#log(\"SINGLETON LOCK: Using existing BackgroundService.\");\n      return BackgroundService._instance;\n    }\n\n    this.logger = setLogger();\n    BackgroundService._instance = this;\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[36m[${this.name}]\\x1b[0m ${text}`, ...args);\n  }\n\n  async boot() {\n    const { DocumentSyncQueue } = require(\"../../models/documentSyncQueue\");\n    this.documentSyncEnabled = await DocumentSyncQueue.enabled();\n    const jobsToRun = this.jobs();\n\n    this.#log(\"Starting...\");\n    this.bree = new Bree({\n      logger: this.logger,\n      root: this.#root,\n      jobs: jobsToRun,\n      errorHandler: this.onError,\n      workerMessageHandler: this.onWorkerMessageHandler,\n      runJobsAs: \"process\",\n    });\n    this.graceful = new Graceful({ brees: [this.bree], logger: this.logger });\n    this.graceful.listen();\n    this.bree.start();\n    this.#log(\n      `Service started with ${jobsToRun.length} jobs`,\n      jobsToRun.map((j) => j.name)\n    );\n  }\n\n  async stop() {\n    this.#log(\"Stopping...\");\n    if (!!this.graceful && !!this.bree) this.graceful.stopBree(this.bree, 0);\n    this.bree = null;\n    this.graceful = null;\n    this.#log(\"Service stopped\");\n  }\n\n  /** @returns {import(\"@mintplex-labs/bree\").Job[]} */\n  jobs() {\n    const activeJobs = [...this.#alwaysRunJobs];\n    if (this.documentSyncEnabled) activeJobs.push(...this.#documentSyncJobs);\n    return activeJobs;\n  }\n\n  onError(error, _workerMetadata) {\n    this.logger.error(`${error.message}`, {\n      service: \"bg-worker\",\n      origin: error.name,\n    });\n  }\n\n  onWorkerMessageHandler(message, _workerMetadata) {\n    this.logger.info(`${message.message}`, {\n      service: \"bg-worker\",\n      origin: message.name,\n    });\n  }\n}\n\nmodule.exports.BackgroundService = BackgroundService;\n"
  },
  {
    "path": "server/utils/DocumentManager/index.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\n\nconst documentsPath =\n  process.env.NODE_ENV === \"development\"\n    ? path.resolve(__dirname, `../../storage/documents`)\n    : path.resolve(process.env.STORAGE_DIR, `documents`);\n\nclass DocumentManager {\n  constructor({ workspace = null, maxTokens = null }) {\n    this.workspace = workspace;\n    this.maxTokens = maxTokens || Number.POSITIVE_INFINITY;\n    this.documentStoragePath = documentsPath;\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[DocumentManager]\\x1b[0m ${text}`, ...args);\n  }\n\n  async pinnedDocuments() {\n    if (!this.workspace) return [];\n    const { Document } = require(\"../../models/documents\");\n    return await Document.where({\n      workspaceId: Number(this.workspace.id),\n      pinned: true,\n    });\n  }\n\n  async pinnedDocs() {\n    if (!this.workspace) return [];\n    const docPaths = (await this.pinnedDocuments()).map((doc) => doc.docpath);\n    if (docPaths.length === 0) return [];\n\n    let tokens = 0;\n    const pinnedDocs = [];\n    for await (const docPath of docPaths) {\n      try {\n        const filePath = path.resolve(this.documentStoragePath, docPath);\n        const data = JSON.parse(\n          fs.readFileSync(filePath, { encoding: \"utf-8\" })\n        );\n\n        if (\n          !data.hasOwnProperty(\"pageContent\") ||\n          !data.hasOwnProperty(\"token_count_estimate\")\n        ) {\n          this.log(\n            `Skipping document - Could not find page content or token_count_estimate in pinned source.`\n          );\n          continue;\n        }\n\n        if (tokens >= this.maxTokens) {\n          this.log(\n            `Skipping document - Token limit of ${this.maxTokens} has already been exceeded by pinned documents.`\n          );\n          continue;\n        }\n\n        pinnedDocs.push(data);\n        tokens += data.token_count_estimate || 0;\n      } catch {}\n    }\n\n    this.log(\n      `Found ${pinnedDocs.length} pinned sources - prepending to content with ~${tokens} tokens of content.`\n    );\n    return pinnedDocs;\n  }\n}\n\nmodule.exports.DocumentManager = DocumentManager;\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/azureOpenAi/index.js",
    "content": "const { toChunks } = require(\"../../helpers\");\n\nclass AzureOpenAiEmbedder {\n  constructor() {\n    const { AzureOpenAI } = require(\"openai\");\n    if (!process.env.AZURE_OPENAI_ENDPOINT)\n      throw new Error(\"No Azure API endpoint was set.\");\n    if (!process.env.AZURE_OPENAI_KEY)\n      throw new Error(\"No Azure API key was set.\");\n\n    this.className = \"AzureOpenAiEmbedder\";\n    this.apiVersion = \"2024-12-01-preview\";\n    const openai = new AzureOpenAI({\n      apiKey: process.env.AZURE_OPENAI_KEY,\n      endpoint: process.env.AZURE_OPENAI_ENDPOINT,\n      apiVersion: this.apiVersion,\n    });\n\n    // We cannot assume the model fallback since the model is based on the deployment name\n    // and not the model name - so this will throw on embedding if the model is not defined.\n    this.model = process.env.EMBEDDING_MODEL_PREF;\n    this.openai = openai;\n\n    // Limit of how many strings we can process in a single pass to stay with resource or network limits\n    // https://learn.microsoft.com/en-us/azure/ai-services/openai/faq#i-am-trying-to-use-embeddings-and-received-the-error--invalidrequesterror--too-many-inputs--the-max-number-of-inputs-is-1---how-do-i-fix-this-:~:text=consisting%20of%20up%20to%2016%20inputs%20per%20API%20request\n    this.maxConcurrentChunks = 16;\n\n    // https://learn.microsoft.com/en-us/answers/questions/1188074/text-embedding-ada-002-token-context-length\n    this.embeddingMaxChunkLength = 2048;\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  async embedTextInput(textInput) {\n    const result = await this.embedChunks(\n      Array.isArray(textInput) ? textInput : [textInput]\n    );\n    return result?.[0] || [];\n  }\n\n  async embedChunks(textChunks = []) {\n    if (!this.model) throw new Error(\"No Embedding Model preference defined.\");\n\n    this.log(`Embedding ${textChunks.length} chunks...`);\n    // Because there is a limit on how many chunks can be sent at once to Azure OpenAI\n    // we concurrently execute each max batch of text chunks possible.\n    // Refer to constructor maxConcurrentChunks for more info.\n    const embeddingRequests = [];\n    for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) {\n      embeddingRequests.push(\n        new Promise((resolve) => {\n          this.openai.embeddings\n            .create({\n              model: this.model,\n              input: chunk,\n            })\n            .then((res) => {\n              resolve({ data: res.data, error: null });\n            })\n            .catch((e) => {\n              e.type =\n                e?.response?.data?.error?.code ||\n                e?.response?.status ||\n                \"failed_to_embed\";\n              e.message = e?.response?.data?.error?.message || e.message;\n              resolve({ data: [], error: e });\n            });\n        })\n      );\n    }\n\n    const { data = [], error = null } = await Promise.all(\n      embeddingRequests\n    ).then((results) => {\n      // If any errors were returned from Azure abort the entire sequence because the embeddings\n      // will be incomplete.\n      const errors = results\n        .filter((res) => !!res.error)\n        .map((res) => res.error)\n        .flat();\n      if (errors.length > 0) {\n        let uniqueErrors = new Set();\n        errors.map((error) =>\n          uniqueErrors.add(`[${error.type}]: ${error.message}`)\n        );\n\n        return {\n          data: [],\n          error: Array.from(uniqueErrors).join(\", \"),\n        };\n      }\n      return {\n        data: results.map((res) => res?.data || []).flat(),\n        error: null,\n      };\n    });\n\n    if (!!error) throw new Error(`Azure OpenAI Failed to embed: ${error}`);\n    return data.length > 0 &&\n      data.every((embd) => embd.hasOwnProperty(\"embedding\"))\n      ? data.map((embd) => embd.embedding)\n      : null;\n  }\n}\n\nmodule.exports = {\n  AzureOpenAiEmbedder,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/cohere/index.js",
    "content": "const { toChunks } = require(\"../../helpers\");\n\nclass CohereEmbedder {\n  constructor() {\n    if (!process.env.COHERE_API_KEY)\n      throw new Error(\"No Cohere API key was set.\");\n\n    const { CohereClient } = require(\"cohere-ai\");\n    const cohere = new CohereClient({\n      token: process.env.COHERE_API_KEY,\n    });\n\n    this.cohere = cohere;\n    this.model = process.env.EMBEDDING_MODEL_PREF || \"embed-english-v3.0\";\n    this.inputType = \"search_document\";\n\n    // Limit of how many strings we can process in a single pass to stay with resource or network limits\n    this.maxConcurrentChunks = 96; // Cohere's limit per request is 96\n    this.embeddingMaxChunkLength = 1945; // https://docs.cohere.com/docs/embed-2 - assume a token is roughly 4 letters with some padding\n  }\n\n  async embedTextInput(textInput) {\n    this.inputType = \"search_query\";\n    const result = await this.embedChunks([textInput]);\n    return result?.[0] || [];\n  }\n\n  async embedChunks(textChunks = []) {\n    const embeddingRequests = [];\n    this.inputType = \"search_document\";\n\n    for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) {\n      embeddingRequests.push(\n        new Promise((resolve) => {\n          this.cohere\n            .embed({\n              texts: chunk,\n              model: this.model,\n              inputType: this.inputType,\n            })\n            .then((res) => {\n              resolve({ data: res.embeddings, error: null });\n            })\n            .catch((e) => {\n              e.type =\n                e?.response?.data?.error?.code ||\n                e?.response?.status ||\n                \"failed_to_embed\";\n              e.message = e?.response?.data?.error?.message || e.message;\n              resolve({ data: [], error: e });\n            });\n        })\n      );\n    }\n\n    const { data = [], error = null } = await Promise.all(\n      embeddingRequests\n    ).then((results) => {\n      const errors = results\n        .filter((res) => !!res.error)\n        .map((res) => res.error)\n        .flat();\n\n      if (errors.length > 0) {\n        let uniqueErrors = new Set();\n        errors.map((error) =>\n          uniqueErrors.add(`[${error.type}]: ${error.message}`)\n        );\n        return { data: [], error: Array.from(uniqueErrors).join(\", \") };\n      }\n\n      return {\n        data: results.map((res) => res?.data || []).flat(),\n        error: null,\n      };\n    });\n\n    if (!!error) throw new Error(`Cohere Failed to embed: ${error}`);\n\n    return data.length > 0 ? data : null;\n  }\n}\n\nmodule.exports = {\n  CohereEmbedder,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/gemini/index.js",
    "content": "const { toChunks } = require(\"../../helpers\");\n\nconst MODEL_MAP = {\n  \"gemini-embedding-001\": 2048,\n};\n\nclass GeminiEmbedder {\n  constructor() {\n    if (!process.env.GEMINI_EMBEDDING_API_KEY)\n      throw new Error(\"No Gemini API key was set.\");\n\n    this.className = \"GeminiEmbedder\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.model = process.env.EMBEDDING_MODEL_PREF || \"gemini-embedding-001\";\n    this.openai = new OpenAIApi({\n      apiKey: process.env.GEMINI_EMBEDDING_API_KEY,\n      // Even models that are v1 in gemini API can be used with v1beta/openai/ endpoint and nobody knows why.\n      baseURL: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n    });\n\n    this.maxConcurrentChunks = 4;\n\n    // https://ai.google.dev/gemini-api/docs/models/gemini#text-embedding-and-embedding\n    this.embeddingMaxChunkLength = MODEL_MAP[this.model] || 2_048;\n    this.log(\n      `Initialized with ${this.model} - Max Size: ${this.embeddingMaxChunkLength}` +\n        (this.outputDimensions\n          ? ` - Output Dimensions: ${this.outputDimensions}`\n          : \" Assuming default output dimensions\")\n    );\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  get outputDimensions() {\n    if (\n      process.env.EMBEDDING_OUTPUT_DIMENSIONS &&\n      !isNaN(process.env.EMBEDDING_OUTPUT_DIMENSIONS) &&\n      process.env.EMBEDDING_OUTPUT_DIMENSIONS > 0\n    )\n      return parseInt(process.env.EMBEDDING_OUTPUT_DIMENSIONS);\n    return null;\n  }\n\n  /**\n   * Embeds a single text input\n   * @param {string|string[]} textInput - The text to embed\n   * @returns {Promise<Array<number>>} The embedding values\n   */\n  async embedTextInput(textInput) {\n    const result = await this.embedChunks(\n      Array.isArray(textInput) ? textInput : [textInput]\n    );\n    return result?.[0] || [];\n  }\n\n  /**\n   * Embeds a list of text inputs\n   * @param {string[]} textChunks - The list of text to embed\n   * @returns {Promise<Array<Array<number>>>} The embedding values\n   */\n  async embedChunks(textChunks = []) {\n    this.log(`Embedding ${textChunks.length} chunks...`);\n\n    // Because there is a hard POST limit on how many chunks can be sent at once to OpenAI (~8mb)\n    // we concurrently execute each max batch of text chunks possible.\n    // Refer to constructor maxConcurrentChunks for more info.\n    const embeddingRequests = [];\n    for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) {\n      embeddingRequests.push(\n        new Promise((resolve) => {\n          this.openai.embeddings\n            .create({\n              model: this.model,\n              input: chunk,\n              dimensions: this.outputDimensions,\n            })\n            .then((result) => {\n              resolve({ data: result?.data, error: null });\n            })\n            .catch((e) => {\n              e.type =\n                e?.response?.data?.error?.code ||\n                e?.response?.status ||\n                \"failed_to_embed\";\n              e.message = e?.response?.data?.error?.message || e.message;\n              resolve({ data: [], error: e });\n            });\n        })\n      );\n    }\n\n    const { data = [], error = null } = await Promise.all(\n      embeddingRequests\n    ).then((results) => {\n      // If any errors were returned from OpenAI abort the entire sequence because the embeddings\n      // will be incomplete.\n      const errors = results\n        .filter((res) => !!res.error)\n        .map((res) => res.error)\n        .flat();\n      if (errors.length > 0) {\n        let uniqueErrors = new Set();\n        errors.map((error) =>\n          uniqueErrors.add(`[${error.type}]: ${error.message}`)\n        );\n\n        return {\n          data: [],\n          error: Array.from(uniqueErrors).join(\", \"),\n        };\n      }\n      return {\n        data: results.map((res) => res?.data || []).flat(),\n        error: null,\n      };\n    });\n\n    if (!!error) throw new Error(`Gemini Failed to embed: ${error}`);\n    return data.length > 0 &&\n      data.every((embd) => embd.hasOwnProperty(\"embedding\"))\n      ? data.map((embd) => embd.embedding)\n      : null;\n  }\n}\n\nmodule.exports = {\n  GeminiEmbedder,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/genericOpenAi/index.js",
    "content": "const { toChunks, maximumChunkLength } = require(\"../../helpers\");\n\nclass GenericOpenAiEmbedder {\n  constructor() {\n    if (!process.env.EMBEDDING_BASE_PATH)\n      throw new Error(\n        \"GenericOpenAI must have a valid base path to use for the api.\"\n      );\n    this.className = \"GenericOpenAiEmbedder\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.basePath = process.env.EMBEDDING_BASE_PATH;\n    this.openai = new OpenAIApi({\n      baseURL: this.basePath,\n      apiKey: process.env.GENERIC_OPEN_AI_EMBEDDING_API_KEY ?? null,\n    });\n    this.model = process.env.EMBEDDING_MODEL_PREF ?? null;\n    this.embeddingMaxChunkLength = maximumChunkLength();\n\n    // this.maxConcurrentChunks is delegated to the getter below.\n    // Refer to your specific model and provider you use this class with to determine a valid maxChunkLength\n    this.log(`Initialized ${this.model}`, {\n      baseURL: this.basePath,\n      maxConcurrentChunks: this.maxConcurrentChunks,\n      embeddingMaxChunkLength: this.embeddingMaxChunkLength,\n    });\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * returns the `GENERIC_OPEN_AI_EMBEDDING_API_DELAY_MS` env variable as a number or null if the env variable is not set or is not a number.\n   * The minimum delay is 500ms.\n   *\n   * For some implementation this is necessary to avoid 429 errors due to rate limiting or\n   * hardware limitations where a single-threaded process is not able to handle the requests fast enough.\n   * @returns {number}\n   */\n  get apiRequestDelay() {\n    if (!(\"GENERIC_OPEN_AI_EMBEDDING_API_DELAY_MS\" in process.env)) return null;\n    if (isNaN(Number(process.env.GENERIC_OPEN_AI_EMBEDDING_API_DELAY_MS)))\n      return null;\n    const delayTimeout = Number(\n      process.env.GENERIC_OPEN_AI_EMBEDDING_API_DELAY_MS\n    );\n    if (delayTimeout < 500) return 500; // minimum delay of 500ms\n    return delayTimeout;\n  }\n\n  /**\n   * runs the delay if it is set and valid.\n   * @returns {Promise<void>}\n   */\n  async runDelay() {\n    if (!this.apiRequestDelay) return;\n    this.log(`Delaying new batch request for ${this.apiRequestDelay}ms`);\n    await new Promise((resolve) => setTimeout(resolve, this.apiRequestDelay));\n  }\n\n  /**\n   * returns the `GENERIC_OPEN_AI_EMBEDDING_MAX_CONCURRENT_CHUNKS` env variable as a number\n   * or 500 if the env variable is not set or is not a number.\n   * @returns {number}\n   */\n  get maxConcurrentChunks() {\n    if (!process.env.GENERIC_OPEN_AI_EMBEDDING_MAX_CONCURRENT_CHUNKS)\n      return 500;\n    if (\n      isNaN(Number(process.env.GENERIC_OPEN_AI_EMBEDDING_MAX_CONCURRENT_CHUNKS))\n    )\n      return 500;\n    return Number(process.env.GENERIC_OPEN_AI_EMBEDDING_MAX_CONCURRENT_CHUNKS);\n  }\n\n  async embedTextInput(textInput) {\n    const result = await this.embedChunks(\n      Array.isArray(textInput) ? textInput : [textInput]\n    );\n    return result?.[0] || [];\n  }\n\n  async embedChunks(textChunks = []) {\n    // Because there is a hard POST limit on how many chunks can be sent at once to OpenAI (~8mb)\n    // we sequentially execute each max batch of text chunks possible.\n    // Refer to constructor maxConcurrentChunks for more info.\n    const allResults = [];\n    for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) {\n      const { data = [], error = null } = await new Promise((resolve) => {\n        this.openai.embeddings\n          .create({\n            model: this.model,\n            input: chunk,\n          })\n          .then((result) => resolve({ data: result?.data, error: null }))\n          .catch((e) => {\n            e.type =\n              e?.response?.data?.error?.code ||\n              e?.response?.status ||\n              \"failed_to_embed\";\n            e.message = e?.response?.data?.error?.message || e.message;\n            resolve({ data: [], error: e });\n          });\n      });\n\n      // If any errors were returned from OpenAI abort the entire sequence because the embeddings\n      // will be incomplete.\n      if (error)\n        throw new Error(`GenericOpenAI Failed to embed: ${error.message}`);\n      allResults.push(...(data || []));\n      if (this.apiRequestDelay) await this.runDelay();\n    }\n\n    return allResults.length > 0 &&\n      allResults.every((embd) => embd.hasOwnProperty(\"embedding\"))\n      ? allResults.map((embd) => embd.embedding)\n      : null;\n  }\n}\n\nmodule.exports = {\n  GenericOpenAiEmbedder,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/lemonade/index.js",
    "content": "const { parseLemonadeServerEndpoint } = require(\"../../AiProviders/lemonade\");\n\nclass LemonadeEmbedder {\n  constructor() {\n    if (!process.env.EMBEDDING_BASE_PATH)\n      throw new Error(\"No Lemonade API Base Path was set.\");\n    if (!process.env.EMBEDDING_MODEL_PREF)\n      throw new Error(\"No Embedding Model Pref was set.\");\n    this.className = \"LemonadeEmbedder\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.lemonade = new OpenAIApi({\n      baseURL: parseLemonadeServerEndpoint(\n        process.env.EMBEDDING_BASE_PATH,\n        \"openai\"\n      ),\n      apiKey: null,\n    });\n    this.model = process.env.EMBEDDING_MODEL_PREF;\n\n    this.embeddingMaxChunkLength =\n      process.env.EMBEDDING_MODEL_MAX_CHUNK_LENGTH || 8_191;\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  async embedTextInput(textInput) {\n    try {\n      this.log(`Embedding text input...`);\n      const response = await this.lemonade.embeddings.create({\n        model: this.model,\n        input: textInput,\n      });\n      return response?.data[0]?.embedding || [];\n    } catch (error) {\n      console.error(\"Failed to get embedding from Lemonade.\", error.message);\n      return [];\n    }\n  }\n\n  async embedChunks(textChunks = []) {\n    try {\n      this.log(`Embedding ${textChunks.length} chunks of text...`);\n      const response = await this.lemonade.embeddings.create({\n        model: this.model,\n        input: textChunks,\n      });\n      return response?.data?.map((emb) => emb.embedding) || [];\n    } catch (error) {\n      console.error(\"Failed to get embeddings from Lemonade.\", error.message);\n      return new Array(textChunks.length).fill([]);\n    }\n  }\n}\n\nmodule.exports = {\n  LemonadeEmbedder,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/liteLLM/index.js",
    "content": "const { toChunks, maximumChunkLength } = require(\"../../helpers\");\n\nclass LiteLLMEmbedder {\n  constructor() {\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    if (!process.env.LITE_LLM_BASE_PATH)\n      throw new Error(\n        \"LiteLLM must have a valid base path to use for the api.\"\n      );\n    this.basePath = process.env.LITE_LLM_BASE_PATH;\n    this.openai = new OpenAIApi({\n      baseURL: this.basePath,\n      apiKey: process.env.LITE_LLM_API_KEY ?? null,\n    });\n    this.model = process.env.EMBEDDING_MODEL_PREF || \"text-embedding-ada-002\";\n\n    // Limit of how many strings we can process in a single pass to stay with resource or network limits\n    this.maxConcurrentChunks = 500;\n    this.embeddingMaxChunkLength = maximumChunkLength();\n  }\n\n  async embedTextInput(textInput) {\n    const result = await this.embedChunks(\n      Array.isArray(textInput) ? textInput : [textInput]\n    );\n    return result?.[0] || [];\n  }\n\n  async embedChunks(textChunks = []) {\n    // Because there is a hard POST limit on how many chunks can be sent at once to LiteLLM (~8mb)\n    // we concurrently execute each max batch of text chunks possible.\n    // Refer to constructor maxConcurrentChunks for more info.\n    const embeddingRequests = [];\n    for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) {\n      embeddingRequests.push(\n        new Promise((resolve) => {\n          this.openai.embeddings\n            .create({\n              model: this.model,\n              input: chunk,\n            })\n            .then((result) => {\n              resolve({ data: result?.data, error: null });\n            })\n            .catch((e) => {\n              e.type =\n                e?.response?.data?.error?.code ||\n                e?.response?.status ||\n                \"failed_to_embed\";\n              e.message = e?.response?.data?.error?.message || e.message;\n              resolve({ data: [], error: e });\n            });\n        })\n      );\n    }\n\n    const { data = [], error = null } = await Promise.all(\n      embeddingRequests\n    ).then((results) => {\n      // If any errors were returned from LiteLLM abort the entire sequence because the embeddings\n      // will be incomplete.\n      const errors = results\n        .filter((res) => !!res.error)\n        .map((res) => res.error)\n        .flat();\n      if (errors.length > 0) {\n        let uniqueErrors = new Set();\n        errors.map((error) =>\n          uniqueErrors.add(`[${error.type}]: ${error.message}`)\n        );\n\n        return {\n          data: [],\n          error: Array.from(uniqueErrors).join(\", \"),\n        };\n      }\n      return {\n        data: results.map((res) => res?.data || []).flat(),\n        error: null,\n      };\n    });\n\n    if (!!error) throw new Error(`LiteLLM Failed to embed: ${error}`);\n    return data.length > 0 &&\n      data.every((embd) => embd.hasOwnProperty(\"embedding\"))\n      ? data.map((embd) => embd.embedding)\n      : null;\n  }\n}\n\nmodule.exports = {\n  LiteLLMEmbedder,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/lmstudio/index.js",
    "content": "const { parseLMStudioBasePath } = require(\"../../AiProviders/lmStudio\");\nconst { maximumChunkLength } = require(\"../../helpers\");\n\nclass LMStudioEmbedder {\n  constructor() {\n    if (!process.env.EMBEDDING_BASE_PATH)\n      throw new Error(\"No embedding base path was set.\");\n    if (!process.env.EMBEDDING_MODEL_PREF)\n      throw new Error(\"No embedding model was set.\");\n\n    const apiKey = process.env.LMSTUDIO_AUTH_TOKEN ?? null;\n    this.className = \"LMStudioEmbedder\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.lmstudio = new OpenAIApi({\n      baseURL: parseLMStudioBasePath(process.env.EMBEDDING_BASE_PATH),\n      apiKey,\n    });\n    this.model = process.env.EMBEDDING_MODEL_PREF;\n\n    // Limit of how many strings we can process in a single pass to stay with resource or network limits\n    this.maxConcurrentChunks = 1;\n    this.embeddingMaxChunkLength = maximumChunkLength();\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  async #isAlive() {\n    return await this.lmstudio.models\n      .list()\n      .then((res) => res?.data?.length > 0)\n      .catch((e) => {\n        this.log(e.message);\n        return false;\n      });\n  }\n\n  async embedTextInput(textInput) {\n    const result = await this.embedChunks(\n      Array.isArray(textInput) ? textInput : [textInput]\n    );\n    return result?.[0] || [];\n  }\n\n  async embedChunks(textChunks = []) {\n    if (!(await this.#isAlive()))\n      throw new Error(\n        `LMStudio service could not be reached. Is LMStudio running?`\n      );\n\n    this.log(\n      `Embedding ${textChunks.length} chunks of text with ${this.model}.`\n    );\n\n    // LMStudio will drop all queued requests now? So if there are many going on\n    // we need to do them sequentially or else only the first resolves and the others\n    // get dropped or go unanswered >:(\n    let results = [];\n    let hasError = false;\n    for (const chunk of textChunks) {\n      if (hasError) break; // If an error occurred don't continue and exit early.\n      results.push(\n        await this.lmstudio.embeddings\n          .create({\n            model: this.model,\n            input: chunk,\n            encoding_format: \"base64\",\n          })\n          .then((result) => {\n            const embedding = result.data?.[0]?.embedding;\n            if (!Array.isArray(embedding) || !embedding.length)\n              throw {\n                type: \"EMPTY_ARR\",\n                message: \"The embedding was empty from LMStudio\",\n              };\n            console.log(`Embedding length: ${embedding.length}`);\n            return { data: embedding, error: null };\n          })\n          .catch((e) => {\n            e.type =\n              e?.response?.data?.error?.code ||\n              e?.response?.status ||\n              \"failed_to_embed\";\n            e.message = e?.response?.data?.error?.message || e.message;\n            hasError = true;\n            return { data: [], error: e };\n          })\n      );\n    }\n\n    // Accumulate errors from embedding.\n    // If any are present throw an abort error.\n    const errors = results\n      .filter((res) => !!res.error)\n      .map((res) => res.error)\n      .flat();\n\n    if (errors.length > 0) {\n      let uniqueErrors = new Set();\n      console.log(errors);\n      errors.map((error) =>\n        uniqueErrors.add(`[${error.type}]: ${error.message}`)\n      );\n\n      if (errors.length > 0)\n        throw new Error(\n          `LMStudio Failed to embed: ${Array.from(uniqueErrors).join(\", \")}`\n        );\n    }\n\n    const data = results.map((res) => res?.data || []);\n    return data.length > 0 ? data : null;\n  }\n}\n\nmodule.exports = {\n  LMStudioEmbedder,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/localAi/index.js",
    "content": "const { toChunks, maximumChunkLength } = require(\"../../helpers\");\n\nclass LocalAiEmbedder {\n  constructor() {\n    if (!process.env.EMBEDDING_BASE_PATH)\n      throw new Error(\"No embedding base path was set.\");\n    if (!process.env.EMBEDDING_MODEL_PREF)\n      throw new Error(\"No embedding model was set.\");\n\n    this.className = \"LocalAiEmbedder\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.model = process.env.EMBEDDING_MODEL_PREF;\n    this.openai = new OpenAIApi({\n      baseURL: process.env.EMBEDDING_BASE_PATH,\n      apiKey: process.env.LOCAL_AI_API_KEY ?? null,\n    });\n\n    // Limit of how many strings we can process in a single pass to stay with resource or network limits\n    this.maxConcurrentChunks = 50;\n    this.embeddingMaxChunkLength = maximumChunkLength();\n\n    this.log(\n      `Initialized with ${this.model} - Max Size: ${this.embeddingMaxChunkLength}` +\n        (this.outputDimensions\n          ? ` - Output Dimensions: ${this.outputDimensions}`\n          : \" Assuming default output dimensions\")\n    );\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  get outputDimensions() {\n    if (\n      process.env.EMBEDDING_OUTPUT_DIMENSIONS &&\n      !isNaN(process.env.EMBEDDING_OUTPUT_DIMENSIONS) &&\n      process.env.EMBEDDING_OUTPUT_DIMENSIONS > 0\n    )\n      return parseInt(process.env.EMBEDDING_OUTPUT_DIMENSIONS);\n    return null;\n  }\n\n  async embedTextInput(textInput) {\n    const result = await this.embedChunks(\n      Array.isArray(textInput) ? textInput : [textInput]\n    );\n    return result?.[0] || [];\n  }\n\n  async embedChunks(textChunks = []) {\n    const embeddingRequests = [];\n    for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) {\n      embeddingRequests.push(\n        new Promise((resolve) => {\n          this.openai.embeddings\n            .create({\n              model: this.model,\n              input: chunk,\n              dimensions: this.outputDimensions,\n            })\n            .then((result) => {\n              resolve({ data: result?.data, error: null });\n            })\n            .catch((e) => {\n              e.type =\n                e?.response?.data?.error?.code ||\n                e?.response?.status ||\n                \"failed_to_embed\";\n              e.message = e?.response?.data?.error?.message || e.message;\n              resolve({ data: [], error: e });\n            });\n        })\n      );\n    }\n\n    const { data = [], error = null } = await Promise.all(\n      embeddingRequests\n    ).then((results) => {\n      // If any errors were returned from LocalAI abort the entire sequence because the embeddings\n      // will be incomplete.\n      const errors = results\n        .filter((res) => !!res.error)\n        .map((res) => res.error)\n        .flat();\n      if (errors.length > 0) {\n        let uniqueErrors = new Set();\n        errors.map((error) =>\n          uniqueErrors.add(`[${error.type}]: ${error.message}`)\n        );\n\n        return {\n          data: [],\n          error: Array.from(uniqueErrors).join(\", \"),\n        };\n      }\n      return {\n        data: results.map((res) => res?.data || []).flat(),\n        error: null,\n      };\n    });\n\n    if (!!error) throw new Error(`LocalAI Failed to embed: ${error}`);\n    return data.length > 0 &&\n      data.every((embd) => embd.hasOwnProperty(\"embedding\"))\n      ? data.map((embd) => embd.embedding)\n      : null;\n  }\n}\n\nmodule.exports = {\n  LocalAiEmbedder,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/mistral/index.js",
    "content": "class MistralEmbedder {\n  constructor() {\n    if (!process.env.MISTRAL_API_KEY)\n      throw new Error(\"No Mistral API key was set.\");\n\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.openai = new OpenAIApi({\n      baseURL: \"https://api.mistral.ai/v1\",\n      apiKey: process.env.MISTRAL_API_KEY ?? null,\n    });\n    this.model = process.env.EMBEDDING_MODEL_PREF || \"mistral-embed\";\n  }\n\n  async embedTextInput(textInput) {\n    try {\n      const response = await this.openai.embeddings.create({\n        model: this.model,\n        input: textInput,\n      });\n      return response?.data[0]?.embedding || [];\n    } catch (error) {\n      console.error(\"Failed to get embedding from Mistral.\", error.message);\n      return [];\n    }\n  }\n\n  async embedChunks(textChunks = []) {\n    try {\n      const response = await this.openai.embeddings.create({\n        model: this.model,\n        input: textChunks,\n      });\n      return response?.data?.map((emb) => emb.embedding) || [];\n    } catch (error) {\n      console.error(\"Failed to get embeddings from Mistral.\", error.message);\n      return new Array(textChunks.length).fill([]);\n    }\n  }\n}\n\nmodule.exports = {\n  MistralEmbedder,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/native/constants.js",
    "content": "const SUPPORTED_NATIVE_EMBEDDING_MODELS = {\n  \"Xenova/all-MiniLM-L6-v2\": {\n    maxConcurrentChunks: 25,\n    // Right now, this is NOT the token length, and is instead the number of characters\n    // that can be processed in a single pass. So we override to 1,000 characters.\n    // roughtly the max number of tokens assuming 2 characters per token. (undershooting)\n    // embeddingMaxChunkLength: 512, (from the model card)\n    embeddingMaxChunkLength: 1_000,\n    chunkPrefix: \"\",\n    queryPrefix: \"\",\n    apiInfo: {\n      id: \"Xenova/all-MiniLM-L6-v2\",\n      name: \"all-MiniLM-L6-v2\",\n      description:\n        \"A lightweight and fast model for embedding text. The default model for AnythingLLM.\",\n      lang: \"English\",\n      size: \"23MB\",\n      modelCard: \"https://huggingface.co/Xenova/all-MiniLM-L6-v2\",\n    },\n  },\n  \"Xenova/nomic-embed-text-v1\": {\n    maxConcurrentChunks: 5,\n    // Right now, this is NOT the token length, and is instead the number of characters\n    // that can be processed in a single pass. So we override to 16,000 characters.\n    // roughtly the max number of tokens assuming 2 characters per token. (undershooting)\n    // embeddingMaxChunkLength: 8192, (from the model card)\n    embeddingMaxChunkLength: 16_000,\n    chunkPrefix: \"search_document: \",\n    queryPrefix: \"search_query: \",\n    apiInfo: {\n      id: \"Xenova/nomic-embed-text-v1\",\n      name: \"nomic-embed-text-v1\",\n      description:\n        \"A high-performing open embedding model with a large token context window. Requires more processing power and memory.\",\n      lang: \"English\",\n      size: \"139MB\",\n      modelCard: \"https://huggingface.co/Xenova/nomic-embed-text-v1\",\n    },\n  },\n  \"MintplexLabs/multilingual-e5-small\": {\n    maxConcurrentChunks: 5,\n    // Right now, this is NOT the token length, and is instead the number of characters\n    // that can be processed in a single pass. So we override to 1,000 characters.\n    // roughtly the max number of tokens assuming 2 characters per token. (undershooting)\n    // embeddingMaxChunkLength: 512, (from the model card)\n    embeddingMaxChunkLength: 1_000,\n    chunkPrefix: \"passage: \",\n    queryPrefix: \"query: \",\n    apiInfo: {\n      id: \"MintplexLabs/multilingual-e5-small\",\n      name: \"multilingual-e5-small\",\n      description:\n        \"A larger multilingual embedding model that supports 100+ languages. Requires more processing power and memory.\",\n      lang: \"100+ languages\",\n      size: \"487MB\",\n      modelCard: \"https://huggingface.co/intfloat/multilingual-e5-small\",\n    },\n  },\n};\n\nmodule.exports = {\n  SUPPORTED_NATIVE_EMBEDDING_MODELS,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/native/index.js",
    "content": "const path = require(\"path\");\nconst fs = require(\"fs\");\nconst { toChunks } = require(\"../../helpers\");\nconst { v4 } = require(\"uuid\");\nconst { SUPPORTED_NATIVE_EMBEDDING_MODELS } = require(\"./constants\");\n\nclass NativeEmbedder {\n  static defaultModel = \"Xenova/all-MiniLM-L6-v2\";\n\n  /**\n   * Supported embedding models for native.\n   * @type {Record<string, {\n   *   chunkPrefix: string;\n   *   queryPrefix: string;\n   *   apiInfo: {\n   *     id: string;\n   *     name: string;\n   *     description: string;\n   *     lang: string;\n   *     size: string;\n   *     modelCard: string;\n   *   };\n   * }>}\n   */\n  static supportedModels = SUPPORTED_NATIVE_EMBEDDING_MODELS;\n\n  // This is a folder that Mintplex Labs hosts for those who cannot capture the HF model download\n  // endpoint for various reasons. This endpoint is not guaranteed to be active or maintained\n  // and may go offline at any time at Mintplex Labs's discretion.\n  #fallbackHost = \"https://cdn.anythingllm.com/support/models/\";\n\n  constructor() {\n    this.className = \"NativeEmbedder\";\n    this.model = this.getEmbeddingModel();\n    this.modelInfo = this.getEmbedderInfo();\n    this.cacheDir = path.resolve(\n      process.env.STORAGE_DIR\n        ? path.resolve(process.env.STORAGE_DIR, `models`)\n        : path.resolve(__dirname, `../../../storage/models`)\n    );\n    this.modelPath = path.resolve(this.cacheDir, ...this.model.split(\"/\"));\n    this.modelDownloaded = fs.existsSync(this.modelPath);\n\n    // Limit of how many strings we can process in a single pass to stay with resource or network limits\n    this.maxConcurrentChunks = this.modelInfo.maxConcurrentChunks;\n    this.embeddingMaxChunkLength = this.modelInfo.embeddingMaxChunkLength;\n\n    // Make directory when it does not exist in existing installations\n    if (!fs.existsSync(this.cacheDir)) fs.mkdirSync(this.cacheDir);\n    this.log(`Initialized ${this.model}`);\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Get the selected model from the environment variable.\n   * @returns {string}\n   */\n  static _getEmbeddingModel() {\n    const envModel =\n      process.env.EMBEDDING_MODEL_PREF ?? NativeEmbedder.defaultModel;\n    if (NativeEmbedder.supportedModels?.[envModel]) return envModel;\n    return NativeEmbedder.defaultModel;\n  }\n\n  get embeddingPrefix() {\n    return NativeEmbedder.supportedModels[this.model]?.chunkPrefix || \"\";\n  }\n\n  get queryPrefix() {\n    return NativeEmbedder.supportedModels[this.model]?.queryPrefix || \"\";\n  }\n\n  /**\n   * Get the available models in an API response format\n   * we can use to populate the frontend dropdown.\n   * @returns {{id: string, name: string, description: string, lang: string, size: string, modelCard: string}[]}\n   */\n  static availableModels() {\n    return Object.values(NativeEmbedder.supportedModels).map(\n      (model) => model.apiInfo\n    );\n  }\n\n  /**\n   * Get the embedding model to use.\n   * We only support a few models and will default to the default model if the environment variable is not set or not supported.\n   *\n   * Why only a few? Because we need to mirror them on the CDN so non-US users can download them.\n   * eg: \"Xenova/all-MiniLM-L6-v2\"\n   * eg: \"Xenova/nomic-embed-text-v1\"\n   * @returns {string}\n   */\n  getEmbeddingModel() {\n    const envModel =\n      process.env.EMBEDDING_MODEL_PREF ?? NativeEmbedder.defaultModel;\n    if (NativeEmbedder.supportedModels?.[envModel]) return envModel;\n    return NativeEmbedder.defaultModel;\n  }\n\n  /**\n   * Get the embedding model info.\n   *\n   * Will always fallback to the default model if the model is not supported.\n   * @returns {Object}\n   */\n  getEmbedderInfo() {\n    const model = this.getEmbeddingModel();\n    return NativeEmbedder.supportedModels[model];\n  }\n\n  #tempfilePath() {\n    const filename = `${v4()}.tmp`;\n    const tmpPath = process.env.STORAGE_DIR\n      ? path.resolve(process.env.STORAGE_DIR, \"tmp\")\n      : path.resolve(__dirname, `../../../storage/tmp`);\n    if (!fs.existsSync(tmpPath)) fs.mkdirSync(tmpPath, { recursive: true });\n    return path.resolve(tmpPath, filename);\n  }\n\n  async #writeToTempfile(filePath, data) {\n    try {\n      await fs.promises.appendFile(filePath, data, { encoding: \"utf8\" });\n    } catch (e) {\n      console.error(`Error writing to tempfile: ${e}`);\n    }\n  }\n\n  async #fetchWithHost(hostOverride = null) {\n    try {\n      // Convert ESM to CommonJS via import so we can load this library.\n      const pipeline = (...args) =>\n        import(\"@xenova/transformers\").then(({ pipeline, env }) => {\n          if (!this.modelDownloaded) {\n            // if model is not downloaded, we will log where we are fetching from.\n            if (hostOverride) {\n              env.remoteHost = hostOverride;\n              env.remotePathTemplate = \"{model}/\"; // Our S3 fallback url does not support revision File structure.\n            }\n            this.log(`Downloading ${this.model} from ${env.remoteHost}`);\n          }\n          return pipeline(...args);\n        });\n      return {\n        pipeline: await pipeline(\"feature-extraction\", this.model, {\n          cache_dir: this.cacheDir,\n          ...(!this.modelDownloaded\n            ? {\n                // Show download progress if we need to download any files\n                progress_callback: (data) => {\n                  if (!data.hasOwnProperty(\"progress\")) return;\n                  console.log(\n                    `\\x1b[36m[NativeEmbedder - Downloading model]\\x1b[0m ${\n                      data.file\n                    } ${~~data?.progress}%`\n                  );\n                },\n              }\n            : {}),\n        }),\n        retry: false,\n        error: null,\n      };\n    } catch (error) {\n      return {\n        pipeline: null,\n        retry: hostOverride === null ? this.#fallbackHost : false,\n        error,\n      };\n    }\n  }\n\n  // This function will do a single fallback attempt (not recursive on purpose) to try to grab the embedder model on first embed\n  // since at time, some clients cannot properly download the model from HF servers due to a number of reasons (IP, VPN, etc).\n  // Given this model is critical and nobody reads the GitHub issues before submitting the bug, we get the same bug\n  // report 20 times a day: https://github.com/Mintplex-Labs/anything-llm/issues/821\n  // So to attempt to monkey-patch this we have a single fallback URL to help alleviate duplicate bug reports.\n  async embedderClient() {\n    if (!this.modelDownloaded)\n      this.log(\n        \"The native embedding model has never been run and will be downloaded right now. Subsequent runs will be faster. (~23MB)\"\n      );\n\n    let fetchResponse = await this.#fetchWithHost();\n    if (fetchResponse.pipeline !== null) {\n      this.modelDownloaded = true;\n      return fetchResponse.pipeline;\n    }\n\n    this.log(\n      `Failed to download model from primary URL. Using fallback ${fetchResponse.retry}`\n    );\n    if (!!fetchResponse.retry)\n      fetchResponse = await this.#fetchWithHost(fetchResponse.retry);\n    if (fetchResponse.pipeline !== null) {\n      this.modelDownloaded = true;\n      return fetchResponse.pipeline;\n    }\n\n    throw fetchResponse.error;\n  }\n\n  /**\n   * Apply the query prefix to the text input if it is required by the model.\n   * eg: nomic-embed-text-v1 requires a query prefix for embedding/searching.\n   * @param {string|string[]} textInput - The text to embed.\n   * @returns {string|string[]} The text with the prefix applied.\n   */\n  #applyQueryPrefix(textInput) {\n    if (!this.queryPrefix) return textInput;\n    if (Array.isArray(textInput))\n      textInput = textInput.map((text) => `${this.queryPrefix}${text}`);\n    else textInput = `${this.queryPrefix}${textInput}`;\n    return textInput;\n  }\n\n  /**\n   * Embed a single text input.\n   * @param {string|string[]} textInput - The text to embed.\n   * @returns {Promise<Array<number>>} The embedded text.\n   */\n  async embedTextInput(textInput) {\n    textInput = this.#applyQueryPrefix(textInput);\n    const result = await this.embedChunks(\n      Array.isArray(textInput) ? textInput : [textInput]\n    );\n    return result?.[0] || [];\n  }\n\n  // If you are thinking you want to edit this function - you probably don't.\n  // This process was benchmarked heavily on a t3.small (2GB RAM 1vCPU)\n  // and without careful memory management for the V8 garbage collector\n  // this function will likely result in an OOM on any resource-constrained deployment.\n  // To help manage very large documents we run a concurrent write-log each iteration\n  // to keep the embedding result out of memory. The `maxConcurrentChunk` is set to 25,\n  // as 50 seems to overflow no matter what. Given the above, memory use hovers around ~30%\n  // during a very large document (>100K words) but can spike up to 70% before gc.\n  // This seems repeatable for all document sizes.\n  // While this does take a while, it is zero set up and is 100% free and on-instance.\n  // It still may crash depending on other elements at play - so no promises it works under all conditions.\n  async embedChunks(textChunks = []) {\n    const tmpFilePath = this.#tempfilePath();\n    const chunks = toChunks(textChunks, this.maxConcurrentChunks);\n    const chunkLen = chunks.length;\n\n    for (let [idx, chunk] of chunks.entries()) {\n      if (idx === 0) await this.#writeToTempfile(tmpFilePath, \"[\");\n      let data;\n      let pipeline = await this.embedderClient();\n      let output = await pipeline(chunk, {\n        pooling: \"mean\",\n        normalize: true,\n      });\n\n      if (output.length === 0) {\n        pipeline = null;\n        output = null;\n        data = null;\n        continue;\n      }\n\n      data = JSON.stringify(output.tolist());\n      await this.#writeToTempfile(tmpFilePath, data);\n      this.log(`Embedded Chunk Group ${idx + 1} of ${chunkLen}`);\n      if (chunkLen - 1 !== idx) await this.#writeToTempfile(tmpFilePath, \",\");\n      if (chunkLen - 1 === idx) await this.#writeToTempfile(tmpFilePath, \"]\");\n      pipeline = null;\n      output = null;\n      data = null;\n    }\n\n    const embeddingResults = JSON.parse(\n      fs.readFileSync(tmpFilePath, { encoding: \"utf-8\" })\n    );\n    fs.rmSync(tmpFilePath, { force: true });\n    return embeddingResults.length > 0 ? embeddingResults.flat() : null;\n  }\n}\n\nmodule.exports = {\n  NativeEmbedder,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/ollama/index.js",
    "content": "const { maximumChunkLength } = require(\"../../helpers\");\nconst { Ollama } = require(\"ollama\");\nconst { OllamaAILLM } = require(\"../../AiProviders/ollama\");\n\nclass OllamaEmbedder {\n  constructor() {\n    if (!process.env.EMBEDDING_BASE_PATH)\n      throw new Error(\"No embedding base path was set.\");\n    if (!process.env.EMBEDDING_MODEL_PREF)\n      throw new Error(\"No embedding model was set.\");\n\n    this.className = \"OllamaEmbedder\";\n    this.basePath = process.env.EMBEDDING_BASE_PATH;\n    this.model = process.env.EMBEDDING_MODEL_PREF;\n    this.maxConcurrentChunks = process.env.OLLAMA_EMBEDDING_BATCH_SIZE\n      ? Number(process.env.OLLAMA_EMBEDDING_BATCH_SIZE)\n      : 1;\n    this.embeddingMaxChunkLength = maximumChunkLength();\n    this.authToken = process.env.OLLAMA_AUTH_TOKEN;\n\n    const headers = this.authToken\n      ? { Authorization: `Bearer ${this.authToken}` }\n      : {};\n    this.client = new Ollama({\n      host: this.basePath,\n      headers,\n      fetch: OllamaAILLM.applyOllamaFetch(),\n    });\n    this.log(\n      `initialized with model ${this.model} at ${this.basePath}. Batch size: ${this.maxConcurrentChunks}, num_ctx: ${this.embeddingMaxChunkLength}`\n    );\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Checks if the Ollama service is alive by pinging the base path.\n   * @returns {Promise<boolean>} - A promise that resolves to true if the service is alive, false otherwise.\n   */\n  async #isAlive() {\n    return await fetch(this.basePath)\n      .then((res) => res.ok)\n      .catch((e) => {\n        this.log(e.message);\n        return false;\n      });\n  }\n\n  async embedTextInput(textInput) {\n    const result = await this.embedChunks(\n      Array.isArray(textInput) ? textInput : [textInput]\n    );\n    return result?.[0] || [];\n  }\n\n  /**\n   * This function takes an array of text chunks and embeds them using the Ollama API.\n   * Chunks are processed in batches based on the maxConcurrentChunks setting to balance\n   * resource usage on the Ollama endpoint.\n   *\n   * We will use the num_ctx option to set the maximum context window to the max chunk length defined by the user in the settings\n   * so that the maximum context window is used and content is not truncated.\n   *\n   * We also assume the default keep alive option. This could cause issues with models being unloaded and reloaded\n   * on low memory machines, but that is simply a user-end issue we cannot control. If the LLM and embedder are\n   * constantly being loaded and unloaded, the user should use another LLM or Embedder to avoid this issue.\n   * @param {string[]} textChunks - An array of text chunks to embed.\n   * @returns {Promise<Array<number[]>>} - A promise that resolves to an array of embeddings.\n   */\n  async embedChunks(textChunks = []) {\n    if (!(await this.#isAlive()))\n      throw new Error(\n        `Ollama service could not be reached. Is Ollama running?`\n      );\n    this.log(\n      `Embedding ${textChunks.length} chunks of text with ${this.model} in batches of ${this.maxConcurrentChunks}.`\n    );\n\n    let data = [];\n    let error = null;\n\n    // Process chunks in batches based on maxConcurrentChunks\n    const totalBatches = Math.ceil(\n      textChunks.length / this.maxConcurrentChunks\n    );\n    let currentBatch = 0;\n\n    for (let i = 0; i < textChunks.length; i += this.maxConcurrentChunks) {\n      const batch = textChunks.slice(i, i + this.maxConcurrentChunks);\n      currentBatch++;\n\n      try {\n        // Use input param instead of prompt param to support batch processing\n        const res = await this.client.embed({\n          model: this.model,\n          input: batch,\n          options: {\n            // Always set the num_ctx to the max chunk length defined by the user in the settings\n            // so that the maximum context window is used and content is not truncated.\n            num_ctx: this.embeddingMaxChunkLength,\n          },\n        });\n\n        const { embeddings } = res;\n        if (!Array.isArray(embeddings) || embeddings.length === 0)\n          throw new Error(\"Ollama returned empty embeddings for batch!\");\n\n        // Using prompt param in embed() would return a single embedding (number[])\n        // but input param returns an array of embeddings (number[][]) for batch processing.\n        // This is why we spread the embeddings array into the data array.\n        data.push(...embeddings);\n        this.log(\n          `Batch ${currentBatch}/${totalBatches}: Embedded ${embeddings.length} chunks. Total: ${data.length}/${textChunks.length}`\n        );\n      } catch (err) {\n        this.log(err.message);\n        error = err.message;\n        data = [];\n        break;\n      }\n    }\n\n    if (!!error) throw new Error(`Ollama Failed to embed: ${error}`);\n    return data.length > 0 ? data : null;\n  }\n}\n\nmodule.exports = {\n  OllamaEmbedder,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/openAi/index.js",
    "content": "const { toChunks } = require(\"../../helpers\");\n\nclass OpenAiEmbedder {\n  constructor() {\n    if (!process.env.OPEN_AI_KEY) throw new Error(\"No OpenAI API key was set.\");\n    this.className = \"OpenAiEmbedder\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.openai = new OpenAIApi({\n      apiKey: process.env.OPEN_AI_KEY,\n    });\n    this.model = process.env.EMBEDDING_MODEL_PREF || \"text-embedding-ada-002\";\n\n    // Limit of how many strings we can process in a single pass to stay with resource or network limits\n    this.maxConcurrentChunks = 500;\n\n    // https://platform.openai.com/docs/guides/embeddings/embedding-models\n    this.embeddingMaxChunkLength = 8_191;\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  async embedTextInput(textInput) {\n    const result = await this.embedChunks(\n      Array.isArray(textInput) ? textInput : [textInput]\n    );\n    return result?.[0] || [];\n  }\n\n  async embedChunks(textChunks = []) {\n    this.log(`Embedding ${textChunks.length} chunks...`);\n\n    // Because there is a hard POST limit on how many chunks can be sent at once to OpenAI (~8mb)\n    // we concurrently execute each max batch of text chunks possible.\n    // Refer to constructor maxConcurrentChunks for more info.\n    const embeddingRequests = [];\n    for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) {\n      embeddingRequests.push(\n        new Promise((resolve) => {\n          this.openai.embeddings\n            .create({\n              model: this.model,\n              input: chunk,\n            })\n            .then((result) => {\n              resolve({ data: result?.data, error: null });\n            })\n            .catch((e) => {\n              e.type =\n                e?.response?.data?.error?.code ||\n                e?.response?.status ||\n                \"failed_to_embed\";\n              e.message = e?.response?.data?.error?.message || e.message;\n              resolve({ data: [], error: e });\n            });\n        })\n      );\n    }\n\n    const { data = [], error = null } = await Promise.all(\n      embeddingRequests\n    ).then((results) => {\n      // If any errors were returned from OpenAI abort the entire sequence because the embeddings\n      // will be incomplete.\n      const errors = results\n        .filter((res) => !!res.error)\n        .map((res) => res.error)\n        .flat();\n      if (errors.length > 0) {\n        let uniqueErrors = new Set();\n        errors.map((error) =>\n          uniqueErrors.add(`[${error.type}]: ${error.message}`)\n        );\n\n        return {\n          data: [],\n          error: Array.from(uniqueErrors).join(\", \"),\n        };\n      }\n      return {\n        data: results.map((res) => res?.data || []).flat(),\n        error: null,\n      };\n    });\n\n    if (!!error) throw new Error(`OpenAI Failed to embed: ${error}`);\n    return data.length > 0 &&\n      data.every((embd) => embd.hasOwnProperty(\"embedding\"))\n      ? data.map((embd) => embd.embedding)\n      : null;\n  }\n}\n\nmodule.exports = {\n  OpenAiEmbedder,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/openRouter/index.js",
    "content": "const { toChunks } = require(\"../../helpers\");\n\nclass OpenRouterEmbedder {\n  constructor() {\n    if (!process.env.OPENROUTER_API_KEY)\n      throw new Error(\"No OpenRouter API key was set.\");\n    this.className = \"OpenRouterEmbedder\";\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.openai = new OpenAIApi({\n      baseURL: \"https://openrouter.ai/api/v1\",\n      apiKey: process.env.OPENROUTER_API_KEY,\n      defaultHeaders: {\n        \"HTTP-Referer\": \"https://anythingllm.com\",\n        \"X-Title\": \"AnythingLLM\",\n      },\n    });\n    this.model = process.env.EMBEDDING_MODEL_PREF || \"baai/bge-m3\";\n\n    // Limit of how many strings we can process in a single pass to stay with resource or network limits\n    this.maxConcurrentChunks = 500;\n\n    // https://openrouter.ai/docs/api/reference/embeddings\n    this.embeddingMaxChunkLength = 8_191;\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  async embedTextInput(textInput) {\n    const result = await this.embedChunks(\n      Array.isArray(textInput) ? textInput : [textInput]\n    );\n    return result?.[0] || [];\n  }\n\n  async embedChunks(textChunks = []) {\n    this.log(`Embedding ${textChunks.length} document chunks...`);\n    const embeddingRequests = [];\n    for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) {\n      embeddingRequests.push(\n        new Promise((resolve) => {\n          this.openai.embeddings\n            .create({\n              model: this.model,\n              input: chunk,\n            })\n            .then((result) => {\n              resolve({ data: result?.data, error: null });\n            })\n            .catch((e) => {\n              e.type =\n                e?.response?.data?.error?.code ||\n                e?.response?.status ||\n                \"failed_to_embed\";\n              e.message = e?.response?.data?.error?.message || e.message;\n              resolve({ data: [], error: e });\n            });\n        })\n      );\n    }\n\n    const { data = [], error = null } = await Promise.all(\n      embeddingRequests\n    ).then((results) => {\n      // If any errors were returned from OpenAI abort the entire sequence because the embeddings\n      // will be incomplete.\n      const errors = results\n        .filter((res) => !!res.error)\n        .map((res) => res.error)\n        .flat();\n      if (errors.length > 0) {\n        let uniqueErrors = new Set();\n        errors.map((error) =>\n          uniqueErrors.add(`[${error.type}]: ${error.message}`)\n        );\n\n        return {\n          data: [],\n          error: Array.from(uniqueErrors).join(\", \"),\n        };\n      }\n      return {\n        data: results.map((res) => res?.data || []).flat(),\n        error: null,\n      };\n    });\n\n    if (!!error) throw new Error(`OpenRouter Failed to embed: ${error}`);\n    return data.length > 0 &&\n      data.every((embd) => embd.hasOwnProperty(\"embedding\"))\n      ? data.map((embd) => embd.embedding)\n      : null;\n  }\n}\n\nasync function fetchOpenRouterEmbeddingModels() {\n  return await fetch(`https://openrouter.ai/api/v1/embeddings/models`, {\n    method: \"GET\",\n    headers: { \"Content-Type\": \"application/json\" },\n  })\n    .then((res) => res.json())\n    .then(({ data = [] }) => {\n      const models = {};\n      data.forEach((model) => {\n        models[model.id] = {\n          id: model.id,\n          name: model.name || model.id,\n          organization:\n            model.id.split(\"/\")[0].charAt(0).toUpperCase() +\n            model.id.split(\"/\")[0].slice(1),\n          maxLength: model.context_length,\n        };\n      });\n      return models;\n    })\n    .catch((e) => {\n      console.error(\"OpenRouter:fetchEmbeddingModels\", e.message);\n      return {};\n    });\n}\n\nmodule.exports = {\n  OpenRouterEmbedder,\n  fetchOpenRouterEmbeddingModels,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingEngines/voyageAi/index.js",
    "content": "class VoyageAiEmbedder {\n  constructor() {\n    if (!process.env.VOYAGEAI_API_KEY)\n      throw new Error(\"No Voyage AI API key was set.\");\n\n    const {\n      VoyageEmbeddings,\n    } = require(\"@langchain/community/embeddings/voyage\");\n\n    this.model = process.env.EMBEDDING_MODEL_PREF || \"voyage-3-lite\";\n    this.voyage = new VoyageEmbeddings({\n      apiKey: process.env.VOYAGEAI_API_KEY,\n      modelName: this.model,\n      // Voyage AI's limit per request is 128 https://docs.voyageai.com/docs/rate-limits#use-larger-batches\n      batchSize: 128,\n    });\n    this.embeddingMaxChunkLength = this.#getMaxEmbeddingLength();\n  }\n\n  // https://docs.voyageai.com/docs/embeddings\n  #getMaxEmbeddingLength() {\n    switch (this.model) {\n      case \"voyage-finance-2\":\n      case \"voyage-multilingual-2\":\n      case \"voyage-3\":\n      case \"voyage-3-lite\":\n      case \"voyage-3-large\":\n      case \"voyage-code-3\":\n        return 32_000;\n      case \"voyage-large-2-instruct\":\n      case \"voyage-law-2\":\n      case \"voyage-code-2\":\n      case \"voyage-large-2\":\n        return 16_000;\n      case \"voyage-2\":\n        return 4_000;\n      default:\n        return 4_000;\n    }\n  }\n\n  async embedTextInput(textInput) {\n    const result = await this.voyage.embedDocuments(\n      Array.isArray(textInput) ? textInput : [textInput]\n    );\n\n    // If given an array return the native Array[Array] format since that should be the outcome.\n    // But if given a single string, we need to flatten it so that we have a 1D array.\n    return (Array.isArray(textInput) ? result : result.flat()) || [];\n  }\n\n  async embedChunks(textChunks = []) {\n    try {\n      const embeddings = await this.voyage.embedDocuments(textChunks);\n      return embeddings;\n    } catch (error) {\n      console.error(\"Voyage AI Failed to embed:\", error);\n      if (\n        error.message.includes(\n          \"Cannot read properties of undefined (reading '0')\"\n        )\n      )\n        throw new Error(\"Voyage AI failed to embed: Rate limit reached\");\n      throw error;\n    }\n  }\n}\n\nmodule.exports = {\n  VoyageAiEmbedder,\n};\n"
  },
  {
    "path": "server/utils/EmbeddingRerankers/native/index.js",
    "content": "const path = require(\"path\");\nconst fs = require(\"fs\");\n\nclass NativeEmbeddingReranker {\n  static #model = null;\n  static #tokenizer = null;\n  static #transformers = null;\n  static #initializationPromise = null;\n\n  // This is a folder that Mintplex Labs hosts for those who cannot capture the HF model download\n  // endpoint for various reasons. This endpoint is not guaranteed to be active or maintained\n  // and may go offline at any time at Mintplex Labs's discretion.\n  #fallbackHost = \"https://cdn.anythingllm.com/support/models/\";\n\n  constructor() {\n    // An alternative model to the mixedbread-ai/mxbai-rerank-xsmall-v1 model (speed on CPU is much slower for this model @ 18docs = 6s)\n    // Model Card: https://huggingface.co/Xenova/ms-marco-MiniLM-L-6-v2 (speed on CPU is much faster @ 18docs = 1.6s)\n    this.model = \"Xenova/ms-marco-MiniLM-L-6-v2\";\n    this.cacheDir = path.resolve(\n      process.env.STORAGE_DIR\n        ? path.resolve(process.env.STORAGE_DIR, `models`)\n        : path.resolve(__dirname, `../../../storage/models`)\n    );\n    this.modelPath = path.resolve(this.cacheDir, ...this.model.split(\"/\"));\n    // Make directory when it does not exist in existing installations\n    if (!fs.existsSync(this.cacheDir)) fs.mkdirSync(this.cacheDir);\n\n    this.modelDownloaded = fs.existsSync(\n      path.resolve(this.cacheDir, this.model)\n    );\n    this.log(\"Initialized\");\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[NativeEmbeddingReranker]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * This function will return the host of the current reranker suite.\n   * If the reranker suite is not initialized, it will return the default HF host.\n   * @returns {string} The host of the current reranker suite.\n   */\n  get host() {\n    if (!NativeEmbeddingReranker.#transformers) return \"https://huggingface.co\";\n    try {\n      return new URL(NativeEmbeddingReranker.#transformers.env.remoteHost).host;\n    } catch {\n      return this.#fallbackHost;\n    }\n  }\n\n  /**\n   * This function will preload the reranker suite and tokenizer.\n   * This is useful for reducing the latency of the first rerank call and pre-downloading the models and such\n   * to avoid having to wait for the models to download on the first rerank call.\n   */\n  async preload() {\n    try {\n      this.log(`Preloading reranker suite...`);\n      await this.initClient();\n      this.log(\n        `Preloaded reranker suite. Reranking is available as a service now.`\n      );\n      return;\n    } catch (e) {\n      console.error(e);\n      this.log(\n        `Failed to preload reranker suite. Reranking will be available on the first rerank call.`\n      );\n      return;\n    }\n  }\n\n  async initClient() {\n    if (\n      NativeEmbeddingReranker.#transformers &&\n      NativeEmbeddingReranker.#model &&\n      NativeEmbeddingReranker.#tokenizer\n    ) {\n      this.log(`Reranker suite already fully initialized - reusing.`);\n      return;\n    }\n\n    if (NativeEmbeddingReranker.#initializationPromise) {\n      this.log(`Waiting for existing initialization to complete...`);\n      await NativeEmbeddingReranker.#initializationPromise;\n      return;\n    }\n\n    NativeEmbeddingReranker.#initializationPromise = (async () => {\n      try {\n        const { AutoModelForSequenceClassification, AutoTokenizer, env } =\n          await import(\"@xenova/transformers\");\n        this.log(`Loading reranker suite...`);\n        NativeEmbeddingReranker.#transformers = {\n          AutoModelForSequenceClassification,\n          AutoTokenizer,\n          env,\n        };\n        // Attempt to load the model and tokenizer in this order:\n        // 1. From local file system cache\n        // 2. Download and cache from remote host (hf.co)\n        // 3. Download and cache from fallback host (cdn.anythingllm.com)\n        await this.#getPreTrainedModel();\n        await this.#getPreTrainedTokenizer();\n      } finally {\n        NativeEmbeddingReranker.#initializationPromise = null;\n      }\n    })();\n\n    await NativeEmbeddingReranker.#initializationPromise;\n  }\n\n  /**\n   * This function will load the model from the local file system cache, or download and cache it from the remote host.\n   * If the model is not found in the local file system cache, it will download and cache it from the remote host.\n   * If the model is not found in the remote host, it will download and cache it from the fallback host.\n   * @returns {Promise<any>} The loaded model.\n   */\n  async #getPreTrainedModel() {\n    if (NativeEmbeddingReranker.#model) {\n      this.log(`Loading model from singleton...`);\n      return NativeEmbeddingReranker.#model;\n    }\n\n    try {\n      const model =\n        await NativeEmbeddingReranker.#transformers.AutoModelForSequenceClassification.from_pretrained(\n          this.model,\n          {\n            progress_callback: (p) => {\n              if (!this.modelDownloaded && p.status === \"progress\") {\n                this.log(\n                  `[${this.host}] Loading model ${this.model}... ${p?.progress}%`\n                );\n              }\n            },\n            cache_dir: this.cacheDir,\n          }\n        );\n      this.log(`Loaded model ${this.model}`);\n      NativeEmbeddingReranker.#model = model;\n      return model;\n    } catch (e) {\n      this.log(\n        `Failed to load model ${this.model} from ${this.host}.`,\n        e.message,\n        e.stack\n      );\n      if (\n        NativeEmbeddingReranker.#transformers.env.remoteHost ===\n        this.#fallbackHost\n      ) {\n        this.log(`Failed to load model ${this.model} from fallback host.`);\n        throw e;\n      }\n\n      this.log(`Falling back to fallback host. ${this.#fallbackHost}`);\n      NativeEmbeddingReranker.#transformers.env.remoteHost = this.#fallbackHost;\n      NativeEmbeddingReranker.#transformers.env.remotePathTemplate = \"{model}/\";\n      return await this.#getPreTrainedModel();\n    }\n  }\n\n  /**\n   * This function will load the tokenizer from the local file system cache, or download and cache it from the remote host.\n   * If the tokenizer is not found in the local file system cache, it will download and cache it from the remote host.\n   * If the tokenizer is not found in the remote host, it will download and cache it from the fallback host.\n   * @returns {Promise<any>} The loaded tokenizer.\n   */\n  async #getPreTrainedTokenizer() {\n    if (NativeEmbeddingReranker.#tokenizer) {\n      this.log(`Loading tokenizer from singleton...`);\n      return NativeEmbeddingReranker.#tokenizer;\n    }\n\n    try {\n      const tokenizer =\n        await NativeEmbeddingReranker.#transformers.AutoTokenizer.from_pretrained(\n          this.model,\n          {\n            progress_callback: (p) => {\n              if (!this.modelDownloaded && p.status === \"progress\") {\n                this.log(\n                  `[${this.host}] Loading tokenizer ${this.model}... ${p?.progress}%`\n                );\n              }\n            },\n            cache_dir: this.cacheDir,\n          }\n        );\n      this.log(`Loaded tokenizer ${this.model}`);\n      NativeEmbeddingReranker.#tokenizer = tokenizer;\n      return tokenizer;\n    } catch (e) {\n      this.log(\n        `Failed to load tokenizer ${this.model} from ${this.host}.`,\n        e.message,\n        e.stack\n      );\n      if (\n        NativeEmbeddingReranker.#transformers.env.remoteHost ===\n        this.#fallbackHost\n      ) {\n        this.log(`Failed to load tokenizer ${this.model} from fallback host.`);\n        throw e;\n      }\n\n      this.log(`Falling back to fallback host. ${this.#fallbackHost}`);\n      NativeEmbeddingReranker.#transformers.env.remoteHost = this.#fallbackHost;\n      NativeEmbeddingReranker.#transformers.env.remotePathTemplate = \"{model}/\";\n      return await this.#getPreTrainedTokenizer();\n    }\n  }\n\n  /**\n   * Reranks a list of documents based on the query.\n   * @param {string} query - The query to rerank the documents against.\n   * @param {{text: string}[]} documents - The list of document text snippets to rerank. Should be output from a vector search.\n   * @param {Object} options - The options for the reranking.\n   * @param {number} options.topK - The number of top documents to return.\n   * @returns {Promise<any[]>} - The reranked list of documents.\n   */\n  async rerank(query, documents, options = { topK: 4 }) {\n    await this.initClient();\n    const model = NativeEmbeddingReranker.#model;\n    const tokenizer = NativeEmbeddingReranker.#tokenizer;\n\n    const start = Date.now();\n    this.log(`Reranking ${documents.length} documents...`);\n    const inputs = tokenizer(new Array(documents.length).fill(query), {\n      text_pair: documents.map((doc) => doc.text),\n      padding: true,\n      truncation: true,\n    });\n    const { logits } = await model(inputs);\n    const reranked = logits\n      .sigmoid()\n      .tolist()\n      .map(([score], i) => ({\n        rerank_corpus_id: i,\n        rerank_score: score,\n        ...documents[i],\n      }))\n      .sort((a, b) => b.rerank_score - a.rerank_score)\n      .slice(0, options.topK);\n\n    this.log(\n      `Reranking ${documents.length} documents to top ${options.topK} took ${Date.now() - start}ms`\n    );\n    return reranked;\n  }\n}\n\nmodule.exports = {\n  NativeEmbeddingReranker,\n};\n"
  },
  {
    "path": "server/utils/EncryptionManager/index.js",
    "content": "const crypto = require(\"crypto\");\nconst { dumpENV } = require(\"../helpers/updateENV\");\n\n// Class that is used to arbitrarily encrypt/decrypt string data via a persistent passphrase/salt that\n// is either user defined or is created and saved to the ENV on creation.\nclass EncryptionManager {\n  #keyENV = \"SIG_KEY\";\n  #saltENV = \"SIG_SALT\";\n  #encryptionKey;\n  #encryptionSalt;\n\n  constructor({ key = null, salt = null } = {}) {\n    this.#loadOrCreateKeySalt(key, salt);\n    this.key = crypto.scryptSync(this.#encryptionKey, this.#encryptionSalt, 32);\n    this.algorithm = \"aes-256-cbc\";\n    this.separator = \":\";\n\n    // Used to send key to collector process to be able to decrypt data since they do not share ENVs\n    // this value should use the CommunicationKey.encrypt process before sending anywhere outside the\n    // server process so it is never sent in its raw format.\n    this.xPayload = this.key.toString(\"base64\");\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[EncryptionManager]\\x1b[0m ${text}`, ...args);\n  }\n\n  #loadOrCreateKeySalt(_key = null, _salt = null) {\n    if (!!_key && !!_salt) {\n      this.log(\n        \"Pre-assigned key & salt for encrypting arbitrary data was used.\"\n      );\n      this.#encryptionKey = _key;\n      this.#encryptionSalt = _salt;\n      return;\n    }\n\n    if (!process.env[this.#keyENV] || !process.env[this.#saltENV]) {\n      this.log(\"Self-assigning key & salt for encrypting arbitrary data.\");\n      process.env[this.#keyENV] = crypto.randomBytes(32).toString(\"hex\");\n      process.env[this.#saltENV] = crypto.randomBytes(32).toString(\"hex\");\n      if (process.env.NODE_ENV === \"production\") dumpENV();\n    } else\n      this.log(\"Loaded existing key & salt for encrypting arbitrary data.\");\n\n    this.#encryptionKey = process.env[this.#keyENV];\n    this.#encryptionSalt = process.env[this.#saltENV];\n    return;\n  }\n\n  encrypt(plainTextString = null) {\n    try {\n      if (!plainTextString)\n        throw new Error(\"Empty string is not valid for this method.\");\n      const iv = crypto.randomBytes(16);\n      const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);\n      const encrypted = cipher.update(plainTextString, \"utf8\", \"hex\");\n      return [\n        encrypted + cipher.final(\"hex\"),\n        Buffer.from(iv).toString(\"hex\"),\n      ].join(this.separator);\n    } catch (e) {\n      this.log(e);\n      return null;\n    }\n  }\n\n  decrypt(encryptedString) {\n    try {\n      const [encrypted, iv] = encryptedString.split(this.separator);\n      if (!iv) throw new Error(\"IV not found\");\n      const decipher = crypto.createDecipheriv(\n        this.algorithm,\n        this.key,\n        Buffer.from(iv, \"hex\")\n      );\n      return decipher.update(encrypted, \"hex\", \"utf8\") + decipher.final(\"utf8\");\n    } catch (e) {\n      this.log(e);\n      return null;\n    }\n  }\n}\n\nmodule.exports = { EncryptionManager };\n"
  },
  {
    "path": "server/utils/MCP/hypervisor/index.js",
    "content": "const { safeJsonParse } = require(\"../../http\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { Client } = require(\"@modelcontextprotocol/sdk/client/index.js\");\nconst {\n  StdioClientTransport,\n} = require(\"@modelcontextprotocol/sdk/client/stdio.js\");\nconst {\n  SSEClientTransport,\n} = require(\"@modelcontextprotocol/sdk/client/sse.js\");\nconst {\n  StreamableHTTPClientTransport,\n} = require(\"@modelcontextprotocol/sdk/client/streamableHttp.js\");\nconst { patchShellEnvironmentPath } = require(\"../../helpers/shell\");\n\n/**\n * @typedef {'stdio' | 'http' | 'sse'} MCPServerTypes\n */\n\n/**\n * @class MCPHypervisor\n * @description A class that manages MCP servers found in the storage/plugins/anythingllm_mcp_servers.json file.\n * This class is responsible for booting, stopping, and reloading MCP servers - it is the user responsibility for the MCP server definitions\n * to me correct and also functioning tools depending on their deployment (docker vs local) as well as the security of said tools\n * since MCP is basically arbitrary code execution.\n *\n * @notice This class is a singleton.\n * @notice Each MCP tool has dependencies specific to it and this call WILL NOT check for them.\n * For example, if the tools requires `npx` then the context in which AnythingLLM mains process is running will need to access npx.\n * This is typically not common in our pre-built image so may not function. But this is the case anywhere MCP is used.\n *\n * AnythingLLM will take care of porting MCP servers to agent-callable functions via @agent directive.\n * @see MCPCompatibilityLayer.convertServerToolsToPlugins\n */\nclass MCPHypervisor {\n  static _instance;\n  /**\n   * The path to the JSON file containing the MCP server definitions.\n   * @type {string}\n   */\n  mcpServerJSONPath;\n\n  /**\n   * The MCP servers currently running.\n   * @type { { [key: string]: Client & {transport: {_process: import('child_process').ChildProcess}, aibitatToolIds: string[]} } }\n   */\n  mcps = {};\n  /**\n   * The results of the MCP server loading process.\n   * @type { { [key: string]: {status: 'success' | 'failed', message: string} } }\n   */\n  mcpLoadingResults = {};\n\n  constructor() {\n    if (MCPHypervisor._instance) return MCPHypervisor._instance;\n    MCPHypervisor._instance = this;\n    this.className = \"MCPHypervisor\";\n    this.log(\"Initializing MCP Hypervisor - subsequent calls will boot faster\");\n    this.#setupConfigFile();\n    return this;\n  }\n\n  /**\n   * Setup the MCP server definitions file.\n   * Will create the file/directory if it doesn't exist already in storage/plugins with blank options\n   */\n  #setupConfigFile() {\n    this.mcpServerJSONPath =\n      process.env.NODE_ENV === \"development\"\n        ? path.resolve(\n            __dirname,\n            `../../../storage/plugins/anythingllm_mcp_servers.json`\n          )\n        : path.resolve(\n            process.env.STORAGE_DIR ??\n              path.resolve(__dirname, `../../../storage`),\n            `plugins/anythingllm_mcp_servers.json`\n          );\n\n    if (!fs.existsSync(this.mcpServerJSONPath)) {\n      fs.mkdirSync(path.dirname(this.mcpServerJSONPath), { recursive: true });\n      fs.writeFileSync(\n        this.mcpServerJSONPath,\n        JSON.stringify({ mcpServers: {} }, null, 2),\n        { encoding: \"utf8\" }\n      );\n    }\n\n    this.log(`MCP Config File: ${this.mcpServerJSONPath}`);\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[${this.className}]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Get the MCP servers from the JSON file.\n   * @returns { { name: string, server: { command: string, args: string[], env: { [key: string]: string } } }[] } The MCP servers.\n   */\n  get mcpServerConfigs() {\n    const servers = safeJsonParse(\n      fs.readFileSync(this.mcpServerJSONPath, \"utf8\"),\n      { mcpServers: {} }\n    );\n    return Object.entries(servers.mcpServers).map(([name, server]) => ({\n      name,\n      server,\n    }));\n  }\n\n  /**\n   * Remove the MCP server from the config file\n   * @param {string} name - The name of the MCP server to remove\n   * @returns {boolean} - True if the MCP server was removed, false otherwise\n   */\n  removeMCPServerFromConfig(name) {\n    const servers = safeJsonParse(\n      fs.readFileSync(this.mcpServerJSONPath, \"utf8\"),\n      { mcpServers: {} }\n    );\n    if (!servers.mcpServers[name]) return false;\n\n    delete servers.mcpServers[name];\n    fs.writeFileSync(\n      this.mcpServerJSONPath,\n      JSON.stringify(servers, null, 2),\n      \"utf8\"\n    );\n    this.log(`MCP server ${name} removed from config file`);\n    return true;\n  }\n\n  /**\n   * Update the suppressed tools for an MCP server\n   * @param {string} serverName - The name of the MCP server\n   * @param {string} toolName - The name of the tool to toggle\n   * @param {boolean} enabled - Whether the tool should be enabled (true) or suppressed (false)\n   * @returns {{success: boolean, error: string | null, suppressedTools: string[]}}\n   */\n  updateSuppressedTools(serverName, toolName, enabled) {\n    const servers = safeJsonParse(\n      fs.readFileSync(this.mcpServerJSONPath, \"utf8\"),\n      { mcpServers: {} }\n    );\n\n    if (!servers.mcpServers[serverName]) {\n      return {\n        success: false,\n        error: `MCP server ${serverName} not found in config file.`,\n        suppressedTools: [],\n      };\n    }\n\n    const server = servers.mcpServers[serverName];\n    if (!server.anythingllm) server.anythingllm = {};\n    if (!Array.isArray(server.anythingllm.suppressedTools))\n      server.anythingllm.suppressedTools = [];\n\n    const suppressedTools = server.anythingllm.suppressedTools;\n\n    if (enabled) {\n      const index = suppressedTools.indexOf(toolName);\n      if (index > -1) suppressedTools.splice(index, 1);\n    } else {\n      if (!suppressedTools.includes(toolName)) suppressedTools.push(toolName);\n    }\n\n    server.anythingllm.suppressedTools = suppressedTools;\n    servers.mcpServers[serverName] = server;\n\n    fs.writeFileSync(\n      this.mcpServerJSONPath,\n      JSON.stringify(servers, null, 2),\n      \"utf8\"\n    );\n\n    this.log(\n      `MCP server ${serverName} tool ${toolName} ${enabled ? \"enabled\" : \"suppressed\"}`\n    );\n    return { success: true, error: null, suppressedTools };\n  }\n\n  /**\n   * Get the suppressed tools for an MCP server\n   * @param {string} serverName - The name of the MCP server\n   * @returns {string[]} - Array of suppressed tool names\n   */\n  getSuppressedTools(serverName) {\n    const config = this.mcpServerConfigs.find((s) => s.name === serverName);\n    return config?.server?.anythingllm?.suppressedTools || [];\n  }\n\n  /**\n   * Reload the MCP servers - can be used to reload the MCP servers without restarting the server or app\n   * and will also apply changes to the config file if any where made.\n   */\n  async reloadMCPServers() {\n    this.pruneMCPServers();\n    await this.bootMCPServers();\n  }\n\n  /**\n   * Start a single MCP server by its server name - public method\n   * @param {string} name - The name of the MCP server to start\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  async startMCPServer(name) {\n    if (this.mcps[name])\n      return { success: false, error: `MCP server ${name} already running` };\n    const config = this.mcpServerConfigs.find((s) => s.name === name);\n    if (!config)\n      return {\n        success: false,\n        error: `MCP server ${name} not found in config file`,\n      };\n\n    try {\n      await this.#startMCPServer(config);\n      this.mcpLoadingResults[name] = {\n        status: \"success\",\n        message: `Successfully connected to MCP server: ${name}`,\n      };\n\n      return { success: true, message: `MCP server ${name} started` };\n    } catch (e) {\n      this.log(`Failed to start single MCP server: ${name}`, {\n        error: e.message,\n        code: e.code,\n        syscall: e.syscall,\n        path: e.path,\n        stack: e.stack,\n      });\n      this.mcpLoadingResults[name] = {\n        status: \"failed\",\n        message: `Failed to start MCP server: ${name} [${e.code || \"NO_CODE\"}] ${e.message}`,\n      };\n\n      // Clean up failed connection\n      if (this.mcps[name]) {\n        this.mcps[name].close();\n        delete this.mcps[name];\n      }\n\n      return { success: false, error: e.message };\n    }\n  }\n  /**\n   * Prune a single MCP server by its server name\n   * @param {string} name - The name of the MCP server to prune\n   * @returns {boolean} - True if the MCP server was pruned, false otherwise\n   */\n  pruneMCPServer(name) {\n    if (!name || !this.mcps[name]) return true;\n\n    this.log(`Pruning MCP server: ${name}`);\n    const mcp = this.mcps[name];\n    if (!mcp.transport) return true;\n    const childProcess = mcp.transport._process;\n    if (childProcess) childProcess.kill(\"SIGTERM\");\n    mcp.transport.close();\n\n    delete this.mcps[name];\n    this.mcpLoadingResults[name] = {\n      status: \"failed\",\n      message: `Server was stopped manually by the administrator.`,\n    };\n    return true;\n  }\n\n  /**\n   * Prune the MCP servers - pkills and forgets all MCP servers\n   * @returns {void}\n   */\n  pruneMCPServers() {\n    this.log(`Pruning ${Object.keys(this.mcps).length} MCP servers...`);\n\n    for (const name of Object.keys(this.mcps)) {\n      if (!this.mcps[name]) continue;\n      const mcp = this.mcps[name];\n      if (!mcp.transport) continue;\n      const childProcess = mcp.transport._process;\n      if (childProcess)\n        this.log(`Killing MCP ${name} (PID: ${childProcess.pid})`, {\n          killed: childProcess.kill(\"SIGTERM\"),\n        });\n\n      mcp.transport.close();\n      mcp.close();\n    }\n    this.mcps = {};\n    this.mcpLoadingResults = {};\n  }\n\n  /**\n   * Build the MCP server environment variables - ensures proper PATH and NODE_PATH\n   * inheritance across all platforms and deployment scenarios.\n   * @param {Object} server - The server definition\n   * @returns {Promise<{env: { [key: string]: string } | {}}}> - The environment variables\n   */\n  async #buildMCPServerENV(server) {\n    const shellEnv = await patchShellEnvironmentPath();\n    let baseEnv = {\n      PATH:\n        shellEnv.PATH ||\n        process.env.PATH ||\n        \"/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\",\n      NODE_PATH:\n        shellEnv.NODE_PATH ||\n        process.env.NODE_PATH ||\n        \"/usr/local/lib/node_modules\",\n      ...shellEnv, // Include all shell environment variables\n    };\n\n    // Docker-specific environment setup\n    if (process.env.ANYTHING_LLM_RUNTIME === \"docker\") {\n      baseEnv = {\n        // Fixed: NODE_PATH should point to modules directory, not node binary\n        NODE_PATH: \"/usr/local/lib/node_modules\",\n        PATH: \"/usr/local/bin:/usr/bin:/bin\",\n        ...baseEnv, // Allow inheritance to override docker defaults if needed\n      };\n    }\n\n    // No custom environment specified - return base environment\n    if (!server?.env || Object.keys(server.env).length === 0) {\n      return { env: baseEnv };\n    }\n\n    // Merge user-specified environment with base environment\n    // User environment takes precedence over defaults\n    return {\n      env: {\n        ...baseEnv,\n        ...server.env,\n      },\n    };\n  }\n\n  /**\n   * Parse the server type from the server definition\n   * @param {Object} server - The server definition\n   * @returns {MCPServerTypes | null} - The server type\n   */\n  #parseServerType(server) {\n    if (\n      server.type === \"sse\" ||\n      server.type === \"streamable\" ||\n      server.type === \"http\"\n    )\n      return \"http\";\n    if (Object.prototype.hasOwnProperty.call(server, \"command\")) return \"stdio\";\n    if (Object.prototype.hasOwnProperty.call(server, \"url\")) return \"http\";\n    return \"sse\";\n  }\n\n  /**\n   * Validate the server definition by type\n   * - Will throw an error if the server definition is invalid\n   * @param {string} name - The name of the MCP server\n   * @param {Object} server - The server definition\n   * @param {MCPServerTypes} type - The server type\n   * @returns {void}\n   */\n  #validateServerDefinitionByType(name, server, type) {\n    if (\n      server.type === \"sse\" ||\n      server.type === \"streamable\" ||\n      server.type === \"http\"\n    ) {\n      if (!server.url) {\n        throw new Error(\n          `MCP server \"${name}\": missing required \"url\" for ${server.type} transport`\n        );\n      }\n\n      try {\n        new URL(server.url);\n      } catch {\n        throw new Error(`MCP server \"${name}\": invalid URL \"${server.url}\"`);\n      }\n      return;\n    }\n\n    if (type === \"stdio\") {\n      if (\n        Object.prototype.hasOwnProperty.call(server, \"args\") &&\n        !Array.isArray(server.args)\n      )\n        throw new Error(\"MCP server args must be an array\");\n    }\n\n    if (type === \"http\") {\n      if (![\"sse\", \"streamable\"].includes(server?.type))\n        throw new Error(\"MCP server type must have sse or streamable value.\");\n    }\n    if (type === \"sse\") return;\n    return;\n  }\n\n  /**\n   * Setup the server transport by type and server definition\n   * @param {Object} server - The server definition\n   * @param {MCPServerTypes} type - The server type\n   * @returns {Promise<StdioClientTransport | StreamableHTTPClientTransport | SSEClientTransport>} - The server transport\n   */\n  async #setupServerTransport(server, type) {\n    // if not stdio then it is http or sse\n    if (type !== \"stdio\") return this.createHttpTransport(server);\n\n    return new StdioClientTransport({\n      command: server.command,\n      args: server?.args ?? [],\n      ...(await this.#buildMCPServerENV(server)),\n    });\n  }\n\n  /**\n   * Create MCP client transport for http MCP server.\n   * @param {Object} server - The server definition\n   * @returns {StreamableHTTPClientTransport | SSEClientTransport} - The server transport\n   */\n  createHttpTransport(server) {\n    const url = new URL(server.url);\n\n    // If the server block has a type property then use that to determine the transport type\n    switch (server.type) {\n      case \"streamable\":\n      case \"http\":\n        return new StreamableHTTPClientTransport(url, {\n          requestInit: {\n            headers: server.headers,\n          },\n        });\n      default:\n        return new SSEClientTransport(url, {\n          requestInit: {\n            headers: server.headers,\n          },\n        });\n    }\n  }\n\n  /**\n   * @private Start a single MCP server by its server definition from the JSON file\n   * @param {string} name - The name of the MCP server to start\n   * @param {Object} server - The server definition\n   * @returns {Promise<boolean>}\n   */\n  async #startMCPServer({ name, server }) {\n    if (!name) throw new Error(\"MCP server name is required\");\n    if (!server) throw new Error(\"MCP server definition is required\");\n    const serverType = this.#parseServerType(server);\n    if (!serverType) throw new Error(\"MCP server command or url is required\");\n\n    this.#validateServerDefinitionByType(name, server, serverType);\n    this.log(`Attempting to start MCP server: ${name}`);\n    const mcp = new Client({ name: name, version: \"1.0.0\" });\n    const transport = await this.#setupServerTransport(server, serverType);\n\n    // Add connection event listeners\n    transport.onclose = () => this.log(`${name} - Transport closed`);\n    transport.onerror = (error) =>\n      this.log(`${name} - Transport error:`, error);\n    transport.onmessage = (message) =>\n      this.log(`${name} - Transport message:`, message);\n\n    // Connect and await the connection with a timeout\n    this.mcps[name] = mcp;\n    const connectionPromise = mcp.connect(transport);\n\n    let timeoutId;\n    const timeoutPromise = new Promise((_, reject) => {\n      timeoutId = setTimeout(\n        () => reject(new Error(\"Connection timeout\")),\n        30_000\n      ); // 30 second timeout\n    });\n\n    try {\n      await Promise.race([connectionPromise, timeoutPromise]);\n      if (timeoutId) clearTimeout(timeoutId);\n    } catch (error) {\n      if (timeoutId) clearTimeout(timeoutId);\n      throw error;\n    }\n    return true;\n  }\n\n  /**\n   * Boot the MCP servers according to the server definitions.\n   * This function will skip booting MCP servers if they are already running.\n   * @returns { Promise<{ [key: string]: {status: string, message: string} }> } The results of the boot process.\n   */\n  async bootMCPServers() {\n    if (Object.keys(this.mcps).length > 0) {\n      this.log(\"MCP Servers already running, skipping boot.\");\n      return this.mcpLoadingResults;\n    }\n\n    const serverDefinitions = this.mcpServerConfigs;\n    for (const { name, server } of serverDefinitions) {\n      if (\n        server.anythingllm?.hasOwnProperty(\"autoStart\") &&\n        server.anythingllm.autoStart === false\n      ) {\n        this.log(\n          `MCP server ${name} has anythingllm.autoStart property set to false, skipping boot!`\n        );\n        this.mcpLoadingResults[name] = {\n          status: \"failed\",\n          message: `MCP server ${name} has anythingllm.autoStart property set to false, boot skipped!`,\n        };\n        continue;\n      }\n\n      try {\n        await this.#startMCPServer({ name, server });\n        // Verify the connection is alive?\n        // if (!(await mcp.ping())) throw new Error('Connection failed to establish');\n        this.mcpLoadingResults[name] = {\n          status: \"success\",\n          message: `Successfully connected to MCP server: ${name}`,\n        };\n      } catch (e) {\n        this.log(`Failed to start MCP server: ${name}`, {\n          error: e.message,\n          code: e.code,\n          syscall: e.syscall,\n          path: e.path,\n          stack: e.stack, // Adding stack trace for better debugging\n        });\n        this.mcpLoadingResults[name] = {\n          status: \"failed\",\n          message: `Failed to start MCP server: ${name} [${e.code || \"NO_CODE\"}] ${e.message}`,\n        };\n\n        // Clean up failed connection\n        if (this.mcps[name]) {\n          this.mcps[name].close();\n          delete this.mcps[name];\n        }\n      }\n    }\n\n    const runningServers = Object.keys(this.mcps);\n    this.log(\n      `Successfully started ${runningServers.length} MCP servers:`,\n      runningServers\n    );\n    return this.mcpLoadingResults;\n  }\n}\n\nmodule.exports = MCPHypervisor;\n"
  },
  {
    "path": "server/utils/MCP/index.js",
    "content": "const MCPHypervisor = require(\"./hypervisor\");\n\nclass MCPCompatibilityLayer extends MCPHypervisor {\n  static _instance;\n\n  constructor() {\n    super();\n    if (MCPCompatibilityLayer._instance) return MCPCompatibilityLayer._instance;\n    MCPCompatibilityLayer._instance = this;\n  }\n\n  /**\n   * Get all of the active MCP servers as plugins we can load into agents.\n   * This will also boot all MCP servers if they have not been started yet.\n   * @returns {Promise<string[]>} Array of flow names in @@mcp_{name} format\n   */\n  async activeMCPServers() {\n    await this.bootMCPServers();\n    return Object.keys(this.mcps).flatMap((name) => `@@mcp_${name}`);\n  }\n\n  /**\n   * Convert an MCP server name to an AnythingLLM Agent plugin\n   * @param {string} name - The base name of the MCP server to convert - not the tool name. eg: `docker-mcp` not `docker-mcp:list-containers`\n   * @param {Object} aibitat - The aibitat object to pass to the plugin\n   * @returns {Promise<{name: string, description: string, plugin: Function}[]|null>} Array of plugin configurations or null if not found\n   */\n  async convertServerToolsToPlugins(name, _aibitat = null) {\n    const mcp = this.mcps[name];\n    if (!mcp) return null;\n\n    let tools;\n    try {\n      const response = await mcp.listTools();\n      tools = response.tools;\n    } catch (error) {\n      this.log(`Failed to list tools for MCP server ${name}:`, error);\n      return null;\n    }\n    if (!tools || !tools.length) return null;\n\n    const suppressedTools = this.getSuppressedTools(name);\n    const totalTools = tools.length;\n    tools = tools.filter((tool) => !suppressedTools.includes(tool.name));\n    const suppressedCount = totalTools - tools.length;\n\n    if (suppressedCount > 0) {\n      this.log(\n        `MCP server ${name}: ${suppressedCount} tool(s) suppressed, ${tools.length} tool(s) enabled`\n      );\n    }\n\n    if (!tools.length) {\n      this.log(`MCP server ${name}: All tools are suppressed, skipping`);\n      return null;\n    }\n\n    const plugins = [];\n    for (const tool of tools) {\n      plugins.push({\n        name: `${name}-${tool.name}`,\n        description: tool.description,\n        plugin: function () {\n          return {\n            name: `${name}-${tool.name}`,\n            setup: (aibitat) => {\n              aibitat.function({\n                super: aibitat,\n                name: `${name}-${tool.name}`,\n                controller: new AbortController(),\n                description: tool.description,\n                isMCPTool: true,\n                examples: [],\n                parameters: {\n                  $schema: \"http://json-schema.org/draft-07/schema#\",\n                  ...tool.inputSchema,\n                },\n                handler: async function (args = {}) {\n                  try {\n                    const mcpLayer = new MCPCompatibilityLayer();\n                    const currentMcp = mcpLayer.mcps[name];\n                    if (!currentMcp)\n                      throw new Error(\n                        `MCP server ${name} is not currently running`\n                      );\n\n                    aibitat.handlerProps.log(\n                      `Executing MCP server: ${name}:${tool.name} with args:`,\n                      args\n                    );\n                    aibitat.introspect(\n                      `Executing MCP server: ${name} with ${JSON.stringify(args, null, 2)}`\n                    );\n                    const result = await currentMcp.callTool({\n                      name: tool.name,\n                      arguments: args,\n                    });\n                    aibitat.handlerProps.log(\n                      `MCP server: ${name}:${tool.name} completed successfully`,\n                      result\n                    );\n                    aibitat.introspect(\n                      `MCP server: ${name}:${tool.name} completed successfully`\n                    );\n                    return MCPCompatibilityLayer.returnMCPResult(result);\n                  } catch (error) {\n                    aibitat.handlerProps.log(\n                      `MCP server: ${name}:${tool.name} failed with error:`,\n                      error\n                    );\n                    aibitat.introspect(\n                      `MCP server: ${name}:${tool.name} failed with error:`,\n                      error\n                    );\n                    return `The tool ${name}:${tool.name} failed with error: ${error?.message || \"An unknown error occurred\"}`;\n                  }\n                },\n              });\n            },\n          };\n        },\n        toolName: `${name}:${tool.name}`,\n      });\n    }\n\n    return plugins;\n  }\n\n  /**\n   * Returns the MCP servers that were loaded or attempted to be loaded\n   * so that we can display them in the frontend for review or error logging.\n   * @returns {Promise<{\n   *   name: string,\n   *   running: boolean,\n   *   tools: {name: string, description: string, inputSchema: Object}[],\n   *   process: {pid: number, cmd: string}|null,\n   *   error: string|null\n   * }[]>} - The active MCP servers\n   */\n  async servers() {\n    await this.bootMCPServers();\n    const servers = [];\n    for (const [name, result] of Object.entries(this.mcpLoadingResults)) {\n      const config = this.mcpServerConfigs.find((s) => s.name === name);\n\n      if (result.status === \"failed\") {\n        servers.push({\n          name,\n          config: config?.server || null,\n          running: false,\n          tools: [],\n          error: result.message,\n          process: null,\n        });\n        continue;\n      }\n\n      const mcp = this.mcps[name];\n      if (!mcp) {\n        delete this.mcpLoadingResults[name];\n        delete this.mcps[name];\n        continue;\n      }\n\n      const online = !!(await mcp.ping());\n      const tools = (online ? (await mcp.listTools()).tools : []).filter(\n        (tool) => !tool.name.startsWith(\"handle_mcp_connection_mcp_\")\n      );\n      servers.push({\n        name,\n        config: config?.server || null,\n        running: online,\n        tools,\n        error: null,\n        process: {\n          pid: mcp.transport?.process?.pid || null,\n        },\n      });\n    }\n    return servers;\n  }\n\n  /**\n   * Toggle the MCP server (start or stop)\n   * @param {string} name - The name of the MCP server to toggle\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  async toggleServerStatus(name) {\n    const server = this.mcpServerConfigs.find((s) => s.name === name);\n    if (!server)\n      return {\n        success: false,\n        error: `MCP server ${name} not found in config file.`,\n      };\n    const mcp = this.mcps[name];\n    const online = !!mcp ? !!(await mcp.ping()) : false; // If the server is not in the mcps object, it is not running\n\n    if (online) {\n      const killed = this.pruneMCPServer(name);\n      return {\n        success: killed,\n        error: killed ? null : `Failed to kill MCP server: ${name}`,\n      };\n    } else {\n      const startupResult = await this.startMCPServer(name);\n      return { success: startupResult.success, error: startupResult.error };\n    }\n  }\n\n  /**\n   * Delete the MCP server - will also remove it from the config file\n   * @param {string} name - The name of the MCP server to delete\n   * @returns {Promise<{success: boolean, error: string | null}>}\n   */\n  async deleteServer(name) {\n    const server = this.mcpServerConfigs.find((s) => s.name === name);\n    if (!server)\n      return {\n        success: false,\n        error: `MCP server ${name} not found in config file.`,\n      };\n\n    const mcp = this.mcps[name];\n    const online = !!mcp ? !!(await mcp.ping()) : false; // If the server is not in the mcps object, it is not running\n    if (online) this.pruneMCPServer(name);\n    this.removeMCPServerFromConfig(name);\n\n    delete this.mcps[name];\n    delete this.mcpLoadingResults[name];\n    this.log(`MCP server was killed and removed from config file: ${name}`);\n    return { success: true, error: null };\n  }\n\n  /**\n   * Return the result of an MCP server call as a string\n   * This will handle circular references and bigints since an MCP server can return any type of data.\n   * @param {Object} result - The result to return\n   * @returns {string} The result as a string\n   */\n  static returnMCPResult(result) {\n    if (typeof result !== \"object\" || result === null) return String(result);\n\n    const seen = new WeakSet();\n    try {\n      return JSON.stringify(result, (key, value) => {\n        if (typeof value === \"bigint\") return value.toString();\n        if (typeof value === \"object\" && value !== null) {\n          if (seen.has(value)) return \"[Circular]\";\n          seen.add(value);\n        }\n        return value;\n      });\n    } catch (e) {\n      return `[Unserializable: ${e.message}]`;\n    }\n  }\n\n  /**\n   * Toggle tool suppression for an MCP server\n   * @param {string} serverName - The name of the MCP server\n   * @param {string} toolName - The name of the tool to toggle\n   * @param {boolean} enabled - Whether the tool should be enabled (true) or suppressed (false)\n   * @returns {Promise<{success: boolean, error: string | null, suppressedTools: string[]}>}\n   */\n  async toggleToolSuppression(serverName, toolName, enabled) {\n    return this.updateSuppressedTools(serverName, toolName, enabled);\n  }\n}\nmodule.exports = MCPCompatibilityLayer;\n"
  },
  {
    "path": "server/utils/PasswordRecovery/index.js",
    "content": "const bcrypt = require(\"bcryptjs\");\nconst { v4, validate } = require(\"uuid\");\nconst { User } = require(\"../../models/user\");\nconst {\n  RecoveryCode,\n  PasswordResetToken,\n} = require(\"../../models/passwordRecovery\");\n\nasync function generateRecoveryCodes(userId) {\n  const newRecoveryCodes = [];\n  const plainTextCodes = [];\n  for (let i = 0; i < 4; i++) {\n    const code = v4();\n    const hashedCode = bcrypt.hashSync(code, 10);\n    newRecoveryCodes.push({\n      user_id: userId,\n      code_hash: hashedCode,\n    });\n    plainTextCodes.push(code);\n  }\n\n  const { error } = await RecoveryCode.createMany(newRecoveryCodes);\n  if (!!error) throw new Error(error);\n\n  const { user: success } = await User._update(userId, {\n    seen_recovery_codes: true,\n  });\n  if (!success) throw new Error(\"Failed to generate user recovery codes!\");\n\n  return plainTextCodes;\n}\n\nasync function recoverAccount(username = \"\", recoveryCodes = []) {\n  const user = await User.get({ username: String(username) });\n  if (!user) return { success: false, error: \"Invalid recovery codes.\" };\n\n  // If hashes do not exist for a user\n  // because this is a user who has not logged out and back in since upgrade.\n  const allUserHashes = await RecoveryCode.hashesForUser(user.id);\n  if (allUserHashes.length < 4)\n    return { success: false, error: \"Invalid recovery codes.\" };\n\n  // If they tried to send more than two unique codes, we only take the first two\n  const uniqueRecoveryCodes = [...new Set(recoveryCodes)]\n    .map((code) => code.trim())\n    .filter((code) => validate(code)) // we know that any provided code must be a uuid v4.\n    .slice(0, 2);\n  if (uniqueRecoveryCodes.length !== 2)\n    return { success: false, error: \"Invalid recovery codes.\" };\n\n  const validCodes = uniqueRecoveryCodes.every((code) => {\n    let valid = false;\n    allUserHashes.forEach((hash) => {\n      if (bcrypt.compareSync(code, hash)) valid = true;\n    });\n    return valid;\n  });\n  if (!validCodes) return { success: false, error: \"Invalid recovery codes.\" };\n\n  const { passwordResetToken, error } = await PasswordResetToken.create(\n    user.id\n  );\n  if (!!error) return { success: false, error };\n  return { success: true, resetToken: passwordResetToken.token };\n}\n\nasync function resetPassword(token, _newPassword = \"\", confirmPassword = \"\") {\n  const newPassword = String(_newPassword).trim(); // No spaces in passwords\n  if (!newPassword) throw new Error(\"Invalid password.\");\n  if (newPassword !== String(confirmPassword))\n    throw new Error(\"Passwords do not match\");\n\n  const resetToken = await PasswordResetToken.findUnique({\n    token: String(token),\n  });\n  if (!resetToken || resetToken.expiresAt < new Date()) {\n    return { success: false, message: \"Invalid reset token\" };\n  }\n\n  // JOI password rules will be enforced inside .update.\n  const { error } = await User.update(resetToken.user_id, {\n    password: newPassword,\n  });\n\n  // seen_recovery_codes is not publicly writable\n  // so we have to do direct update here\n  await User._update(resetToken.user_id, {\n    seen_recovery_codes: false,\n  });\n\n  if (error) return { success: false, message: error };\n  await PasswordResetToken.deleteMany({ user_id: resetToken.user_id });\n  await RecoveryCode.deleteMany({ user_id: resetToken.user_id });\n\n  // New codes are provided on first new login.\n  return { success: true, message: \"Password reset successful\" };\n}\n\nmodule.exports = {\n  recoverAccount,\n  resetPassword,\n  generateRecoveryCodes,\n};\n"
  },
  {
    "path": "server/utils/PushNotifications/index.js",
    "content": "const webpush = require(\"web-push\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { User } = require(\"../../models/user\");\nconst { SystemSettings } = require(\"../../models/systemSettings\");\nconst { safeJsonParse } = require(\"../http\");\n\n/**\n * For more options, see:\n * https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#options\n * @typedef {Object} PushNotificationPayload\n * @property {string} title - The title of the notification.\n * @property {string} body - The message of the notification.\n * @property {Object} data - Unstructured data for the notification. Use this for anything non-standard.\n * @property {string} [data.onClickUrl] - The URL to open when the notification is clicked. Note: Can be relative or absolute.\n * @property {Object[]} actions - The actions for the notification.\n * @property {string} [actions[].action] - The action to perform when the notification is clicked. Handled in the service worker.\n * @property {string} [actions[].title] - The title of the action to show in the Options dropdown\n * @property {string} image - A string containing the URL of an image to be displayed in the notification.\n */\n\nclass PushNotifications {\n  static mailTo = \"anythingllm@localhost\";\n  /**\n   * @type {PushNotifications}\n   */\n  static instance = null;\n\n  /**\n   * The VAPID keys for the push notification service.\n   * @type {{publicKey: string | null, privateKey: string | null}}\n   */\n  #vapidKeys = {\n    publicKey: null,\n    privateKey: null,\n  };\n\n  /**\n   * The subscriptions for the push notification service.\n   * @type {Map<string, Object>}\n   */\n  #subscriptions = new Map();\n\n  constructor() {\n    if (PushNotifications.instance) return PushNotifications.instance;\n    PushNotifications.instance = this;\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[36m[PushNotifications]\\x1b[0m ${text}`, ...args);\n  }\n\n  get pushService() {\n    try {\n      const vapidKeys = this.existingVapidKeys;\n      if (!vapidKeys.publicKey || !vapidKeys.privateKey)\n        throw new Error(\n          \"VAPID keys not found. Make sure they are generated in the main process first.\"\n        );\n      webpush.setVapidDetails(\n        `mailto:${this.mailTo}`,\n        vapidKeys.publicKey,\n        vapidKeys.privateKey\n      );\n      return webpush;\n    } catch (e) {\n      console.error(\"Failed to set VAPID details\", e);\n      return null;\n    }\n  }\n\n  get storagePath() {\n    return process.env.NODE_ENV === \"development\"\n      ? path.resolve(__dirname, `../../storage`, \"push-notifications\")\n      : path.resolve(process.env.STORAGE_DIR, \"push-notifications\");\n  }\n\n  get primarySubscriptionPath() {\n    return path.resolve(this.storagePath, `primary-subscription.json`);\n  }\n\n  get existingVapidKeys() {\n    // Already loaded and binded to the instance\n    if (this.#vapidKeys.publicKey && this.#vapidKeys.privateKey)\n      return this.#vapidKeys;\n\n    const vapidKeysPath = path.resolve(this.storagePath, `vapid-keys.json`);\n    if (!fs.existsSync(vapidKeysPath))\n      return { publicKey: null, privateKey: null };\n\n    const existingVapidKeys = JSON.parse(\n      fs.readFileSync(vapidKeysPath, \"utf8\")\n    );\n    this.#log(`Loaded existing VAPID keys!`);\n    this.#vapidKeys.publicKey = existingVapidKeys.publicKey;\n    this.#vapidKeys.privateKey = existingVapidKeys.privateKey;\n    return this.#vapidKeys;\n  }\n\n  get publicVapidKey() {\n    return this.existingVapidKeys.publicKey;\n  }\n\n  /**\n   * Load the subscriptions for the push notification service.\n   * In single user mode, the subscription is stored in the primary-subscription.json file.\n   * In multi user mode, the subscriptions are stored in the database so we grab them from there\n   * and store them in the #subscriptions map for reference later.\n   * @returns {Promise<void>}\n   */\n  async loadSubscriptions() {\n    const isMultiUserMode = await SystemSettings.isMultiUserMode();\n    if (isMultiUserMode) {\n      const users = await User._where({\n        web_push_subscription_config: { not: null },\n      });\n      for (const user of users) {\n        const subscription = safeJsonParse(\n          user.web_push_subscription_config,\n          null\n        );\n        if (subscription) this.#subscriptions.set(user.id, subscription);\n      }\n      this.#log(`Loaded ${this.#subscriptions.size} existing subscriptions.`);\n      return;\n    }\n\n    this.#log(\"Loading single user mode subscriptions...\");\n    if (!fs.existsSync(this.primarySubscriptionPath)) return;\n    const subscription = JSON.parse(\n      fs.readFileSync(this.primarySubscriptionPath, \"utf8\")\n    );\n    if (subscription) this.#subscriptions.set(\"primary\", subscription);\n    this.#log(`Loaded primary user's existing subscription.`);\n  }\n\n  /**\n   * Register a new subscription for a user.\n   * In single user mode, the userId is mapped to \"primary\"\n   * In multi user mode, the userId is the user's id in the database\n   *\n   * @param {Object|null} user - The user to register the subscription for.\n   * @param {Object} subscription - The subscription to register.\n   * @returns {Promise<PushNotifications>}\n   */\n  async registerSubscription(user = null, subscription) {\n    let userId = user?.id || \"primary\";\n    this.#subscriptions.set(userId, subscription);\n\n    // If this was a real user, write the subscription to the database\n    if (!!user) {\n      await User._update(user.id, {\n        web_push_subscription_config: JSON.stringify(subscription),\n      });\n      this.#log(`Registered or updated subscription for user - ${user.id}`);\n    } else {\n      if (!fs.existsSync(this.storagePath))\n        fs.mkdirSync(this.storagePath, { recursive: true });\n      fs.writeFileSync(\n        this.primarySubscriptionPath,\n        JSON.stringify(subscription, null, 2)\n      );\n      this.#log(`Registered or updated primary user's subscription.`);\n    }\n    return this;\n  }\n\n  /**\n   * Send a push notification to all subscribed clients.\n   * @param {Object} options - The options for the notification.\n   * @param {\"primary\"|number} [options.to] - The subscription to send the notification to. \"all\" sends to all subscriptions, \"primary\" sends to the primary user (single user mode only), a number sends subscription to specific user\n   * @param {PushNotificationPayload} [options.payload] - The payload to send to the clients.\n   * @returns {void}\n   */\n  sendNotification({ to = \"primary\", payload = {} } = {}) {\n    if (this.#subscriptions.size === 0)\n      return this.#log(\".sendNotification() - No subscriptions found\");\n    if (!this.#subscriptions.has(to))\n      return this.#log(\n        `.sendNotification() - Subscription for user ${to} not found`\n      );\n    this.#log(`.sendNotification() - Sending notification to user ${to}`);\n    this.pushService.sendNotification(\n      this.#subscriptions.get(to),\n      JSON.stringify(payload)\n    );\n  }\n\n  /**\n   * Setup the push notification service.\n   * This will generate new VAPID keys if they don't exist and save them to the storage path.\n   * It will also load the subscriptions from the database or the primary-subscription.json file.\n   * @returns {Promise<void>}\n   */\n  static async setupPushNotificationService() {\n    const instance = PushNotifications.instance;\n    const existingVapidKeys = instance.existingVapidKeys;\n\n    if (!existingVapidKeys.publicKey || !existingVapidKeys.privateKey) {\n      instance.#log(\"Generating new VAPID keys...\");\n      const vapidKeys = webpush.generateVAPIDKeys();\n      instance.#vapidKeys.publicKey = vapidKeys.publicKey;\n      instance.#vapidKeys.privateKey = vapidKeys.privateKey;\n      instance.#log(`New VAPID keys generated!`);\n      if (!fs.existsSync(instance.storagePath))\n        fs.mkdirSync(instance.storagePath, { recursive: true });\n      fs.writeFileSync(\n        path.resolve(instance.storagePath, `vapid-keys.json`),\n        JSON.stringify(vapidKeys, null, 2)\n      );\n    }\n\n    await instance.loadSubscriptions();\n    instance.pushService;\n    return;\n  }\n}\n\nmodule.exports = {\n  pushNotificationService: new PushNotifications(),\n  PushNotifications,\n};\n"
  },
  {
    "path": "server/utils/TextSplitter/index.js",
    "content": "/**\n * @typedef {object} DocumentMetadata\n * @property {string} id - eg; \"123e4567-e89b-12d3-a456-426614174000\"\n * @property {string} url - eg; \"file://example.com/index.html\"\n * @property {string} title - eg; \"example.com/index.html\"\n * @property {string} docAuthor - eg; \"no author found\"\n * @property {string} description - eg; \"No description found.\"\n * @property {string} docSource - eg; \"URL link uploaded by the user.\"\n * @property {string} chunkSource - eg; link://https://example.com\n * @property {string} published - ISO 8601 date string\n * @property {number} wordCount - Number of words in the document\n * @property {string} pageContent - The raw text content of the document\n * @property {number} token_count_estimate - Number of tokens in the document\n */\n\nfunction isNullOrNaN(value) {\n  if (value === null) return true;\n  return isNaN(value);\n}\n\nclass TextSplitter {\n  #splitter;\n\n  /**\n   * Creates a new TextSplitter instance.\n   * @param {Object} config\n   * @param {string} [config.chunkPrefix = \"\"] - Prefix to be added to the start of each chunk.\n   * @param {number} [config.chunkSize = 1000] - The size of each chunk.\n   * @param {number} [config.chunkOverlap = 20] - The overlap between chunks.\n   * @param {Object} [config.chunkHeaderMeta = null] - Metadata to be added to the start of each chunk - will come after the prefix.\n   */\n  constructor(config = {}) {\n    this.config = config;\n    this.#splitter = this.#setSplitter(config);\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[35m[TextSplitter]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   *  Does a quick check to determine the text chunk length limit.\n   * Embedder models have hard-set limits that cannot be exceeded, just like an LLM context\n   * so here we want to allow override of the default 1000, but up to the models maximum, which is\n   * sometimes user defined.\n   */\n  static determineMaxChunkSize(preferred = null, embedderLimit = 1000) {\n    const prefValue = isNullOrNaN(preferred)\n      ? Number(embedderLimit)\n      : Number(preferred);\n    const limit = Number(embedderLimit);\n    if (prefValue > limit)\n      console.log(\n        `\\x1b[43m[WARN]\\x1b[0m Text splitter chunk length of ${prefValue} exceeds embedder model max of ${embedderLimit}. Will use ${embedderLimit}.`\n      );\n    return prefValue > limit ? limit : prefValue;\n  }\n\n  /**\n   *  Creates a string of metadata to be prepended to each chunk.\n   * @param {DocumentMetadata} metadata - Metadata to be prepended to each chunk.\n   * @returns {{[key: ('title' | 'published' | 'source')]: string}} Object of metadata that will be prepended to each chunk.\n   */\n  static buildHeaderMeta(metadata = {}) {\n    if (!metadata || Object.keys(metadata).length === 0) return null;\n    const PLUCK_MAP = {\n      title: {\n        as: \"sourceDocument\",\n        pluck: (metadata) => {\n          return metadata?.title || null;\n        },\n      },\n      published: {\n        as: \"published\",\n        pluck: (metadata) => {\n          return metadata?.published || null;\n        },\n      },\n      chunkSource: {\n        as: \"source\",\n        pluck: (metadata) => {\n          const validPrefixes = [\"link://\", \"youtube://\"];\n          // If the chunkSource is a link or youtube link, we can add the URL\n          // as its source in the metadata so the LLM can use it for context.\n          // eg prompt: Where did you get this information? -> answer: \"from https://example.com\"\n          if (\n            !metadata?.chunkSource || // Exists\n            !metadata?.chunkSource.length || // Is not empty\n            typeof metadata.chunkSource !== \"string\" || // Is a string\n            !validPrefixes.some(\n              (prefix) => metadata.chunkSource.startsWith(prefix) // Has a valid prefix we respect\n            )\n          )\n            return null;\n\n          // We know a prefix is present, so we can split on it and return the rest.\n          // If nothing is found, return null and it will not be added to the metadata.\n          let source = null;\n          for (const prefix of validPrefixes) {\n            source = metadata.chunkSource.split(prefix)?.[1] || null;\n            if (source) break;\n          }\n\n          return source;\n        },\n      },\n    };\n\n    const pluckedData = {};\n    Object.entries(PLUCK_MAP).forEach(([key, value]) => {\n      if (!(key in metadata)) return; // Skip if the metadata key is not present.\n      const pluckedValue = value.pluck(metadata);\n      if (!pluckedValue) return; // Skip if the plucked value is null/empty.\n      pluckedData[value.as] = pluckedValue;\n    });\n\n    return pluckedData;\n  }\n\n  /**\n   * Apply the chunk prefix to the text if it is present.\n   * @param {string} text - The text to apply the prefix to.\n   * @returns {string} The text with the embedder model prefix applied.\n   */\n  #applyPrefix(text = \"\") {\n    if (!this.config.chunkPrefix) return text;\n    return `${this.config.chunkPrefix}${text}`;\n  }\n\n  /**\n   * Creates a string of metadata to be prepended to each chunk.\n   * Will additionally prepend a prefix to the text if it was provided (requirement for some embedders).\n   * @returns {string} The text with the embedder model prefix applied.\n   */\n  stringifyHeader() {\n    let content = \"\";\n    if (!this.config.chunkHeaderMeta) return this.#applyPrefix(content);\n    Object.entries(this.config.chunkHeaderMeta).map(([key, value]) => {\n      if (!key || !value) return;\n      content += `${key}: ${value}\\n`;\n    });\n\n    if (!content) return this.#applyPrefix(content);\n    return this.#applyPrefix(\n      `<document_metadata>\\n${content}</document_metadata>\\n\\n`\n    );\n  }\n\n  /**\n   * Sets the splitter to use a defined config passes to other subclasses.\n   * @param {Object} config\n   * @param {string} [config.chunkPrefix = \"\"] - Prefix to be added to the start of each chunk.\n   * @param {number} [config.chunkSize = 1000] - The size of each chunk.\n   * @param {number} [config.chunkOverlap = 20] - The overlap between chunks.\n   */\n  #setSplitter(config = {}) {\n    // if (!config?.splitByFilename) {// TODO do something when specific extension is present? }\n    return new RecursiveSplitter({\n      chunkSize: isNaN(config?.chunkSize) ? 1_000 : Number(config?.chunkSize),\n      chunkOverlap: isNaN(config?.chunkOverlap)\n        ? 20\n        : Number(config?.chunkOverlap),\n      chunkHeader: this.stringifyHeader(),\n    });\n  }\n\n  async splitText(documentText) {\n    return this.#splitter._splitText(documentText);\n  }\n}\n\n// Wrapper for Langchain default RecursiveCharacterTextSplitter class.\nclass RecursiveSplitter {\n  constructor({ chunkSize, chunkOverlap, chunkHeader = null }) {\n    const {\n      RecursiveCharacterTextSplitter,\n    } = require(\"@langchain/textsplitters\");\n    this.log(`Will split with`, {\n      chunkSize,\n      chunkOverlap,\n      chunkHeader: chunkHeader ? `${chunkHeader?.slice(0, 50)}...` : null,\n    });\n    this.chunkHeader = chunkHeader;\n    this.engine = new RecursiveCharacterTextSplitter({\n      chunkSize,\n      chunkOverlap,\n    });\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[35m[RecursiveSplitter]\\x1b[0m ${text}`, ...args);\n  }\n\n  async _splitText(documentText) {\n    if (!this.chunkHeader) return this.engine.splitText(documentText);\n    const strings = await this.engine.splitText(documentText);\n    const documents = await this.engine.createDocuments(strings, [], {\n      chunkHeader: this.chunkHeader,\n    });\n    return documents\n      .filter((doc) => !!doc.pageContent)\n      .map((doc) => doc.pageContent);\n  }\n}\n\nmodule.exports.TextSplitter = TextSplitter;\n"
  },
  {
    "path": "server/utils/TextToSpeech/elevenLabs/index.js",
    "content": "const { ElevenLabsClient } = require(\"elevenlabs\");\n\nclass ElevenLabsTTS {\n  constructor() {\n    if (!process.env.TTS_ELEVEN_LABS_KEY)\n      throw new Error(\"No ElevenLabs API key was set.\");\n    this.elevenLabs = new ElevenLabsClient({\n      apiKey: process.env.TTS_ELEVEN_LABS_KEY,\n    });\n\n    // Rachel as default voice\n    // https://api.elevenlabs.io/v1/voices\n    this.voiceId =\n      process.env.TTS_ELEVEN_LABS_VOICE_MODEL ?? \"21m00Tcm4TlvDq8ikWAM\";\n    this.modelId = \"eleven_multilingual_v2\";\n  }\n\n  static async voices(apiKey = null) {\n    try {\n      const client = new ElevenLabsClient({\n        apiKey: apiKey ?? process.env.TTS_ELEVEN_LABS_KEY ?? null,\n      });\n      return (await client.voices.getAll())?.voices ?? [];\n    } catch {}\n    return [];\n  }\n\n  #stream2buffer(stream) {\n    return new Promise((resolve, reject) => {\n      const _buf = [];\n      stream.on(\"data\", (chunk) => _buf.push(chunk));\n      stream.on(\"end\", () => resolve(Buffer.concat(_buf)));\n      stream.on(\"error\", (err) => reject(err));\n    });\n  }\n\n  async ttsBuffer(textInput) {\n    try {\n      const audio = await this.elevenLabs.generate({\n        voice: this.voiceId,\n        text: textInput,\n        model_id: \"eleven_multilingual_v2\",\n      });\n      return Buffer.from(await this.#stream2buffer(audio));\n    } catch (e) {\n      console.error(e);\n    }\n    return null;\n  }\n}\n\nmodule.exports = {\n  ElevenLabsTTS,\n};\n"
  },
  {
    "path": "server/utils/TextToSpeech/index.js",
    "content": "function getTTSProvider() {\n  const provider = process.env.TTS_PROVIDER || \"openai\";\n  switch (provider) {\n    case \"openai\":\n      const { OpenAiTTS } = require(\"./openAi\");\n      return new OpenAiTTS();\n    case \"elevenlabs\":\n      const { ElevenLabsTTS } = require(\"./elevenLabs\");\n      return new ElevenLabsTTS();\n    case \"generic-openai\":\n      const { GenericOpenAiTTS } = require(\"./openAiGeneric\");\n      return new GenericOpenAiTTS();\n    default:\n      throw new Error(\"ENV: No TTS_PROVIDER value found in environment!\");\n  }\n}\n\nmodule.exports = { getTTSProvider };\n"
  },
  {
    "path": "server/utils/TextToSpeech/openAi/index.js",
    "content": "class OpenAiTTS {\n  constructor() {\n    if (!process.env.TTS_OPEN_AI_KEY)\n      throw new Error(\"No OpenAI API key was set.\");\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.openai = new OpenAIApi({\n      apiKey: process.env.TTS_OPEN_AI_KEY,\n    });\n    this.voice = process.env.TTS_OPEN_AI_VOICE_MODEL ?? \"alloy\";\n  }\n\n  async ttsBuffer(textInput) {\n    try {\n      const result = await this.openai.audio.speech.create({\n        model: \"tts-1\",\n        voice: this.voice,\n        input: textInput,\n      });\n      return Buffer.from(await result.arrayBuffer());\n    } catch (e) {\n      console.error(e);\n    }\n    return null;\n  }\n}\n\nmodule.exports = {\n  OpenAiTTS,\n};\n"
  },
  {
    "path": "server/utils/TextToSpeech/openAiGeneric/index.js",
    "content": "class GenericOpenAiTTS {\n  constructor() {\n    if (!process.env.TTS_OPEN_AI_COMPATIBLE_KEY)\n      this.#log(\n        \"No OpenAI compatible API key was set. You might need to set this to use your OpenAI compatible TTS service.\"\n      );\n    if (!process.env.TTS_OPEN_AI_COMPATIBLE_MODEL)\n      this.#log(\n        \"No OpenAI compatible TTS model was set. We will use the default voice model 'tts-1'. This may not exist or be valid your selected endpoint.\"\n      );\n    if (!process.env.TTS_OPEN_AI_COMPATIBLE_VOICE_MODEL)\n      this.#log(\n        \"No OpenAI compatible voice model was set. We will use the default voice model 'alloy'. This may not exist for your selected endpoint.\"\n      );\n    if (!process.env.TTS_OPEN_AI_COMPATIBLE_ENDPOINT)\n      throw new Error(\n        \"No OpenAI compatible endpoint was set. Please set this to use your OpenAI compatible TTS service.\"\n      );\n\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    this.openai = new OpenAIApi({\n      apiKey: process.env.TTS_OPEN_AI_COMPATIBLE_KEY || null,\n      baseURL: process.env.TTS_OPEN_AI_COMPATIBLE_ENDPOINT,\n    });\n    this.model = process.env.TTS_OPEN_AI_COMPATIBLE_MODEL ?? \"tts-1\";\n    this.voice = process.env.TTS_OPEN_AI_COMPATIBLE_VOICE_MODEL ?? \"alloy\";\n    this.#log(\n      `Service (${process.env.TTS_OPEN_AI_COMPATIBLE_ENDPOINT}) with model: ${this.model} and voice: ${this.voice}`\n    );\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[32m[OpenAiGenericTTS]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Generates a buffer from the given text input using the OpenAI compatible TTS service.\n   * @param {string} textInput - The text to be converted to audio.\n   * @returns {Promise<Buffer>} A buffer containing the audio data.\n   */\n  async ttsBuffer(textInput) {\n    try {\n      const result = await this.openai.audio.speech.create({\n        model: this.model,\n        voice: this.voice,\n        input: textInput,\n      });\n      return Buffer.from(await result.arrayBuffer());\n    } catch (e) {\n      console.error(e);\n    }\n    return null;\n  }\n}\n\nmodule.exports = {\n  GenericOpenAiTTS,\n};\n"
  },
  {
    "path": "server/utils/agentFlows/executor.js",
    "content": "const { FLOW_TYPES } = require(\"./flowTypes\");\nconst executeApiCall = require(\"./executors/api-call\");\nconst executeLLMInstruction = require(\"./executors/llm-instruction\");\nconst executeWebScraping = require(\"./executors/web-scraping\");\nconst { Telemetry } = require(\"../../models/telemetry\");\nconst { safeJsonParse } = require(\"../http\");\n\nclass FlowExecutor {\n  constructor() {\n    this.variables = {};\n    this.introspect = (...args) => console.log(\"[introspect] \", ...args);\n    this.logger = console.info;\n    this.aibitat = null;\n  }\n\n  attachLogging(introspectFn = null, loggerFn = null) {\n    this.introspect =\n      introspectFn || ((...args) => console.log(\"[introspect] \", ...args));\n    this.logger = loggerFn || console.info;\n  }\n\n  /**\n   * Resolves nested values from objects using dot notation and array indices\n   * Supports paths like \"data.items[0].name\" or \"response.users[2].address.city\"\n   * Returns undefined for invalid paths or errors\n   * @param {Object|string} obj - The object to resolve the value from\n   * @param {string} path - The path to the value\n   * @returns {string} The resolved value\n   */\n  getValueFromPath(obj = {}, path = \"\") {\n    if (typeof obj === \"string\") obj = safeJsonParse(obj, {});\n\n    if (\n      !obj ||\n      !path ||\n      typeof obj !== \"object\" ||\n      Object.keys(obj).length === 0 ||\n      typeof path !== \"string\"\n    )\n      return \"\";\n\n    // First split by dots that are not inside brackets\n    const parts = [];\n    let currentPart = \"\";\n    let inBrackets = false;\n\n    for (let i = 0; i < path.length; i++) {\n      const char = path[i];\n      if (char === \"[\") {\n        inBrackets = true;\n        if (currentPart) {\n          parts.push(currentPart);\n          currentPart = \"\";\n        }\n        currentPart += char;\n      } else if (char === \"]\") {\n        inBrackets = false;\n        currentPart += char;\n        parts.push(currentPart);\n        currentPart = \"\";\n      } else if (char === \".\" && !inBrackets) {\n        if (currentPart) {\n          parts.push(currentPart);\n          currentPart = \"\";\n        }\n      } else {\n        currentPart += char;\n      }\n    }\n\n    if (currentPart) parts.push(currentPart);\n    let current = obj;\n\n    for (const part of parts) {\n      if (current === null || typeof current !== \"object\") return undefined;\n\n      // Handle bracket notation\n      if (part.startsWith(\"[\") && part.endsWith(\"]\")) {\n        const key = part.slice(1, -1);\n        const cleanKey = key.replace(/^['\"]|['\"]$/g, \"\");\n\n        if (!isNaN(cleanKey)) {\n          if (!Array.isArray(current)) return undefined;\n          current = current[parseInt(cleanKey)];\n        } else {\n          if (!(cleanKey in current)) return undefined;\n          current = current[cleanKey];\n        }\n      } else {\n        // Handle dot notation\n        if (!(part in current)) return undefined;\n        current = current[part];\n      }\n\n      if (current === undefined || current === null) return undefined;\n    }\n\n    return typeof current === \"object\" ? JSON.stringify(current) : current;\n  }\n\n  /**\n   * Replaces variables in the config with their values\n   * @param {Object} config - The config to replace variables in\n   * @returns {Object} The config with variables replaced\n   */\n  replaceVariables(config) {\n    const deepReplace = (obj) => {\n      if (typeof obj === \"string\") {\n        return obj.replace(/\\${([^}]+)}/g, (match, varName) => {\n          const value = this.getValueFromPath(this.variables, varName);\n          return value !== undefined ? value : match;\n        });\n      }\n\n      if (Array.isArray(obj)) return obj.map((item) => deepReplace(item));\n\n      if (obj && typeof obj === \"object\") {\n        const result = {};\n        for (const [key, value] of Object.entries(obj)) {\n          result[key] = deepReplace(value);\n        }\n        return result;\n      }\n      return obj;\n    };\n\n    return deepReplace(config);\n  }\n\n  /**\n   * Executes a single step of the flow\n   * @param {Object} step - The step to execute\n   * @returns {Promise<Object>} The result of the step\n   */\n  async executeStep(step) {\n    const config = this.replaceVariables(step.config);\n    let result;\n    // Create execution context with introspect\n    const context = {\n      introspect: this.introspect,\n      variables: this.variables,\n      logger: this.logger,\n      aibitat: this.aibitat,\n    };\n\n    switch (step.type) {\n      case FLOW_TYPES.START.type:\n        // For start blocks, we just initialize variables if they're not already set\n        if (config.variables) {\n          config.variables.forEach((v) => {\n            if (v.name && !this.variables[v.name]) {\n              this.variables[v.name] = v.value || \"\";\n            }\n          });\n        }\n        result = this.variables;\n        break;\n      case FLOW_TYPES.API_CALL.type:\n        result = await executeApiCall(config, context);\n        break;\n      case FLOW_TYPES.LLM_INSTRUCTION.type:\n        result = await executeLLMInstruction(config, context);\n        break;\n      case FLOW_TYPES.WEB_SCRAPING.type:\n        result = await executeWebScraping(config, context);\n        break;\n      default:\n        throw new Error(`Unknown flow type: ${step.type}`);\n    }\n\n    // Store result in variable if specified\n    if (config.resultVariable || config.responseVariable) {\n      const varName = config.resultVariable || config.responseVariable;\n      this.variables[varName] = result;\n    }\n\n    // If directOutput is true, mark this result for direct output\n    if (config.directOutput) result = { directOutput: true, result };\n    return result;\n  }\n\n  /**\n   * Execute entire flow\n   * @param {Object} flow - The flow to execute\n   * @param {Object} initialVariables - Initial variables for the flow\n   * @param {Object} aibitat - The aibitat instance from the agent handler\n   */\n  async executeFlow(flow, initialVariables = {}, aibitat) {\n    await Telemetry.sendTelemetry(\"agent_flow_execution_started\");\n\n    // Initialize variables with both initial values and any passed-in values\n    this.variables = {\n      ...(\n        flow.config.steps.find((s) => s.type === \"start\")?.config?.variables ||\n        []\n      ).reduce((acc, v) => ({ ...acc, [v.name]: v.value }), {}),\n      ...initialVariables, // This will override any default values with passed-in values\n    };\n\n    this.aibitat = aibitat;\n    this.attachLogging(aibitat?.introspect, aibitat?.handlerProps?.log);\n    const results = [];\n    let directOutputResult = null;\n\n    for (const step of flow.config.steps) {\n      try {\n        const result = await this.executeStep(step);\n\n        // If the step has directOutput, stop processing and return the result\n        // so that no other steps are executed or processed\n        if (result?.directOutput) {\n          directOutputResult = result.result;\n          break;\n        }\n\n        results.push({ success: true, result });\n      } catch (error) {\n        results.push({ success: false, error: error.message });\n        break;\n      }\n    }\n\n    return {\n      success: results.every((r) => r.success),\n      results,\n      variables: this.variables,\n      directOutput: directOutputResult,\n    };\n  }\n}\n\nmodule.exports = {\n  FlowExecutor,\n  FLOW_TYPES,\n};\n"
  },
  {
    "path": "server/utils/agentFlows/executors/api-call.js",
    "content": "const { safeJsonParse } = require(\"../../http\");\n\n/**\n * Execute an API call flow step\n * @param {Object} config Flow step configuration\n * @param {Object} context Execution context with introspect function\n * @returns {Promise<string>} Response data\n */\nasync function executeApiCall(config, context) {\n  const { url, method, headers = [], body, bodyType, formData } = config;\n  const { introspect, logger } = context;\n  logger(`\\x1b[43m[AgentFlowToolExecutor]\\x1b[0m - executing API Call block`);\n  introspect(`Making ${method} request to external API...`);\n\n  const requestConfig = {\n    method,\n    headers: headers.reduce((acc, h) => ({ ...acc, [h.key]: h.value }), {}),\n  };\n\n  if ([\"POST\", \"PUT\", \"PATCH\"].includes(method)) {\n    if (bodyType === \"form\") {\n      const formDataObj = new URLSearchParams();\n      formData.forEach(({ key, value }) => formDataObj.append(key, value));\n      requestConfig.body = formDataObj.toString();\n      requestConfig.headers[\"Content-Type\"] =\n        \"application/x-www-form-urlencoded\";\n    } else if (bodyType === \"json\") {\n      const parsedBody = safeJsonParse(body, null);\n      if (parsedBody !== null) {\n        requestConfig.body = JSON.stringify(parsedBody);\n      }\n      requestConfig.headers[\"Content-Type\"] = \"application/json\";\n    } else if (bodyType === \"text\") {\n      requestConfig.body = String(body);\n    } else {\n      requestConfig.body = body;\n    }\n  }\n\n  try {\n    introspect(`Sending body to ${url}: ${requestConfig?.body || \"No body\"}`);\n    const response = await fetch(url, requestConfig);\n    if (!response.ok) {\n      introspect(`Request failed with status ${response.status}`);\n      throw new Error(`HTTP error! status: ${response.status}`);\n    }\n\n    introspect(`API call completed`);\n    return await response\n      .text()\n      .then((text) =>\n        safeJsonParse(text, \"Failed to parse output from API call block\")\n      );\n  } catch (error) {\n    console.error(error);\n    throw new Error(`API Call failed: ${error.message}`);\n  }\n}\n\nmodule.exports = executeApiCall;\n"
  },
  {
    "path": "server/utils/agentFlows/executors/llm-instruction.js",
    "content": "/**\n * Execute an LLM instruction flow step\n * @param {Object} config Flow step configuration\n * @param {{introspect: Function, logger: Function}} context Execution context with introspect function\n * @returns {Promise<string>} Processed result\n */\nasync function executeLLMInstruction(config, context) {\n  const { instruction, resultVariable } = config;\n  const { introspect, logger, aibitat } = context;\n  logger(\n    `\\x1b[43m[AgentFlowToolExecutor]\\x1b[0m - executing LLM Instruction block`\n  );\n  introspect(`Processing data with LLM instruction...`);\n\n  try {\n    logger(\n      `Sending request to LLM (${aibitat.defaultProvider.provider}::${aibitat.defaultProvider.model})`\n    );\n    introspect(`Sending request to LLM...`);\n\n    // Ensure the input is a string since we are sending it to the LLM direct as a message\n    let input = instruction;\n    if (typeof input === \"object\") input = JSON.stringify(input);\n    if (typeof input !== \"string\") input = String(input);\n\n    const provider = aibitat.getProviderForConfig(aibitat.defaultProvider);\n    const completion = await provider.complete([\n      {\n        role: \"user\",\n        content: input,\n      },\n    ]);\n\n    introspect(`Successfully received LLM response`);\n    if (resultVariable) config.resultVariable = resultVariable;\n    return completion.textResponse;\n  } catch (error) {\n    logger(`LLM processing failed: ${error.message}`, error);\n    throw new Error(`LLM processing failed: ${error.message}`);\n  }\n}\n\nmodule.exports = executeLLMInstruction;\n"
  },
  {
    "path": "server/utils/agentFlows/executors/web-scraping.js",
    "content": "/**\n * Execute a web scraping flow step\n * @param {Object} config Flow step configuration\n * @param {Object} context Execution context with introspect function\n * @returns {Promise<string>} Scraped content\n */\nasync function executeWebScraping(config, context) {\n  const { CollectorApi } = require(\"../../collectorApi\");\n  const { TokenManager } = require(\"../../helpers/tiktoken\");\n  const Provider = require(\"../../agents/aibitat/providers/ai-provider\");\n  const { summarizeContent } = require(\"../../agents/aibitat/utils/summarize\");\n\n  const { url, captureAs = \"text\", enableSummarization = true } = config;\n  const { introspect, logger, aibitat } = context;\n  logger(\n    `\\x1b[43m[AgentFlowToolExecutor]\\x1b[0m - executing Web Scraping block`\n  );\n\n  if (!url) {\n    throw new Error(\"URL is required for web scraping\");\n  }\n\n  const captureMode = captureAs === \"querySelector\" ? \"html\" : captureAs;\n  introspect(`Scraping the content of ${url} as ${captureAs}`);\n  const { success, content } = await new CollectorApi()\n    .getLinkContent(url, captureMode)\n    .then((res) => {\n      if (captureAs !== \"querySelector\") return res;\n      return parseHTMLwithSelector(res.content, config.querySelector, context);\n    });\n\n  if (!success) {\n    introspect(`Could not scrape ${url}. Cannot use this page's content.`);\n    throw new Error(\"URL could not be scraped and no content was found.\");\n  }\n\n  introspect(`Successfully scraped content from ${url}`);\n  if (!content || content?.length === 0) {\n    introspect(\"There was no content to be collected or read.\");\n    throw new Error(\"There was no content to be collected or read.\");\n  }\n\n  if (!enableSummarization) {\n    logger(`Returning raw content as summarization is disabled`);\n    return content;\n  }\n\n  const tokenCount = new TokenManager(\n    aibitat.defaultProvider.model\n  ).countFromString(content);\n  const contextLimit = Provider.contextLimit(\n    aibitat.defaultProvider.provider,\n    aibitat.defaultProvider.model\n  );\n\n  if (tokenCount < contextLimit) {\n    logger(\n      `Content within token limit (${tokenCount}/${contextLimit}). Returning raw content.`\n    );\n    return content;\n  }\n\n  introspect(\n    `This page's content is way too long (${tokenCount} tokens). I will summarize it right now.`\n  );\n  const summary = await summarizeContent({\n    provider: aibitat.defaultProvider.provider,\n    model: aibitat.defaultProvider.model,\n    content,\n  });\n\n  introspect(`Successfully summarized content`);\n  return summary;\n}\n\n/**\n * Parse HTML with a CSS selector\n * @param {string} html - The HTML to parse\n * @param {string|null} selector - The CSS selector to use (as text string)\n * @param {{introspect: Function}} context - The context object\n * @returns {Object} The parsed content\n */\nfunction parseHTMLwithSelector(html, selector = null, context) {\n  if (!selector || selector.length === 0) {\n    context.introspect(\"No selector provided. Returning the entire HTML.\");\n    return { success: true, content: html };\n  }\n\n  const Cheerio = require(\"cheerio\");\n  const $ = Cheerio.load(html);\n  const selectedElements = $(selector);\n\n  let content;\n  if (selectedElements.length === 0) {\n    return { success: false, content: null };\n  } else if (selectedElements.length === 1) {\n    content = selectedElements.html();\n  } else {\n    context.introspect(\n      `Found ${selectedElements.length} elements matching selector: ${selector}`\n    );\n    content = selectedElements\n      .map((_, element) => $(element).html())\n      .get()\n      .join(\"\\n\");\n  }\n  return { success: true, content };\n}\n\nmodule.exports = executeWebScraping;\n"
  },
  {
    "path": "server/utils/agentFlows/flowTypes.js",
    "content": "const FLOW_TYPES = {\n  START: {\n    type: \"start\",\n    description: \"Initialize flow variables\",\n    parameters: {\n      variables: {\n        type: \"array\",\n        description: \"List of variables to initialize\",\n      },\n    },\n  },\n  API_CALL: {\n    type: \"apiCall\",\n    description: \"Make an HTTP request to an API endpoint\",\n    parameters: {\n      url: { type: \"string\", description: \"The URL to make the request to\" },\n      method: { type: \"string\", description: \"HTTP method (GET, POST, etc.)\" },\n      headers: {\n        type: \"array\",\n        description: \"Request headers as key-value pairs\",\n      },\n      bodyType: {\n        type: \"string\",\n        description: \"Type of request body (json, form)\",\n      },\n      body: {\n        type: \"string\",\n        description:\n          \"Request body content. If body type is json, always return a valid json object. If body type is form, always return a valid form data object.\",\n      },\n      formData: { type: \"array\", description: \"Form data as key-value pairs\" },\n      responseVariable: {\n        type: \"string\",\n        description: \"Variable to store the response\",\n      },\n      directOutput: {\n        type: \"boolean\",\n        description:\n          \"Whether to return the response directly to the user without LLM processing\",\n      },\n    },\n    examples: [\n      {\n        url: \"https://api.example.com/data\",\n        method: \"GET\",\n        headers: [{ key: \"Authorization\", value: \"Bearer 1234567890\" }],\n      },\n    ],\n  },\n  LLM_INSTRUCTION: {\n    type: \"llmInstruction\",\n    description: \"Process data using LLM instructions\",\n    parameters: {\n      instruction: {\n        type: \"string\",\n        description: \"The instruction for the LLM to follow\",\n      },\n      resultVariable: {\n        type: \"string\",\n        description: \"Variable to store the processed result\",\n      },\n    },\n  },\n  WEB_SCRAPING: {\n    type: \"webScraping\",\n    description: \"Scrape content from a webpage\",\n    parameters: {\n      url: {\n        type: \"string\",\n        description: \"The URL of the webpage to scrape\",\n      },\n      resultVariable: {\n        type: \"string\",\n        description: \"Variable to store the scraped content\",\n      },\n      directOutput: {\n        type: \"boolean\",\n        description:\n          \"Whether to return the scraped content directly to the user without LLM processing\",\n      },\n    },\n  },\n};\n\nmodule.exports.FLOW_TYPES = FLOW_TYPES;\n"
  },
  {
    "path": "server/utils/agentFlows/index.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst { FlowExecutor, FLOW_TYPES } = require(\"./executor\");\nconst { normalizePath } = require(\"../files\");\nconst { safeJsonParse } = require(\"../http\");\n\n/**\n * @typedef {Object} LoadedFlow\n * @property {string} name - The name of the flow\n * @property {string} uuid - The UUID of the flow\n * @property {Object} config - The flow configuration details\n * @property {string} config.description - The description of the flow\n * @property {Array<{type: string, config: Object, [key: string]: any}>} config.steps - The steps of the flow. Each step has at least a type and config\n */\n\nclass AgentFlows {\n  static flowsDir = process.env.STORAGE_DIR\n    ? path.join(process.env.STORAGE_DIR, \"plugins\", \"agent-flows\")\n    : path.join(process.cwd(), \"storage\", \"plugins\", \"agent-flows\");\n\n  constructor() {}\n\n  /**\n   * Ensure flows directory exists\n   * @returns {Boolean} True if directory exists, false otherwise\n   */\n  static createOrCheckFlowsDir() {\n    try {\n      if (fs.existsSync(AgentFlows.flowsDir)) return true;\n      fs.mkdirSync(AgentFlows.flowsDir, { recursive: true });\n      return true;\n    } catch (error) {\n      console.error(\"Failed to create flows directory:\", error);\n      return false;\n    }\n  }\n\n  /**\n   * Helper to get all flow files with their contents\n   * @returns {Object} Map of flow UUID to flow config\n   */\n  static getAllFlows() {\n    AgentFlows.createOrCheckFlowsDir();\n    const files = fs.readdirSync(AgentFlows.flowsDir);\n    const flows = {};\n\n    for (const file of files) {\n      if (!file.endsWith(\".json\")) continue;\n      try {\n        const filePath = path.join(AgentFlows.flowsDir, file);\n        const content = fs.readFileSync(normalizePath(filePath), \"utf8\");\n        const config = JSON.parse(content);\n        const id = file.replace(\".json\", \"\");\n        flows[id] = config;\n      } catch (error) {\n        console.error(`Error reading flow file ${file}:`, error);\n      }\n    }\n\n    return flows;\n  }\n\n  /**\n   * Load a flow configuration by UUID\n   * @param {string} uuid - The UUID of the flow to load\n   * @returns {LoadedFlow|null} Flow configuration or null if not found\n   */\n  static loadFlow(uuid) {\n    try {\n      const flowJsonPath = normalizePath(\n        path.join(AgentFlows.flowsDir, `${uuid}.json`)\n      );\n      if (!uuid || !fs.existsSync(flowJsonPath)) return null;\n      const flow = safeJsonParse(fs.readFileSync(flowJsonPath, \"utf8\"), null);\n      if (!flow) return null;\n\n      return {\n        name: flow.name,\n        uuid,\n        config: flow,\n      };\n    } catch (error) {\n      console.error(\"Failed to load flow:\", error);\n      return null;\n    }\n  }\n\n  /**\n   * Save a flow configuration\n   * @param {string} name - The name of the flow\n   * @param {Object} config - The flow configuration\n   * @param {string|null} uuid - Optional UUID for the flow\n   * @returns {Object} Result of the save operation\n   */\n  static saveFlow(name, config, uuid = null) {\n    try {\n      AgentFlows.createOrCheckFlowsDir();\n\n      if (!uuid) uuid = uuidv4();\n      const normalizedUuid = normalizePath(`${uuid}.json`);\n      const filePath = path.join(AgentFlows.flowsDir, normalizedUuid);\n\n      // Prevent saving flows with unsupported blocks or importing\n      // flows with unsupported blocks (eg: file writing or code execution on Desktop importing to Docker)\n      const supportedFlowTypes = Object.values(FLOW_TYPES).map(\n        (definition) => definition.type\n      );\n      const supportsAllBlocks = config.steps.every((step) =>\n        supportedFlowTypes.includes(step.type)\n      );\n      if (!supportsAllBlocks)\n        throw new Error(\n          \"This flow includes unsupported blocks. They may not be supported by your version of AnythingLLM or are not available on this platform.\"\n        );\n\n      fs.writeFileSync(filePath, JSON.stringify({ ...config, name }, null, 2));\n      return { success: true, uuid };\n    } catch (error) {\n      console.error(\"Failed to save flow:\", error);\n      return { success: false, error: error.message };\n    }\n  }\n\n  /**\n   * List all available flows\n   * @returns {Array} Array of flow summaries\n   */\n  static listFlows() {\n    try {\n      const flows = AgentFlows.getAllFlows();\n      return Object.entries(flows).map(([uuid, flow]) => ({\n        name: flow.name,\n        uuid,\n        description: flow.description,\n        active: flow.active !== false,\n      }));\n    } catch (error) {\n      console.error(\"Failed to list flows:\", error);\n      return [];\n    }\n  }\n\n  /**\n   * Delete a flow by UUID\n   * @param {string} uuid - The UUID of the flow to delete\n   * @returns {Object} Result of the delete operation\n   */\n  static deleteFlow(uuid) {\n    try {\n      const filePath = normalizePath(\n        path.join(AgentFlows.flowsDir, `${uuid}.json`)\n      );\n      if (!fs.existsSync(filePath)) throw new Error(`Flow ${uuid} not found`);\n      fs.rmSync(filePath);\n      return { success: true };\n    } catch (error) {\n      console.error(\"Failed to delete flow:\", error);\n      return { success: false, error: error.message };\n    }\n  }\n\n  /**\n   * Execute a flow by UUID\n   * @param {string} uuid - The UUID of the flow to execute\n   * @param {Object} variables - Initial variables for the flow\n   * @param {Object} aibitat - The aibitat instance from the agent handler\n   * @returns {Promise<Object>} Result of flow execution\n   */\n  static async executeFlow(uuid, variables = {}, aibitat = null) {\n    const flow = AgentFlows.loadFlow(uuid);\n    if (!flow) throw new Error(`Flow ${uuid} not found`);\n    const flowExecutor = new FlowExecutor();\n    return await flowExecutor.executeFlow(flow, variables, aibitat);\n  }\n\n  /**\n   * Get all active flows as plugins that can be loaded into the agent\n   * @returns {string[]} Array of flow names in @@flow_{uuid} format\n   */\n  static activeFlowPlugins() {\n    const flows = AgentFlows.getAllFlows();\n    return Object.entries(flows)\n      .filter(([_, flow]) => flow.active !== false)\n      .map(([uuid]) => `@@flow_${uuid}`);\n  }\n\n  /**\n   * Sanitize a flow name into a valid OpenAI-compatible tool name.\n   * Must match ^[a-zA-Z0-9_-]{1,64}$\n   * @param {string} flowName - The human-readable flow name\n   * @returns {string|null} Sanitized tool name, or null if empty after sanitization\n   */\n  static sanitizeToolName(flowName) {\n    const sanitized = flowName\n      .toLowerCase()\n      .trim()\n      .replace(/\\s+/g, \"_\")\n      .replace(/[^a-z0-9_-]/g, \"\")\n      .replace(/_+/g, \"_\")\n      .replace(/^[-_]+|[-_]+$/g, \"\");\n    if (!sanitized) return null;\n    return sanitized.slice(0, 64);\n  }\n\n  /**\n   * Load a flow plugin by its UUID\n   * @param {string} uuid - The UUID of the flow to load\n   * @returns {Object|null} Plugin configuration or null if not found\n   */\n  static loadFlowPlugin(uuid) {\n    const flow = AgentFlows.loadFlow(uuid);\n    if (!flow) return null;\n\n    const startBlock = flow.config.steps?.find((s) => s.type === \"start\");\n    const variables = startBlock?.config?.variables || [];\n    const toolName = AgentFlows.sanitizeToolName(flow.name) || `flow_${uuid}`;\n\n    return {\n      name: toolName,\n      description: `Execute agent flow: ${flow.name}`,\n      plugin: (_runtimeArgs = {}) => ({\n        name: toolName,\n        description:\n          flow.config.description || `Execute agent flow: ${flow.name}`,\n        setup: (aibitat) => {\n          aibitat.function({\n            name: toolName,\n            description:\n              flow.config.description || `Execute agent flow: ${flow.name}`,\n            parameters: {\n              type: \"object\",\n              properties: variables.reduce((acc, v) => {\n                if (v.name) {\n                  acc[v.name] = {\n                    type: \"string\",\n                    description:\n                      v.description || `Value for variable ${v.name}`,\n                  };\n                }\n                return acc;\n              }, {}),\n            },\n            handler: async (args) => {\n              aibitat.introspect(`Executing flow: ${flow.name}`);\n              const result = await AgentFlows.executeFlow(uuid, args, aibitat);\n              if (!result.success) {\n                aibitat.introspect(\n                  `Flow failed: ${result.results[0]?.error || \"Unknown error\"}`\n                );\n                return `Flow execution failed: ${result.results[0]?.error || \"Unknown error\"}`;\n              }\n              aibitat.introspect(`${flow.name} completed successfully`);\n\n              // If the flow result has directOutput, return it\n              // as the aibitat result so that no other processing is done\n              if (!!result.directOutput) {\n                aibitat.skipHandleExecution = true;\n                return AgentFlows.stringifyResult(result.directOutput);\n              }\n\n              return AgentFlows.stringifyResult(result);\n            },\n          });\n        },\n      }),\n      flowName: flow.name,\n    };\n  }\n\n  /**\n   * Stringify the result of a flow execution or return the input as is\n   * @param {Object|string} input - The result to stringify\n   * @returns {string} The stringified result\n   */\n  static stringifyResult(input) {\n    return typeof input === \"object\" ? JSON.stringify(input) : String(input);\n  }\n}\n\nmodule.exports.AgentFlows = AgentFlows;\n"
  },
  {
    "path": "server/utils/agents/aibitat/error.js",
    "content": "class AIbitatError extends Error {}\n\nclass APIError extends AIbitatError {\n  constructor(message) {\n    super(message);\n  }\n}\n\n/**\n * The error when the AI provider returns an error that should be treated as something\n * that should be retried.\n */\nclass RetryError extends APIError {}\n\nmodule.exports = {\n  APIError,\n  RetryError,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/example/.gitignore",
    "content": "history/"
  },
  {
    "path": "server/utils/agents/aibitat/example/beginner-chat.js",
    "content": "// You must execute this example from within the example folder.\nconst AIbitat = require(\"../index.js\");\nconst { cli } = require(\"../plugins/cli.js\");\nconst { NodeHtmlMarkdown } = require(\"node-html-markdown\");\nrequire(\"dotenv\").config({ path: `../../../../.env.development` });\n\nconst Agent = {\n  HUMAN: \"🧑\",\n  AI: \"🤖\",\n};\n\nconst aibitat = new AIbitat({\n  provider: \"openai\",\n  model: \"gpt-4o\",\n})\n  .use(cli.plugin())\n  .function({\n    name: \"aibitat-documentations\",\n    description: \"The documentation about aibitat AI project.\",\n    parameters: {\n      type: \"object\",\n      properties: {},\n    },\n    handler: async () => {\n      return await fetch(\n        \"https://raw.githubusercontent.com/wladiston/aibitat/main/README.md\"\n      )\n        .then((res) => res.text())\n        .then((html) => NodeHtmlMarkdown.translate(html))\n        .catch((e) => {\n          console.error(e.message);\n          return \"FAILED TO FETCH\";\n        });\n    },\n  })\n  .agent(Agent.HUMAN, {\n    interrupt: \"ALWAYS\",\n    role: \"You are a human assistant.\",\n  })\n  .agent(Agent.AI, {\n    functions: [\"aibitat-documentations\"],\n  });\n\nasync function main() {\n  if (!process.env.OPEN_AI_KEY)\n    throw new Error(\n      \"This example requires a valid OPEN_AI_KEY in the env.development file\"\n    );\n  await aibitat.start({\n    from: Agent.HUMAN,\n    to: Agent.AI,\n    content: `Please, talk about the documentation of AIbitat.`,\n  });\n}\n\nmain();\n"
  },
  {
    "path": "server/utils/agents/aibitat/example/blog-post-coding.js",
    "content": "const AIbitat = require(\"../index.js\");\nconst {\n  cli,\n  webBrowsing,\n  fileHistory,\n  webScraping,\n} = require(\"../plugins/index.js\");\nrequire(\"dotenv\").config({ path: `../../../../.env.development` });\n\nconst aibitat = new AIbitat({\n  model: \"gpt-4o\",\n})\n  .use(cli.plugin())\n  .use(fileHistory.plugin())\n  .use(webBrowsing.plugin()) // Does not have introspect so will fail.\n  .use(webScraping.plugin())\n  .agent(\"researcher\", {\n    role: `You are a Researcher. Conduct thorough research to gather all necessary information about the topic \n    you are writing about. Collect data, facts, and statistics. Analyze competitor blogs for insights. \n    Provide accurate and up-to-date information that supports the blog post's content to @copywriter.`,\n    functions: [\"web-browsing\"],\n  })\n  .agent(\"copywriter\", {\n    role: `You are a Copywriter. Interpret the draft as general idea and write the full blog post using markdown, \n    ensuring it is tailored to the target audience's preferences, interests, and demographics. Apply genre-specific \n    writing techniques relevant to the author's genre. Add code examples when needed. Code must be written in \n    Typescript. Always mention references. Revisit and edit the post for clarity, coherence, and \n    correctness based on the feedback provided. Ask for feedbacks to the channel when you are done`,\n  })\n  .agent(\"pm\", {\n    role: `You are a Project Manager. Coordinate the project, ensure tasks are completed on time and within budget. \n    Communicate with team members and stakeholders.`,\n    interrupt: \"ALWAYS\",\n  })\n  .channel(\"content-team\", [\"researcher\", \"copywriter\", \"pm\"]);\n\nasync function main() {\n  if (!process.env.OPEN_AI_KEY)\n    throw new Error(\n      \"This example requires a valid OPEN_AI_KEY in the env.development file\"\n    );\n  await aibitat.start({\n    from: \"pm\",\n    to: \"content-team\",\n    content: `We have got this draft of the new blog post, let us start working on it.\n    --- BEGIN DRAFT OF POST ---\n    \n    Maui is a beautiful island in the state of Hawaii and is world-renowned for its whale watching season. Here are 2 additional things to do in Maui, HI:\n    \n    --- END DRAFT OF POST ---\n    `,\n  });\n}\n\nmain();\n"
  },
  {
    "path": "server/utils/agents/aibitat/example/websocket/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <script type=\"text/javascript\">\n      window.buttonEl;\n      window.outputEl;\n\n      function handleListen() {\n        const socket = new WebSocket(\"ws://localhost:3000/ws\");\n        window.buttonEl.setAttribute(\"hidden\", \"true\");\n\n        socket.addEventListener(\"message\", (event) => {\n          try {\n            const data = JSON.parse(event.data);\n\n            if (!data.hasOwnProperty(\"type\")) {\n              window.outputEl.innerHTML += `<p>${data.from} says to ${data.to}:  ${data.content}<p></br></br>`;\n              return;\n            }\n\n            // Handle async input loops\n            if (data?.type === \"WAITING_ON_INPUT\") {\n              // Put in time as hack to now have the prompt block DOM update.\n              setTimeout(() => {\n                console.log(\n                  \"We are waiting for feedback from the socket. Will timeout in 30s...\"\n                );\n                const feedback = window.prompt(\n                  \"We are waiting for feedback from the socket. Will timeout in 30s...\"\n                );\n                !!feedback\n                  ? socket.send(\n                      JSON.stringify({ type: \"awaitingFeedback\", feedback })\n                    )\n                  : socket.send(\n                      JSON.stringify({\n                        type: \"awaitingFeedback\",\n                        feedback: \"exit\",\n                      })\n                    );\n                return;\n              }, 800);\n            }\n          } catch (e) {\n            console.error(\"Failed to parse data\");\n          }\n        });\n\n        socket.addEventListener(\"close\", (event) => {\n          window.outputEl.innerHTML = `<p>Socket connection closed. Test is complete.<p></br></br>`;\n          window.buttonEl.removeAttribute(\"hidden\");\n        });\n      }\n\n      window.addEventListener(\"load\", function () {\n        window.buttonEl = document.getElementById(\"listen\");\n        window.outputEl = document.getElementById(\"output\");\n        window.buttonEl.addEventListener(\"click\", handleListen);\n      });\n    </script>\n  </head>\n\n  <body>\n    <button type=\"button\" id=\"listen\">Open websocket connection chat</button>\n    <div id=\"output\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "server/utils/agents/aibitat/example/websocket/websock-branding-collab.js",
    "content": "// You can only run this example from within the websocket/ directory.\n// NODE_ENV=development node websock-branding-collab.js\n// Scraping is enabled, but search requires AGENT_GSE_* keys.\n\nconst express = require(\"express\");\nconst chalk = require(\"chalk\");\nconst AIbitat = require(\"../../index.js\");\nconst {\n  websocket,\n  webBrowsing,\n  webScraping,\n} = require(\"../../plugins/index.js\");\nconst path = require(\"path\");\nconst port = 3000;\nconst app = express();\nrequire(\"@mintplex-labs/express-ws\").default(app); // load WebSockets in non-SSL mode.\nrequire(\"dotenv\").config({ path: `../../../../../.env.development` });\n\n// Debugging echo function if this is working for you.\n// app.ws('/echo', function (ws, req) {\n//   ws.on('message', function (msg) {\n//     ws.send(msg);\n//   });\n// });\n\n// Set up WSS sockets for listening.\napp.ws(\"/ws\", function (ws, _response) {\n  try {\n    ws.on(\"message\", function (msg) {\n      if (ws?.handleFeedback) ws.handleFeedback(msg);\n    });\n\n    ws.on(\"close\", function () {\n      console.log(\"Socket killed\");\n      return;\n    });\n\n    console.log(\"Socket online and waiting...\");\n    runAIbitat(ws).catch((error) => {\n      ws.send(\n        JSON.stringify({\n          from: \"AI\",\n          to: \"HUMAN\",\n          content: error.message,\n        })\n      );\n    });\n  } catch {}\n});\n\napp.all(\"*\", function (_, response) {\n  response.sendFile(path.join(__dirname, \"index.html\"));\n});\n\napp.listen(port, () => {\n  console.log(`Testing HTTP/WSS server listening at http://localhost:${port}`);\n});\n\nasync function runAIbitat(socket) {\n  console.log(chalk.blue(\"Booting AIbitat class & starting agent(s)\"));\n\n  const aibitat = new AIbitat({\n    provider: \"openai\",\n    model: \"gpt-4\",\n  })\n    .use(websocket.plugin({ socket }))\n    .use(webBrowsing.plugin())\n    .use(webScraping.plugin())\n    .agent(\"creativeDirector\", {\n      role: `You are a Creative Director. Your role is overseeing the entire branding project, ensuring\n       the client's brief is met, and maintaining consistency across all brand elements, developing the \n       brand strategy, guiding the visual and conceptual direction, and providing overall creative leadership.`,\n    })\n    .agent(\"marketResearcher\", {\n      role: `You do competitive market analysis via searching on the internet and learning about \n      comparative products and services. You can search by using keywords and phrases that you think will lead \n      to competitor research that can help find the unique angle and market of the idea.`,\n      functions: [\"web-browsing\"],\n    })\n    .agent(\"PM\", {\n      role: `You are the Project Coordinator. Your role is overseeing the project's progress, timeline, \n      and budget. Ensure effective communication and coordination among team members, client, and stakeholders. \n      Your tasks include planning and scheduling project milestones, tracking tasks, and managing any \n      risks or issues that arise.`,\n      interrupt: \"ALWAYS\",\n    })\n    .channel(\"<b>#branding</b>\", [\n      \"creativeDirector\",\n      \"marketResearcher\",\n      \"PM\",\n    ]);\n\n  await aibitat.start({\n    from: \"PM\",\n    to: \"<b>#branding</b>\",\n    content: `I have an idea for a muslim focused meetup called Chai & Vibes. \n    I want to focus on professionals that are muslim and are in their 18-30 year old range who live in big cities. \n    Does anything like this exist? How can we differentiate?`,\n  });\n}\n"
  },
  {
    "path": "server/utils/agents/aibitat/example/websocket/websock-multi-turn-chat.js",
    "content": "// You can only run this example from within the websocket/ directory.\n// NODE_ENV=development node websock-multi-turn-chat.js\n// Scraping is enabled, but search requires AGENT_GSE_* keys.\n\nconst express = require(\"express\");\nconst chalk = require(\"chalk\");\nconst AIbitat = require(\"../../index.js\");\nconst {\n  websocket,\n  webBrowsing,\n  webScraping,\n} = require(\"../../plugins/index.js\");\nconst path = require(\"path\");\nconst port = 3000;\nconst app = express();\nrequire(\"@mintplex-labs/express-ws\").default(app); // load WebSockets in non-SSL mode.\nrequire(\"dotenv\").config({ path: `../../../../../.env.development` });\n\n// Debugging echo function if this is working for you.\n// app.ws('/echo', function (ws, req) {\n//   ws.on('message', function (msg) {\n//     ws.send(msg);\n//   });\n// });\n\n// Set up WSS sockets for listening.\napp.ws(\"/ws\", function (ws, _response) {\n  try {\n    ws.on(\"message\", function (msg) {\n      if (ws?.handleFeedback) ws.handleFeedback(msg);\n    });\n\n    ws.on(\"close\", function () {\n      console.log(\"Socket killed\");\n      return;\n    });\n\n    console.log(\"Socket online and waiting...\");\n    runAIbitat(ws).catch((error) => {\n      ws.send(\n        JSON.stringify({\n          from: Agent.AI,\n          to: Agent.HUMAN,\n          content: error.message,\n        })\n      );\n    });\n  } catch {}\n});\n\napp.all(\"*\", function (_, response) {\n  response.sendFile(path.join(__dirname, \"index.html\"));\n});\n\napp.listen(port, () => {\n  console.log(`Testing HTTP/WSS server listening at http://localhost:${port}`);\n});\n\nconst Agent = {\n  HUMAN: \"🧑\",\n  AI: \"🤖\",\n};\n\nasync function runAIbitat(socket) {\n  if (!process.env.OPEN_AI_KEY)\n    throw new Error(\n      \"This example requires a valid OPEN_AI_KEY in the env.development file\"\n    );\n  console.log(chalk.blue(\"Booting AIbitat class & starting agent(s)\"));\n  const aibitat = new AIbitat({\n    provider: \"openai\",\n    model: \"gpt-4o\",\n  })\n    .use(websocket.plugin({ socket }))\n    .use(webBrowsing.plugin())\n    .use(webScraping.plugin())\n    .agent(Agent.HUMAN, {\n      interrupt: \"ALWAYS\",\n      role: \"You are a human assistant.\",\n    })\n    .agent(Agent.AI, {\n      role: \"You are a helpful ai assistant who likes to chat with the user who an also browse the web for questions it does not know or have real-time access to.\",\n      functions: [\"web-browsing\"],\n    });\n\n  await aibitat.start({\n    from: Agent.HUMAN,\n    to: Agent.AI,\n    content: `How are you doing today?`,\n  });\n}\n"
  },
  {
    "path": "server/utils/agents/aibitat/index.js",
    "content": "/* eslint-disable unused-imports/no-unused-vars */\nconst { EventEmitter } = require(\"events\");\nconst { APIError } = require(\"./error.js\");\nconst Providers = require(\"./providers/index.js\");\nconst { Telemetry } = require(\"../../../models/telemetry.js\");\nconst { v4 } = require(\"uuid\");\nconst { ToolReranker } = require(\"./utils/toolReranker.js\");\n\n/**\n * AIbitat is a class that manages the conversation between agents.\n * It is designed to solve a task with LLM.\n *\n * Guiding the chat through a graph of agents.\n */\nclass AIbitat {\n  emitter = new EventEmitter();\n\n  /**\n   * Temporary flag to skip the handleExecution function\n   * This is used to return the result of a flow execution directly to the chat\n   * without going through the handleExecution function (resulting in more LLM processing)\n   *\n   * Setting Skip execution to true will prevent any further tool calls from being executed.\n   * This is useful for flow executions that need to return a result directly to the chat but\n   * can also prevent tool-call chaining.\n   *\n   * @type {boolean}\n   */\n  skipHandleExecution = false;\n\n  provider = null;\n  defaultProvider = null;\n  defaultInterrupt;\n  maxRounds;\n  _chats;\n\n  agents = new Map();\n  channels = new Map();\n  functions = new Map();\n\n  /**\n   * Buffer for citations collected during tool execution.\n   * Citations are flushed to the frontend when the response is finalized.\n   * @type {Array<{id: string, title: string, text: string, chunkSource?: string, score?: number}>}\n   */\n  _pendingCitations = [];\n\n  /**\n   * Get the default maximum number of tools an agent can chain for a single response.\n   * @returns {number}\n   */\n  static defaultMaxToolCalls() {\n    const envMaxToolCalls = parseInt(process.env.AGENT_MAX_TOOL_CALLS, 10);\n    return !isNaN(envMaxToolCalls) && envMaxToolCalls > 0\n      ? envMaxToolCalls\n      : 10;\n  }\n\n  /**\n   * Create a new AIbitat instance.\n   * @param {Object} props - The properties for the AIbitat instance.\n   * @param {Array} props.chats - [default: []] The chat history between agents and channels.\n   * @param {string} props.interrupt - [default: \"NEVER\"] The interrupt mode for the AIbitat instance.\n   * @param {number} props.maxRounds - [default: 100] The maximum number of rounds for the AIbitat instance.\n   * @param {number} props.maxToolCalls - [default: AIbitat.defaultMaxToolCalls()] The maximum number of tools an agent can chain for a single response.\n   * @param {string} props.provider - [default: \"openai\"] The provider for the AIbitat instance.\n   * @param {Object} props.handlerProps - The handler properties for the AIbitat instance.\n   * @param {Object} rest - The rest of the properties for the AIbitat instance.\n   */\n  constructor(props = {}) {\n    const {\n      chats = [],\n      interrupt = \"NEVER\",\n      maxRounds = 100,\n      maxToolCalls = AIbitat.defaultMaxToolCalls(),\n      provider = \"openai\",\n      handlerProps = {}, // Inherited props we can spread so aibitat can access.\n      ...rest\n    } = props;\n    this._chats = chats;\n    this.defaultInterrupt = interrupt;\n    this.maxRounds = maxRounds;\n    this.maxToolCalls = maxToolCalls;\n    this.handlerProps = handlerProps;\n\n    this.defaultProvider = {\n      provider,\n      ...rest,\n    };\n    this.provider = this.defaultProvider.provider;\n    this.model = this.defaultProvider.model;\n  }\n\n  /**\n   * Get the chat history between agents and channels.\n   */\n  get chats() {\n    return this._chats;\n  }\n\n  /**\n   * Install a plugin.\n   */\n  use(plugin) {\n    plugin.setup(this);\n    return this;\n  }\n\n  /**\n   * Add citation(s) to be reported when the response is finalized.\n   * Citations are buffered and flushed with the correct message UUID.\n   * @param {{id: string, title: string, text: string, chunkSource?: string, score?: number}|Array<{id: string, title: string, text: string, chunkSource?: string, score?: number}>} citations - Citation object or array of citation objects\n   */\n  addCitation(citations) {\n    if (!citations) return;\n    if (Array.isArray(citations))\n      this._pendingCitations.push(...citations.filter(Boolean));\n    else if (typeof citations === \"object\")\n      this._pendingCitations.push(citations);\n  }\n\n  /**\n   * Flush all pending citations to the frontend with the given message UUID.\n   * Called automatically when the agent response is finalized.\n   * Note: Does not clear citations - they are cleared by chat-history plugin after persisting.\n   * @param {string} messageUuid - The UUID of the message to attach citations to\n   */\n  flushCitations(messageUuid) {\n    if (!messageUuid || this._pendingCitations.length === 0) return;\n    this.socket?.send?.(\"reportStreamEvent\", {\n      type: \"citations\",\n      uuid: messageUuid,\n      citations: this._pendingCitations,\n    });\n  }\n\n  /**\n   * Clear all pending citations. Called after citations have been persisted.\n   */\n  clearCitations() {\n    this._pendingCitations = [];\n  }\n\n  /**\n   * Add a new agent to the AIbitat.\n   *\n   * @param name\n   * @param config\n   * @returns\n   */\n  agent(name = \"\", config = {}) {\n    this.agents.set(name, config);\n    return this;\n  }\n\n  /**\n   * Add a new channel to the AIbitat.\n   *\n   * @param name\n   * @param members\n   * @param config\n   * @returns\n   */\n  channel(name = \"\", members = [\"\"], config = {}) {\n    this.channels.set(name, {\n      members,\n      ...config,\n    });\n    return this;\n  }\n\n  /**\n   * Get the specific agent configuration.\n   *\n   * @param agent The name of the agent.\n   * @throws When the agent configuration is not found.\n   * @returns The agent configuration.\n   */\n  getAgentConfig(agent = \"\") {\n    const config = this.agents.get(agent);\n    if (!config) {\n      throw new Error(`Agent configuration \"${agent}\" not found`);\n    }\n    return {\n      role: \"You are a helpful AI assistant.\",\n      //       role: `You are a helpful AI assistant.\n      // Solve tasks using your coding and language skills.\n      // In the following cases, suggest typescript code (in a typescript coding block) or shell script (in a sh coding block) for the user to execute.\n      //     1. When you need to collect info, use the code to output the info you need, for example, browse or search the web, download/read a file, print the content of a webpage or a file, get the current date/time, check the operating system. After sufficient info is printed and the task is ready to be solved based on your language skill, you can solve the task by yourself.\n      //     2. When you need to perform some task with code, use the code to perform the task and output the result. Finish the task smartly.\n      // Solve the task step by step if you need to. If a plan is not provided, explain your plan first. Be clear which step uses code, and which step uses your language skill.\n      // When using code, you must indicate the script type in the code block. The user cannot provide any other feedback or perform any other action beyond executing the code you suggest. The user can't modify your code. So do not suggest incomplete code which requires users to modify. Don't use a code block if it's not intended to be executed by the user.\n      // If you want the user to save the code in a file before executing it, put # filename: <filename> inside the code block as the first line. Don't include multiple code blocks in one response. Do not ask users to copy and paste the result. Instead, use 'print' function for the output when relevant. Check the execution result returned by the user.\n      // If the result indicates there is an error, fix the error and output the code again. Suggest the full code instead of partial code or code changes. If the error can't be fixed or if the task is not solved even after the code is executed successfully, analyze the problem, revisit your assumption, collect additional info you need, and think of a different approach to try.\n      // When you find an answer, verify the answer carefully. Include verifiable evidence in your response if possible.\n      // Reply \"TERMINATE\" when everything is done.`,\n      ...config,\n    };\n  }\n\n  /**\n   * Get the specific channel configuration.\n   *\n   * @param channel The name of the channel.\n   * @throws When the channel configuration is not found.\n   * @returns The channel configuration.\n   */\n  getChannelConfig(channel = \"\") {\n    const config = this.channels.get(channel);\n    if (!config) {\n      throw new Error(`Channel configuration \"${channel}\" not found`);\n    }\n    return {\n      maxRounds: 10,\n      role: \"\",\n      ...config,\n    };\n  }\n\n  /**\n   * Get the members of a group.\n   * @throws When the group is not defined as an array in the connections.\n   * @param node The name of the group.\n   * @returns The members of the group.\n   */\n  getGroupMembers(node = \"\") {\n    const group = this.getChannelConfig(node);\n    return group.members;\n  }\n\n  /**\n   * Triggered when a plugin, socket, or command is aborted.\n   *\n   * @param listener\n   * @returns\n   */\n  onAbort(listener = () => null) {\n    this.emitter.on(\"abort\", listener);\n    return this;\n  }\n\n  /**\n   * Abort the running of any plugins that may still be pending (Langchain summarize)\n   */\n  abort() {\n    this.emitter.emit(\"abort\", null, this);\n  }\n\n  /**\n   * Triggered when a chat is terminated. After this, the chat can't be continued.\n   *\n   * @param listener\n   * @returns\n   */\n  onTerminate(listener = () => null) {\n    this.emitter.on(\"terminate\", listener);\n    return this;\n  }\n\n  /**\n   * Terminate the chat. After this, the chat can't be continued.\n   *\n   * @param node Last node to chat with\n   */\n  terminate(node = \"\") {\n    this.emitter.emit(\"terminate\", node, this);\n  }\n\n  /**\n   * Triggered when a chat is interrupted by a node.\n   *\n   * @param listener\n   * @returns\n   */\n  onInterrupt(listener = () => null) {\n    this.emitter.on(\"interrupt\", listener);\n    return this;\n  }\n\n  /**\n   * Interruption the chat.\n   *\n   * @param route The nodes that participated in the interruption.\n   * @returns\n   */\n  interrupt(route) {\n    this._chats.push({\n      ...route,\n      state: \"interrupt\",\n    });\n    this.emitter.emit(\"interrupt\", route, this);\n  }\n\n  /**\n   * Triggered when a message is added to the chat history.\n   * This can either be the first message or a reply to a message.\n   *\n   * @param listener\n   * @returns\n   */\n  onMessage(listener = (chat) => null) {\n    this.emitter.on(\"message\", listener);\n    return this;\n  }\n\n  /**\n   * Register a new successful message in the chat history.\n   * This will trigger the `onMessage` event.\n   *\n   * @param message\n   */\n  newMessage(message) {\n    const chat = {\n      ...message,\n      state: \"success\",\n    };\n\n    this._chats.push(chat);\n    this.emitter.emit(\"message\", chat, this);\n  }\n\n  /**\n   * Triggered when an error occurs during the chat.\n   *\n   * @param listener\n   * @returns\n   */\n  onError(\n    listener = (\n      /**\n       * The error that occurred.\n       *\n       * Native errors are:\n       * - `APIError`\n       * - `AuthorizationError`\n       * - `UnknownError`\n       * - `RateLimitError`\n       * - `ServerError`\n       */\n      error = null,\n      /**\n       * The message when the error occurred.\n       */\n      // eslint-disable-next-line\n      {}\n    ) => null\n  ) {\n    this.emitter.on(\"replyError\", listener);\n    return this;\n  }\n\n  /**\n   * Register an error in the chat history.\n   * This will trigger the `onError` event.\n   *\n   * @param route\n   * @param error\n   */\n  newError(route, error) {\n    const chat = {\n      ...route,\n      content: error instanceof Error ? error.message : String(error),\n      state: \"error\",\n    };\n    this._chats.push(chat);\n    this.emitter.emit(\"replyError\", error, chat);\n  }\n\n  /**\n   * Triggered when a chat is interrupted by a node.\n   *\n   * @param listener\n   * @returns\n   */\n  onStart(listener = (chat, aibitat) => null) {\n    this.emitter.on(\"start\", listener);\n    return this;\n  }\n\n  /**\n   * Start a new chat.\n   *\n   * @param message The message to start the chat.\n   */\n  async start(message) {\n    // register the message in the chat history\n    this.newMessage(message);\n    this.emitter.emit(\"start\", message, this);\n\n    // ask the node to reply\n    await this.chat({\n      to: message.from,\n      from: message.to,\n    });\n\n    return this;\n  }\n\n  /**\n   * Recursively chat between two nodes.\n   *\n   * @param route\n   * @param keepAlive Whether to keep the chat alive.\n   */\n  async chat(route, keepAlive = true) {\n    // check if the message is for a group\n    // if it is, select the next node to chat with from the group\n    // and then ask them to reply.\n    if (this.channels.get(route.from)) {\n      // select a node from the group\n      let nextNode;\n      try {\n        nextNode = await this.selectNext(route.from);\n      } catch (error) {\n        if (error instanceof APIError) {\n          return this.newError({ from: route.from, to: route.to }, error);\n        }\n        throw error;\n      }\n\n      if (!nextNode) {\n        // TODO: should it throw an error or keep the chat alive when there is no node to chat with in the group?\n        // maybe it should wrap up the chat and reply to the original node\n        // For now, it will terminate the chat\n        this.terminate(route.from);\n        return;\n      }\n\n      const nextChat = {\n        from: nextNode,\n        to: route.from,\n      };\n\n      if (this.shouldAgentInterrupt(nextNode)) {\n        this.interrupt(nextChat);\n        return;\n      }\n\n      // get chats only from the group's nodes\n      const history = this.getHistory({ to: route.from });\n      const group = this.getGroupMembers(route.from);\n      const rounds = history.filter((chat) => group.includes(chat.from)).length;\n\n      const { maxRounds } = this.getChannelConfig(route.from);\n      if (rounds >= maxRounds) {\n        this.terminate(route.to);\n        return;\n      }\n\n      await this.chat(nextChat);\n      return;\n    }\n\n    // If it's a direct message, reply to the message\n    let reply = \"\";\n    try {\n      reply = await this.reply(route);\n    } catch (error) {\n      if (error instanceof APIError) {\n        return this.newError({ from: route.from, to: route.to }, error);\n      }\n      throw error;\n    }\n\n    if (\n      reply === \"TERMINATE\" ||\n      this.hasReachedMaximumRounds(route.from, route.to)\n    ) {\n      this.terminate(route.to);\n      return;\n    }\n\n    const newChat = { to: route.from, from: route.to };\n\n    if (\n      reply === \"INTERRUPT\" ||\n      (this.agents.get(route.to) && this.shouldAgentInterrupt(route.to))\n    ) {\n      this.interrupt(newChat);\n      return;\n    }\n\n    if (keepAlive) {\n      // keep the chat alive by replying to the other node\n      await this.chat(newChat, true);\n    }\n  }\n\n  /**\n   * Check if the agent should interrupt the chat based on its configuration.\n   *\n   * @param agent\n   * @returns {boolean} Whether the agent should interrupt the chat.\n   */\n  shouldAgentInterrupt(agent = \"\") {\n    const config = this.getAgentConfig(agent);\n    return this.defaultInterrupt === \"ALWAYS\" || config.interrupt === \"ALWAYS\";\n  }\n\n  /**\n   * Select the next node to chat with from a group. The node will be selected based on the history of chats.\n   * It will select the node that has not reached the maximum number of rounds yet and has not chatted with the channel in the last round.\n   * If it could not determine the next node, it will return a random node.\n   *\n   * @param channel The name of the group.\n   * @returns The name of the node to chat with.\n   */\n  async selectNext(channel = \"\") {\n    // get all members of the group\n    const nodes = this.getGroupMembers(channel);\n    const channelConfig = this.getChannelConfig(channel);\n\n    // TODO: move this to when the group is created\n    // warn if the group is underpopulated\n    if (nodes.length < 3) {\n      console.warn(\n        `- Group (${channel}) is underpopulated with ${nodes.length} agents. Direct communication would be more efficient.`\n      );\n    }\n\n    // get the nodes that have not reached the maximum number of rounds\n    const availableNodes = nodes.filter(\n      (node) => !this.hasReachedMaximumRounds(channel, node)\n    );\n\n    // remove the last node that chatted with the channel, so it doesn't chat again\n    const lastChat = this._chats.filter((c) => c.to === channel).at(-1);\n    if (lastChat) {\n      const index = availableNodes.indexOf(lastChat.from);\n      if (index > -1) {\n        availableNodes.splice(index, 1);\n      }\n    }\n\n    // TODO: what should it do when there is no node to chat with?\n    if (!availableNodes.length) return;\n\n    // get the provider that will be used for the channel\n    // if the channel has a provider, use that otherwise\n    // use the GPT-4 because it has a better reasoning\n    const provider = this.getProviderForConfig({\n      // @ts-expect-error\n      model: \"gpt-4\",\n      ...this.defaultProvider,\n      ...channelConfig,\n    });\n    provider.attachHandlerProps(this.handlerProps);\n\n    const history = this.getHistory({ to: channel });\n\n    // build the messages to send to the provider\n    const messages = [\n      {\n        role: \"system\",\n        content: channelConfig.role,\n      },\n      {\n        role: \"user\",\n        content: `You are in a role play game. The following roles are available:\n${availableNodes.map((node) => `@${node}: ${this.getAgentConfig(node).role}`).join(\"\\n\")}.\n\nRead the following conversation.\n\nCHAT HISTORY\n${history.map((c) => `@${c.from}: ${c.content}`).join(\"\\n\")}\n\nThen select the next role from that is going to speak next.\nOnly return the role.\n`,\n      },\n    ];\n\n    // ask the provider to select the next node to chat with\n    // and remove the @ from the response\n    const { result } = await provider.complete(messages);\n    const name = result?.replace(/^@/g, \"\");\n    if (this.agents.get(name)) return name;\n\n    // if the name is not in the nodes, return a random node\n    return availableNodes[Math.floor(Math.random() * availableNodes.length)];\n  }\n\n  /**\n   *\n   * @param {string} pluginName this name of the plugin being called\n   * @returns string of the plugin to be called compensating for children denoted by # in the string.\n   * eg: sql-agent:list-database-connections\n   * or is a custom plugin\n   * eg: @@custom-plugin-name\n   */\n  #parseFunctionName(pluginName = \"\") {\n    if (!pluginName.includes(\"#\") && !pluginName.startsWith(\"@@\"))\n      return pluginName;\n    if (pluginName.startsWith(\"@@\")) return pluginName.replace(\"@@\", \"\");\n    return pluginName.split(\"#\")[1];\n  }\n\n  /**\n   * Extract the user's prompt from the messages array for tool reranking.\n   * Gets the content of the last user message.\n   * @param {Array} messages - Array of chat messages\n   * @returns {string|null} The user's prompt or null if not found\n   */\n  #extractUserPrompt(messages) {\n    if (!messages || !Array.isArray(messages)) return null;\n\n    // Find the last user message\n    for (let i = messages.length - 1; i >= 0; i--) {\n      const msg = messages[i];\n      if (msg.role === \"user\" && msg.content) {\n        return typeof msg.content === \"string\"\n          ? msg.content\n          : JSON.stringify(msg.content);\n      }\n    }\n    return null;\n  }\n\n  /**\n   * Check if the chat has reached the maximum number of rounds.\n   */\n  hasReachedMaximumRounds(from = \"\", to = \"\") {\n    return this.getHistory({ from, to }).length >= this.maxRounds;\n  }\n\n  /**\n   * Get the chat history between two nodes or all chats to/from a node.\n   *\n   * @param route\n   * @returns\n   */\n  getOrFormatNodeChatHistory(route) {\n    if (this.channels.get(route.to)) {\n      return [\n        {\n          role: \"user\",\n          content: `You are in a whatsapp group. Read the following conversation and then reply.\nDo not add introduction or conclusion to your reply because this will be a continuous conversation. Don't introduce yourself.\n\nCHAT HISTORY\n${this.getHistory({ to: route.to })\n  .map((c) => `@${c.from}: ${c.content}`)\n  .join(\"\\n\")}\n\n@${route.from}:`,\n        },\n      ];\n    }\n\n    // This is normal chat between user<->agent\n    // Include attachments if present (for vision/multimodal support)\n    return this.getHistory(route).map((c) => {\n      const message = {\n        content: c.content,\n        role: c.from === route.to ? \"user\" : \"assistant\",\n      };\n      // Pass attachments through for user messages that have them\n      if (\n        c.attachments &&\n        c.attachments.length > 0 &&\n        message.role === \"user\"\n      ) {\n        message.attachments = c.attachments;\n      }\n      return message;\n    });\n  }\n\n  /**\n   * Ask the for the AI provider to generate a reply to the chat.\n   * This will load the functions that the node can call and the chat history.\n   * Then before calling the provider, it will check if the provider supports agent streaming.\n   * If it does, it will call the provider asynchronously (streaming).\n   * Otherwise, it will call the provider synchronously (non-streaming).\n   * `.supportsAgentStreaming` is used to determine if the provider supports agent streaming on the respective provider.\n   *\n   * @param route.to The node that sent the chat.\n   * @param route.from The node that will reply to the chat.\n   */\n  async reply(route) {\n    const fromConfig = this.getAgentConfig(route.from);\n    const chatHistory = this.getOrFormatNodeChatHistory(route);\n\n    // Fetch fresh parsed file context and inject into the last user message\n    if (this.fetchParsedFileContext) {\n      const parsedContext = await this.fetchParsedFileContext();\n      if (parsedContext) {\n        // Find the last user message and append context to it\n        for (let i = chatHistory.length - 1; i >= 0; i--) {\n          if (chatHistory[i].role === \"user\") {\n            chatHistory[i] = {\n              ...chatHistory[i],\n              content: chatHistory[i].content + parsedContext,\n            };\n            break;\n          }\n        }\n      }\n    }\n\n    const messages = [\n      {\n        content: fromConfig.role,\n        role: \"system\",\n      },\n      ...chatHistory,\n    ];\n\n    // get the functions that the node can call\n    let functions = fromConfig.functions\n      ?.map((name) => this.functions.get(this.#parseFunctionName(name)))\n      .filter((a) => !!a);\n\n    // Rerank tools based on user prompt if enabled\n    if (ToolReranker.isEnabled() && functions?.length) {\n      const toolReranker = new ToolReranker();\n      const userPrompt = this.#extractUserPrompt(messages);\n      if (userPrompt)\n        functions = await toolReranker.rerank(userPrompt, functions);\n    } else {\n      if (functions?.length > ToolReranker.defaultTopN) {\n        this.handlerProps.log?.(\n          `\n\n\\x1b[44m[HINT]\\x1b[0m: You are injecting \\x1b[0;93m${functions.length} tools\\x1b[0m into every request.\nConsider enabling \\x1b[0;93mIntelligent Skill Selection\\x1b[0m to reduce token usage from tool call bloat by up to \\x1b[0;93m80% per request\\x1b[0m.\nhttps://docs.anythingllm.com/agent/intelligent-tool-selection\n\n`\n        );\n      }\n    }\n\n    const provider = this.getProviderForConfig({\n      ...this.defaultProvider,\n      ...fromConfig,\n    });\n    provider.attachHandlerProps(this.handlerProps);\n\n    let content;\n    if (provider.supportsAgentStreaming) {\n      this.handlerProps.log?.(\n        \"[DEBUG] Provider supports agent streaming - will use async execution!\"\n      );\n      content = await this.handleAsyncExecution(\n        provider,\n        messages,\n        functions,\n        route.from\n      );\n    } else {\n      this.handlerProps.log?.(\n        \"[DEBUG] Provider does not support agent streaming - will use synchronous execution!\"\n      );\n      content = await this.handleExecution(\n        provider,\n        messages,\n        functions,\n        route.from\n      );\n    }\n\n    // Store the active provider so plugins can access usage metrics\n    this.provider = provider;\n    this.newMessage({ ...route, content });\n    return content;\n  }\n\n  /**\n   * Wrapper for provider calls that catches errors and converts them to APIError.\n   * This ensures provider errors are properly surfaced to the user instead of crashing.\n   *\n   * @param {Function} providerCall - Async function that calls the provider\n   * @returns {Promise<any>} - The result of the provider call\n   * @throws {APIError} - If the provider call fails\n   */\n  async #safeProviderCall(providerCall) {\n    try {\n      return await providerCall();\n    } catch (error) {\n      console.error(`[AIbitat] Provider error: ${error.message}`, {\n        hide_meta: true,\n      });\n      throw new APIError(`The agent model failed to respond: ${error.message}`);\n    }\n  }\n\n  /**\n   * Handle the async (streaming) execution of the provider\n   * with tool calls.\n   *\n   * @param provider\n   * @param messages\n   * @param functions\n   * @param byAgent\n   *\n   * @returns {Promise<string>}\n   */\n  async handleAsyncExecution(\n    provider,\n    messages = [],\n    functions = [],\n    byAgent = null,\n    depth = 0\n  ) {\n    const eventHandler = (type, data) => {\n      this?.socket?.send(type, data);\n    };\n\n    /** @type {{ functionCall: { name: string, arguments: string }, textResponse: string }} */\n    const completionStream = await this.#safeProviderCall(() =>\n      provider.stream(messages, functions, eventHandler)\n    );\n\n    if (completionStream.functionCall) {\n      if (depth >= this.maxToolCalls) {\n        this.handlerProps?.log?.(\n          `[warning]: Maximum tool call limit (${this.maxToolCalls}) reached. Making final response without tools.`\n        );\n        this?.introspect?.(\n          `Maximum tool call limit (${this.maxToolCalls}) reached. Generating a final response from what I have so far.`\n        );\n\n        const finalStream = await this.#safeProviderCall(() =>\n          provider.stream(messages, [], eventHandler)\n        );\n        const finalResponse =\n          finalStream?.textResponse ||\n          \"I reached the maximum number of tool calls allowed for a single response. Here is what I have so far based on the tools I was able to run.\";\n        return finalResponse;\n      }\n\n      const { name, arguments: args } = completionStream.functionCall;\n      const fn = this.functions.get(name);\n\n      if (!fn) {\n        return await this.handleAsyncExecution(\n          provider,\n          [\n            ...messages,\n            {\n              name,\n              role: \"function\",\n              content: `Function \"${name}\" not found. Try again.`,\n              originalFunctionCall: completionStream.functionCall,\n            },\n          ],\n          functions,\n          byAgent,\n          depth + 1\n        );\n      }\n\n      fn.caller = byAgent || \"agent\";\n\n      if (provider?.verbose) {\n        this?.introspect?.(\n          `${fn.caller} is executing \\`${name}\\` tool ${JSON.stringify(args, null, 2)}`\n        );\n      }\n\n      this.handlerProps?.log?.(\n        `[debug]: ${fn.caller} is attempting to call \\`${name}\\` tool ${JSON.stringify(args, null, 2)}`\n      );\n\n      const result = await fn.handler(args);\n      Telemetry.sendTelemetry(\"agent_tool_call\", { tool: name }, null, true);\n\n      /**\n       * If the tool call has direct output enabled, return the result directly to the chat\n       * without any further processing and no further tool calls will be run.\n       * For streaming, we need to return the result directly to the chat via the event handler\n       * or else no response will be sent to the chat.\n       */\n      if (this.skipHandleExecution) {\n        this.skipHandleExecution = false;\n        this?.introspect?.(\n          `The tool call has direct output enabled! The result will be returned directly to the chat without any further processing and no further tool calls will be run.`\n        );\n        this?.introspect?.(`Tool use completed.`);\n        this.handlerProps?.log?.(\n          `${fn.caller} tool call resulted in direct output! Returning raw result as string. NO MORE TOOL CALLS WILL BE EXECUTED.`\n        );\n        const directOutputUUID = completionStream?.uuid || v4();\n        eventHandler?.(\"reportStreamEvent\", {\n          type: \"fullTextResponse\",\n          uuid: directOutputUUID,\n          content: result,\n        });\n        eventHandler?.(\"reportStreamEvent\", {\n          type: \"usageMetrics\",\n          uuid: directOutputUUID,\n          metrics: provider.getUsage(),\n        });\n        this?.flushCitations?.(directOutputUUID);\n        return result;\n      }\n\n      return await this.handleAsyncExecution(\n        provider,\n        [\n          ...messages,\n          {\n            name,\n            role: \"function\",\n            content: result,\n            originalFunctionCall: completionStream.functionCall,\n          },\n        ],\n        functions,\n        byAgent,\n        depth + 1\n      );\n    }\n\n    const responseUuid = completionStream?.uuid || v4();\n    eventHandler?.(\"reportStreamEvent\", {\n      type: \"usageMetrics\",\n      uuid: responseUuid,\n      metrics: provider.getUsage(),\n    });\n    this?.flushCitations?.(responseUuid);\n    return completionStream?.textResponse;\n  }\n\n  /**\n   * Handle the synchronous (non-streaming) execution of the provider\n   * with tool calls.\n   *\n   * @param provider\n   * @param messages\n   * @param functions\n   * @param byAgent\n   * @param depth\n   * @param msgUUID - The message UUID to use for event correlation (created at depth=0)\n   *\n   * @returns {Promise<string>}\n   */\n  async handleExecution(\n    provider,\n    messages = [],\n    functions = [],\n    byAgent = null,\n    depth = 0,\n    msgUUID = null\n  ) {\n    // Create a stable UUID at the start of execution for event correlation\n    if (!msgUUID) msgUUID = v4();\n    const eventHandler = (type, data) => {\n      this?.socket?.send(type, data);\n    };\n\n    // get the chat completion\n    const completion = await this.#safeProviderCall(() =>\n      provider.complete(messages, functions)\n    );\n\n    if (completion.functionCall) {\n      if (depth >= this.maxToolCalls) {\n        this.handlerProps?.log?.(\n          `[warning]: Maximum tool call limit (${this.maxToolCalls}) reached. Making final response without tools.`\n        );\n        this?.introspect?.(\n          `Maximum tool call limit (${this.maxToolCalls}) reached. Generating a final response from what I have so far.`\n        );\n\n        const finalCompletion = await this.#safeProviderCall(() =>\n          provider.complete(messages, [])\n        );\n        eventHandler?.(\"reportStreamEvent\", {\n          type: \"usageMetrics\",\n          uuid: msgUUID,\n          metrics: provider.getUsage(),\n        });\n        this?.flushCitations?.(msgUUID);\n        return (\n          finalCompletion?.textResponse ||\n          \"I reached the maximum number of tool calls allowed for a single response. Here is what I have so far based on the tools I was able to run.\"\n        );\n      }\n\n      const { name, arguments: args } = completion.functionCall;\n      const fn = this.functions.get(name);\n\n      if (!fn) {\n        return await this.handleExecution(\n          provider,\n          [\n            ...messages,\n            {\n              name,\n              role: \"function\",\n              content: `Function \"${name}\" not found. Try again.`,\n              originalFunctionCall: completion.functionCall,\n            },\n          ],\n          functions,\n          byAgent,\n          depth + 1,\n          msgUUID\n        );\n      }\n\n      fn.caller = byAgent || \"agent\";\n\n      if (provider?.verbose) {\n        this?.introspect?.(\n          `[debug]: ${fn.caller} is attempting to call \\`${name}\\` tool`\n        );\n      }\n\n      this.handlerProps?.log?.(\n        `[debug]: ${fn.caller} is attempting to call \\`${name}\\` tool`\n      );\n\n      const result = await fn.handler(args);\n      Telemetry.sendTelemetry(\"agent_tool_call\", { tool: name }, null, true);\n\n      if (this.skipHandleExecution) {\n        this.skipHandleExecution = false;\n        this?.introspect?.(\n          `The tool call has direct output enabled! The result will be returned directly to the chat without any further processing and no further tool calls will be run.`\n        );\n        this?.introspect?.(`Tool use completed.`);\n        this.handlerProps?.log?.(\n          `${fn.caller} tool call resulted in direct output! Returning raw result as string. NO MORE TOOL CALLS WILL BE EXECUTED.`\n        );\n        eventHandler?.(\"reportStreamEvent\", {\n          type: \"usageMetrics\",\n          uuid: msgUUID,\n          metrics: provider.getUsage(),\n        });\n        this?.flushCitations?.(msgUUID);\n        return result;\n      }\n\n      return await this.handleExecution(\n        provider,\n        [\n          ...messages,\n          {\n            name,\n            role: \"function\",\n            content: result,\n            originalFunctionCall: completion.functionCall,\n          },\n        ],\n        functions,\n        byAgent,\n        depth + 1,\n        msgUUID\n      );\n    }\n\n    eventHandler?.(\"reportStreamEvent\", {\n      type: \"usageMetrics\",\n      uuid: msgUUID,\n      metrics: provider.getUsage(),\n    });\n    this?.flushCitations?.(msgUUID);\n    return completion?.textResponse;\n  }\n\n  /**\n   * Continue the chat from the last interruption.\n   * If the last chat was not an interruption, it will throw an error.\n   * Provide a feedback where it was interrupted if you want to.\n   *\n   * @param feedback The feedback to the interruption if any.\n   * @param attachments Optional attachments (images) to include with the feedback.\n   * @returns\n   */\n  async continue(feedback, attachments = []) {\n    const lastChat = this._chats.at(-1);\n    if (!lastChat || lastChat.state !== \"interrupt\") {\n      throw new Error(\"No chat to continue\");\n    }\n\n    // remove the last chat's that was interrupted\n    this._chats.pop();\n\n    const { from, to } = lastChat;\n\n    if (this.hasReachedMaximumRounds(from, to)) {\n      throw new Error(\"Maximum rounds reached\");\n    }\n\n    if (feedback) {\n      const message = {\n        from,\n        to,\n        content: feedback,\n        ...(attachments?.length > 0 ? { attachments } : {}),\n      };\n\n      // register the message in the chat history\n      this.newMessage(message);\n\n      // ask the node to reply\n      await this.chat({\n        to: message.from,\n        from: message.to,\n      });\n    } else {\n      await this.chat({ from, to });\n    }\n\n    return this;\n  }\n\n  /**\n   * Retry the last chat that threw an error.\n   * If the last chat was not an error, it will throw an error.\n   */\n  async retry() {\n    const lastChat = this._chats.at(-1);\n    if (!lastChat || lastChat.state !== \"error\") {\n      throw new Error(\"No chat to retry\");\n    }\n\n    // remove the last chat's that threw an error\n    // eslint-disable-next-line\n    const { from, to } = this?._chats?.pop();\n\n    await this.chat({ from, to });\n    return this;\n  }\n\n  /**\n   * Get the chat history between two nodes or all chats to/from a node.\n   */\n  getHistory({ from, to }) {\n    return this._chats.filter((chat) => {\n      const isSuccess = chat.state === \"success\";\n\n      // return all chats to the node\n      if (!from) {\n        return isSuccess && chat.to === to;\n      }\n\n      // get all chats from the node\n      if (!to) {\n        return isSuccess && chat.from === from;\n      }\n\n      // check if the chat is between the two nodes\n      const hasSent = chat.from === from && chat.to === to;\n      const hasReceived = chat.from === to && chat.to === from;\n      const mutual = hasSent || hasReceived;\n\n      return isSuccess && mutual;\n    });\n  }\n\n  /**\n   * Get provider based on configurations.\n   * If the provider is a string, it will return the default provider for that string.\n   *\n   * @param config The provider configuration.\n   * @returns {Providers.OpenAIProvider} The provider instance.\n   */\n  getProviderForConfig(config) {\n    if (typeof config.provider === \"object\") return config.provider;\n\n    switch (config.provider) {\n      case \"openai\":\n        return new Providers.OpenAIProvider({ model: config.model });\n      case \"anthropic\":\n        return new Providers.AnthropicProvider({ model: config.model });\n      case \"lmstudio\":\n        return new Providers.LMStudioProvider({ model: config.model });\n      case \"ollama\":\n        return new Providers.OllamaProvider({ model: config.model });\n      case \"groq\":\n        return new Providers.GroqProvider({ model: config.model });\n      case \"togetherai\":\n        return new Providers.TogetherAIProvider({ model: config.model });\n      case \"azure\":\n        return new Providers.AzureOpenAiProvider({ model: config.model });\n      case \"koboldcpp\":\n        return new Providers.KoboldCPPProvider({});\n      case \"localai\":\n        return new Providers.LocalAIProvider({ model: config.model });\n      case \"openrouter\":\n        return new Providers.OpenRouterProvider({ model: config.model });\n      case \"mistral\":\n        return new Providers.MistralProvider({ model: config.model });\n      case \"generic-openai\":\n        return new Providers.GenericOpenAiProvider({ model: config.model });\n      case \"perplexity\":\n        return new Providers.PerplexityProvider({ model: config.model });\n      case \"textgenwebui\":\n        return new Providers.TextWebGenUiProvider({});\n      case \"bedrock\":\n        return new Providers.AWSBedrockProvider({});\n      case \"fireworksai\":\n        return new Providers.FireworksAIProvider({ model: config.model });\n      case \"nvidia-nim\":\n        return new Providers.NvidiaNimProvider({ model: config.model });\n      case \"moonshotai\":\n        return new Providers.MoonshotAiProvider({ model: config.model });\n      case \"deepseek\":\n        return new Providers.DeepSeekProvider({ model: config.model });\n      case \"litellm\":\n        return new Providers.LiteLLMProvider({ model: config.model });\n      case \"apipie\":\n        return new Providers.ApiPieProvider({ model: config.model });\n      case \"xai\":\n        return new Providers.XAIProvider({ model: config.model });\n      case \"zai\":\n        return new Providers.ZAIProvider({ model: config.model });\n      case \"novita\":\n        return new Providers.NovitaProvider({ model: config.model });\n      case \"ppio\":\n        return new Providers.PPIOProvider({ model: config.model });\n      case \"gemini\":\n        return new Providers.GeminiProvider({ model: config.model });\n      case \"dpais\":\n        return new Providers.DellProAiStudioProvider({ model: config.model });\n      case \"cometapi\":\n        return new Providers.CometApiProvider({ model: config.model });\n      case \"foundry\":\n        return new Providers.FoundryProvider({ model: config.model });\n      case \"giteeai\":\n        return new Providers.GiteeAIProvider({ model: config.model });\n      case \"cohere\":\n        return new Providers.CohereProvider({ model: config.model });\n      case \"docker-model-runner\":\n        return new Providers.DockerModelRunnerProvider({ model: config.model });\n      case \"privatemode\":\n        return new Providers.PrivatemodeProvider({ model: config.model });\n      case \"sambanova\":\n        return new Providers.SambaNovaProvider({ model: config.model });\n      case \"lemonade\":\n        return new Providers.LemonadeProvider({ model: config.model });\n      default:\n        throw new Error(\n          `Unknown provider: ${config.provider}. Please use a valid provider.`\n        );\n    }\n  }\n\n  /**\n   * Register a new function to be called by the AIbitat agents.\n   * You are also required to specify the which node can call the function.\n   * @param functionConfig The function configuration.\n   */\n  function(functionConfig) {\n    this.functions.set(functionConfig.name, functionConfig);\n    return this;\n  }\n}\n\nmodule.exports = AIbitat;\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/chat-history.js",
    "content": "const { WorkspaceChats } = require(\"../../../../models/workspaceChats\");\n\n/**\n * Plugin to save chat history to AnythingLLM DB.\n */\nconst chatHistory = {\n  name: \"chat-history\",\n  startupConfig: {\n    params: {},\n  },\n  plugin: function () {\n    return {\n      name: this.name,\n      setup: function (aibitat) {\n        aibitat.onMessage(async () => {\n          try {\n            const lastResponses = aibitat.chats.slice(-2);\n            if (lastResponses.length !== 2) return;\n            const [prev, last] = lastResponses;\n\n            // We need a full conversation reply with prev being from\n            // the USER and the last being from anyone other than the user.\n            if (prev.from !== \"USER\" || last.from === \"USER\") return;\n\n            // Extract attachments from user message if present\n            const attachments = prev.attachments || [];\n\n            // If we have a post-reply flow we should save the chat using this special flow\n            // so that post save cleanup and other unique properties can be run as opposed to regular chat.\n            if (aibitat.hasOwnProperty(\"_replySpecialAttributes\")) {\n              await this._storeSpecial(aibitat, {\n                prompt: prev.content,\n                response: last.content,\n                attachments,\n                options: aibitat._replySpecialAttributes,\n              });\n              delete aibitat._replySpecialAttributes;\n              return;\n            }\n\n            await this._store(aibitat, {\n              prompt: prev.content,\n              response: last.content,\n              attachments,\n            });\n          } catch {}\n        });\n      },\n      _store: async function (\n        aibitat,\n        { prompt, response, attachments = [] } = {}\n      ) {\n        const invocation = aibitat.handlerProps.invocation;\n        const metrics = aibitat.provider?.getUsage?.() ?? {};\n        const citations = aibitat._pendingCitations ?? [];\n        await WorkspaceChats.new({\n          workspaceId: Number(invocation.workspace_id),\n          prompt,\n          response: {\n            text: response,\n            sources: citations,\n            type: \"chat\",\n            attachments,\n            metrics,\n          },\n          user: { id: invocation?.user_id || null },\n          threadId: invocation?.thread_id || null,\n        });\n        aibitat.clearCitations?.();\n      },\n      _storeSpecial: async function (\n        aibitat,\n        { prompt, response, attachments = [], options = {} } = {}\n      ) {\n        const invocation = aibitat.handlerProps.invocation;\n        const metrics = aibitat.provider?.getUsage?.() ?? {};\n        const citations = aibitat._pendingCitations ?? [];\n        const existingSources = options?.sources ?? [];\n        await WorkspaceChats.new({\n          workspaceId: Number(invocation.workspace_id),\n          prompt,\n          response: {\n            sources: [...existingSources, ...citations],\n            // when we have a _storeSpecial called the options param can include a storedResponse() function\n            // that will override the text property to store extra information in, depending on the special type of chat.\n            text: options.hasOwnProperty(\"storedResponse\")\n              ? options.storedResponse(response)\n              : response,\n            type: options?.saveAsType ?? \"chat\",\n            attachments,\n            metrics,\n          },\n          user: { id: invocation?.user_id || null },\n          threadId: invocation?.thread_id || null,\n        });\n        aibitat.clearCitations?.();\n        options?.postSave();\n      },\n    };\n  },\n};\n\nmodule.exports = { chatHistory };\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/cli.js",
    "content": "// Plugin CAN ONLY BE USE IN DEVELOPMENT.\nconst { input } = require(\"@inquirer/prompts\");\nconst chalk = require(\"chalk\");\n\n/**\n * Command-line Interface plugin. It prints the messages on the console and asks for feedback\n * while the conversation is running in the background.\n */\nconst cli = {\n  name: \"cli\",\n  startupConfig: {\n    params: {},\n  },\n  plugin: function ({ simulateStream = true } = {}) {\n    return {\n      name: this.name,\n      setup(aibitat) {\n        let printing = [];\n\n        aibitat.onError(async (error) => {\n          let errorMessage =\n            error?.message || \"An error occurred while running the agent.\";\n          console.error(chalk.red(`   error: ${errorMessage}`), error);\n        });\n\n        aibitat.onStart(() => {\n          console.log();\n          console.log(\"🚀 starting chat ...\\n\");\n          printing = [Promise.resolve()];\n        });\n\n        aibitat.onMessage(async (message) => {\n          const next = new Promise(async (resolve) => {\n            await Promise.all(printing);\n            await this.print(message, simulateStream);\n            resolve();\n          });\n          printing.push(next);\n        });\n\n        aibitat.onTerminate(async () => {\n          await Promise.all(printing);\n          console.log(\"🚀 chat finished\");\n        });\n\n        aibitat.onInterrupt(async (node) => {\n          await Promise.all(printing);\n          const feedback = await this.askForFeedback(node);\n          // Add an extra line after the message\n          console.log();\n\n          if (feedback === \"exit\") {\n            console.log(\"🚀 chat finished\");\n            return process.exit(0);\n          }\n\n          await aibitat.continue(feedback);\n        });\n      },\n\n      /**\n   * Print a message on the terminal\n   *\n   * @param message\n   * // message Type { from: string; to: string; content?: string } & {\n      state: 'loading' | 'error' | 'success' | 'interrupt'\n    }\n   * @param simulateStream\n   */\n      print: async function (message = {}, simulateStream = true) {\n        const replying = chalk.dim(`(to ${message.to})`);\n        const reference = `${chalk.magenta(\"✎\")} ${chalk.bold(\n          message.from\n        )} ${replying}:`;\n\n        if (!simulateStream) {\n          console.log(reference);\n          console.log(message.content);\n          // Add an extra line after the message\n          console.log();\n          return;\n        }\n\n        process.stdout.write(`${reference}\\n`);\n\n        // Emulate streaming by breaking the cached response into chunks\n        const chunks = message.content?.split(\" \") || [];\n        const stream = new ReadableStream({\n          async start(controller) {\n            for (const chunk of chunks) {\n              const bytes = new TextEncoder().encode(chunk + \" \");\n              controller.enqueue(bytes);\n              await new Promise((r) =>\n                setTimeout(\n                  r,\n                  // get a random number between 10ms and 50ms to simulate a random delay\n                  Math.floor(Math.random() * 40) + 10\n                )\n              );\n            }\n            controller.close();\n          },\n        });\n\n        // Stream the response to the chat\n        for await (const chunk of stream) {\n          process.stdout.write(new TextDecoder().decode(chunk));\n        }\n\n        // Add an extra line after the message\n        console.log();\n        console.log();\n      },\n\n      /**\n       * Ask for feedback to the user using the terminal\n       *\n       * @param node //{ from: string; to: string }\n       * @returns\n       */\n      askForFeedback: function (node = {}) {\n        return input({\n          message: `Provide feedback to ${chalk.yellow(\n            node.to\n          )} as ${chalk.yellow(\n            node.from\n          )}. Press enter to skip and use auto-reply, or type 'exit' to end the conversation: `,\n        });\n      },\n    };\n  },\n};\n\nmodule.exports = { cli };\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/file-history.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\n\n/**\n * Plugin to save chat history to a json file\n */\nconst fileHistory = {\n  name: \"file-history-plugin\",\n  startupConfig: {\n    params: {},\n  },\n  plugin: function ({\n    filename = `history/chat-history-${new Date().toISOString()}.json`,\n  } = {}) {\n    return {\n      name: this.name,\n      setup(aibitat) {\n        const folderPath = path.dirname(filename);\n        // get path from filename\n        if (folderPath) {\n          fs.mkdirSync(folderPath, { recursive: true });\n        }\n\n        aibitat.onMessage(() => {\n          const content = JSON.stringify(aibitat.chats, null, 2);\n          fs.writeFile(filename, content, (err) => {\n            if (err) {\n              console.error(err);\n            }\n          });\n        });\n      },\n    };\n  },\n};\n\nmodule.exports = { fileHistory };\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/http-socket.js",
    "content": "const chalk = require(\"chalk\");\nconst { Telemetry } = require(\"../../../../models/telemetry\");\n\n/**\n * HTTP Interface plugin for Aibitat to emulate a websocket interface in the agent\n * framework so we dont have to modify the interface for passing messages and responses\n * in REST or WSS.\n */\nconst httpSocket = {\n  name: \"httpSocket\",\n  startupConfig: {\n    params: {\n      handler: {\n        required: true,\n      },\n      muteUserReply: {\n        required: false,\n        default: true,\n      },\n      introspection: {\n        required: false,\n        default: true,\n      },\n    },\n  },\n  plugin: function ({\n    handler,\n    muteUserReply = true, // Do not post messages to \"USER\" back to frontend.\n    introspection = false, // when enabled will attach socket to Aibitat object with .introspect method which reports status updates to frontend.\n  }) {\n    return {\n      name: this.name,\n      setup(aibitat) {\n        aibitat.onError(async (error) => {\n          let errorMessage =\n            error?.message || \"An error occurred while running the agent.\";\n          console.error(chalk.red(`   error: ${errorMessage}`), error);\n          aibitat.introspect(\n            `Error encountered while running: ${errorMessage}`\n          );\n          handler.send(\n            JSON.stringify({ type: \"wssFailure\", content: errorMessage })\n          );\n          aibitat.terminate();\n        });\n\n        aibitat.introspect = (messageText) => {\n          if (!introspection) return; // Dump thoughts when not wanted.\n          handler.send(\n            JSON.stringify({ type: \"statusResponse\", content: messageText })\n          );\n        };\n\n        // expose function for sockets across aibitat\n        // type param must be set or else msg will not be shown or handled in UI.\n        aibitat.socket = {\n          send: (type = \"__unhandled\", content = \"\") => {\n            handler.send(JSON.stringify({ type, content }));\n          },\n        };\n\n        // We can only receive one message response with HTTP\n        // so we end on first response.\n        aibitat.onMessage((message) => {\n          if (message.from !== \"USER\")\n            Telemetry.sendTelemetry(\"agent_chat_sent\");\n          if (message.from === \"USER\" && muteUserReply) return;\n          handler.send(JSON.stringify(message));\n          handler.close();\n        });\n\n        aibitat.onTerminate(() => {\n          handler.close();\n        });\n      },\n    };\n  },\n};\n\nmodule.exports = {\n  httpSocket,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/index.js",
    "content": "const { webBrowsing } = require(\"./web-browsing.js\");\nconst { webScraping } = require(\"./web-scraping.js\");\nconst { websocket } = require(\"./websocket.js\");\nconst { docSummarizer } = require(\"./summarize.js\");\nconst { saveFileInBrowser } = require(\"./save-file-browser.js\");\nconst { chatHistory } = require(\"./chat-history.js\");\nconst { memory } = require(\"./memory.js\");\nconst { rechart } = require(\"./rechart.js\");\nconst { sqlAgent } = require(\"./sql-agent/index.js\");\n\nmodule.exports = {\n  webScraping,\n  webBrowsing,\n  websocket,\n  docSummarizer,\n  saveFileInBrowser,\n  chatHistory,\n  memory,\n  rechart,\n  sqlAgent,\n\n  // Plugin name aliases so they can be pulled by slug as well.\n  [webScraping.name]: webScraping,\n  [webBrowsing.name]: webBrowsing,\n  [websocket.name]: websocket,\n  [docSummarizer.name]: docSummarizer,\n  [saveFileInBrowser.name]: saveFileInBrowser,\n  [chatHistory.name]: chatHistory,\n  [memory.name]: memory,\n  [rechart.name]: rechart,\n  [sqlAgent.name]: sqlAgent,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/memory.js",
    "content": "const { v4 } = require(\"uuid\");\nconst { getVectorDbClass, getLLMProvider } = require(\"../../../helpers\");\nconst { Deduplicator } = require(\"../utils/dedupe\");\n\nconst memory = {\n  name: \"rag-memory\",\n  startupConfig: {\n    params: {},\n  },\n  plugin: function () {\n    return {\n      name: this.name,\n      setup(aibitat) {\n        aibitat.function({\n          super: aibitat,\n          tracker: new Deduplicator(),\n          name: this.name,\n          description:\n            \"Search your local documents and workspace files for relevant information, or store information to long-term memory. Use search to find answers in uploaded documents, embedded files, or previously stored memories. Use store only when explicitly asked to remember or save something.\",\n          examples: [\n            {\n              prompt: \"Check my files for information about the project\",\n              call: JSON.stringify({\n                action: \"search\",\n                content: \"<project information to search for>\",\n              }),\n            },\n            {\n              prompt: \"What do you know about Plato's motives?\",\n              call: JSON.stringify({\n                action: \"search\",\n                content: \"What are the facts about Plato's motives?\",\n              }),\n            },\n            {\n              prompt: \"Remember that you are a robot\",\n              call: JSON.stringify({\n                action: \"store\",\n                content: \"I am a robot, the user told me that i am.\",\n              }),\n            },\n          ],\n          parameters: {\n            $schema: \"http://json-schema.org/draft-07/schema#\",\n            type: \"object\",\n            properties: {\n              action: {\n                type: \"string\",\n                enum: [\"search\", \"store\"],\n                description:\n                  \"The action we want to take to search for existing similar context or storage of new context.\",\n              },\n              content: {\n                type: \"string\",\n                description:\n                  \"The plain text to search our local documents with or to store in our vector database.\",\n              },\n            },\n            additionalProperties: false,\n          },\n          handler: async function ({ action = \"\", content = \"\" }) {\n            try {\n              const { isDuplicate } = this.tracker.isDuplicate(this.name, {\n                action,\n                content,\n              });\n              if (isDuplicate)\n                return `This was a duplicated call and it's output will be ignored.`;\n\n              let response = \"There was nothing to do.\";\n              if (action === \"search\") response = await this.search(content);\n              if (action === \"store\") response = await this.store(content);\n\n              this.tracker.trackRun(this.name, { action, content });\n              return response;\n            } catch (error) {\n              console.log(error);\n              return `There was an error while calling the function. ${error.message}`;\n            }\n          },\n          search: async function (query = \"\") {\n            try {\n              const workspace = this.super.handlerProps.invocation.workspace;\n              const LLMConnector = getLLMProvider({\n                provider: workspace?.chatProvider,\n                model: workspace?.chatModel,\n              });\n              const vectorDB = getVectorDbClass();\n              const { contextTexts = [] } =\n                await vectorDB.performSimilaritySearch({\n                  namespace: workspace.slug,\n                  input: query,\n                  LLMConnector,\n                  topN: workspace?.topN ?? 4,\n                  rerank: workspace?.vectorSearchMode === \"rerank\",\n                });\n\n              if (contextTexts.length === 0) {\n                this.super.introspect(\n                  `${this.caller}: I didn't find anything locally that would help answer this question.`\n                );\n                return \"There was no additional context found for that query. We should search the web for this information.\";\n              }\n\n              this.super.introspect(\n                `${this.caller}: Found ${contextTexts.length} additional piece of context to help answer this question.`\n              );\n\n              let combinedText = \"Additional context for query:\\n\";\n              for (const text of contextTexts) combinedText += text + \"\\n\\n\";\n              return combinedText;\n            } catch (error) {\n              this.super.handlerProps.log(\n                `memory.search raised an error. ${error.message}`\n              );\n              return `An error was raised while searching the vector database. ${error.message}`;\n            }\n          },\n          store: async function (content = \"\") {\n            try {\n              const workspace = this.super.handlerProps.invocation.workspace;\n              const vectorDB = getVectorDbClass();\n              const { error } = await vectorDB.addDocumentToNamespace(\n                workspace.slug,\n                {\n                  docId: v4(),\n                  id: v4(),\n                  url: \"file://embed-via-agent.txt\",\n                  title: \"agent-memory.txt\",\n                  docAuthor: \"@agent\",\n                  description: \"Unknown\",\n                  docSource: \"a text file stored by the workspace agent.\",\n                  chunkSource: \"\",\n                  published: new Date().toLocaleString(),\n                  wordCount: content.split(\" \").length,\n                  pageContent: content,\n                  token_count_estimate: 0,\n                },\n                null\n              );\n\n              if (!!error)\n                return \"The content was failed to be embedded properly.\";\n              this.super.introspect(\n                `${this.caller}: I saved the content to long-term memory in this workspaces vector database.`\n              );\n              return \"The content given was successfully embedded. There is nothing else to do.\";\n            } catch (error) {\n              this.super.handlerProps.log(\n                `memory.store raised an error. ${error.message}`\n              );\n              return `Let the user know this action was not successful. An error was raised while storing data in the vector database. ${error.message}`;\n            }\n          },\n        });\n      },\n    };\n  },\n};\n\nmodule.exports = {\n  memory,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/rechart.js",
    "content": "const { safeJsonParse } = require(\"../../../http\");\nconst { Deduplicator } = require(\"../utils/dedupe\");\n\nconst rechart = {\n  name: \"create-chart\",\n  startupConfig: {\n    params: {},\n  },\n  plugin: function () {\n    return {\n      name: this.name,\n      setup(aibitat) {\n        // Scrape a website and summarize the content based on objective if the content is too large.',\n        aibitat.function({\n          super: aibitat,\n          name: this.name,\n          tracker: new Deduplicator(),\n          description:\n            \"Create a chart, graph, or data visualization. Generate bar charts, line graphs, pie charts, area charts, or scatter plots to visualize data, statistics, trends, or results. Use to display numbers and data visually.\",\n          examples: [\n            { prompt: \"Create a chart from that data\" },\n            { prompt: \"Make a bar graph of the results\" },\n            { prompt: \"Visualize these numbers\" },\n          ],\n          parameters: {\n            $schema: \"http://json-schema.org/draft-07/schema#\",\n            type: \"object\",\n            properties: {\n              type: {\n                type: \"string\",\n                enum: [\n                  \"area\",\n                  \"bar\",\n                  \"line\",\n                  \"composed\",\n                  \"scatter\",\n                  \"pie\",\n                  \"radar\",\n                  \"radialBar\",\n                  \"treemap\",\n                  \"funnel\",\n                ],\n                description: \"The type of chart to be generated.\",\n              },\n              title: {\n                type: \"string\",\n                description:\n                  \"Title of the chart. There MUST always be a title. Do not leave it blank.\",\n              },\n              dataset: {\n                type: \"string\",\n                description: `Valid JSON in which each element is an object for Recharts API for the 'type' of chart defined WITHOUT new line characters. Strictly using this FORMAT and naming:\n{ \"name\": \"a\", \"value\": 12 }].\nMake sure field \"name\" always stays named \"name\". Instead of naming value field value in JSON, name it based on user metric and make it the same across every item.\nMake sure the format use double quotes and property names are string literals. Provide JSON data only.`,\n              },\n            },\n            additionalProperties: false,\n          },\n          required: [\"type\", \"title\", \"dataset\"],\n          handler: async function ({ type, dataset, title }) {\n            try {\n              if (this.tracker.isMarkedUnique(this.name)) {\n                this.super.handlerProps.log(\n                  `${this.name} has been called for this chat response already. It can only be called once per chat.`\n                );\n                return \"The chart was generated and returned to the user. This function completed successfully. Do not call this function again.\";\n              }\n\n              const data = safeJsonParse(dataset, null);\n              if (data === null) {\n                this.super.introspect(\n                  `${this.caller}: ${this.name} provided invalid JSON data - so we cant make a ${type} chart.`\n                );\n                return \"Invalid JSON provided. Please only provide valid RechartJS JSON to generate a chart.\";\n              }\n\n              this.super.introspect(`${this.caller}: Rendering ${type} chart.`);\n              this.super.socket.send(\"rechartVisualize\", {\n                type,\n                dataset,\n                title,\n              });\n\n              this.super._replySpecialAttributes = {\n                saveAsType: \"rechartVisualize\",\n                storedResponse: (additionalText = \"\") =>\n                  JSON.stringify({\n                    type,\n                    dataset,\n                    title,\n                    caption: additionalText,\n                  }),\n                postSave: () => this.tracker.removeUniqueConstraint(this.name),\n              };\n\n              this.tracker.markUnique(this.name);\n              return \"The chart was generated and returned to the user. This function completed successfully. Do not make another chart.\";\n            } catch (error) {\n              this.super.handlerProps.log(\n                `create-chart raised an error. ${error.message}`\n              );\n              return `Let the user know this action was not successful. An error was raised while generating the chart. ${error.message}`;\n            }\n          },\n        });\n      },\n    };\n  },\n};\n\nmodule.exports = {\n  rechart,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/save-file-browser.js",
    "content": "const { Deduplicator } = require(\"../utils/dedupe\");\n\nconst saveFileInBrowser = {\n  name: \"save-file-to-browser\",\n  startupConfig: {\n    params: {},\n  },\n  plugin: function () {\n    return {\n      name: this.name,\n      setup(aibitat) {\n        // List and summarize the contents of files that are embedded in the workspace\n        aibitat.function({\n          super: aibitat,\n          tracker: new Deduplicator(),\n          name: this.name,\n          description:\n            \"Download or export content as a file. Save text, code, data, or conversation content to a downloadable file. Use when the user wants to save, download, or export something as a file.\",\n          examples: [\n            {\n              prompt: \"Download that as a file\",\n              call: JSON.stringify({\n                file_content: \"<content to save>\",\n                filename: \"download.txt\",\n              }),\n            },\n            {\n              prompt: \"Save that code to a file\",\n              call: JSON.stringify({\n                file_content: \"<code content>\",\n                filename: \"code.js\",\n              }),\n            },\n            {\n              prompt: \"Save me that to a file named 'output'\",\n              call: JSON.stringify({\n                file_content: \"<content of the file>\",\n                filename: \"output.txt\",\n              }),\n            },\n          ],\n          parameters: {\n            $schema: \"http://json-schema.org/draft-07/schema#\",\n            type: \"object\",\n            properties: {\n              file_content: {\n                type: \"string\",\n                description: \"The content of the file that will be saved.\",\n              },\n              filename: {\n                type: \"string\",\n                description:\n                  \"filename to save the file as with extension. Extension should be plaintext file extension.\",\n              },\n            },\n            additionalProperties: false,\n          },\n          handler: async function ({ file_content = \"\", filename }) {\n            try {\n              const { isDuplicate, reason } = this.tracker.isDuplicate(\n                this.name,\n                { file_content, filename }\n              );\n              if (isDuplicate) {\n                this.super.handlerProps.log(\n                  `${this.name} was called, but exited early because ${reason}.`\n                );\n                return `${filename} file has been saved successfully!`;\n              }\n\n              this.super.socket.send(\"fileDownload\", {\n                filename,\n                b64Content:\n                  \"data:text/plain;base64,\" +\n                  Buffer.from(file_content, \"utf8\").toString(\"base64\"),\n              });\n              this.super.introspect(`${this.caller}: Saving file ${filename}.`);\n              this.tracker.trackRun(this.name, { file_content, filename });\n              return `${filename} file has been saved successfully and will be downloaded automatically to the users browser.`;\n            } catch (error) {\n              this.super.handlerProps.log(\n                `save-file-to-browser raised an error. ${error.message}`\n              );\n              return `Let the user know this action was not successful. An error was raised while saving a file to the browser. ${error.message}`;\n            }\n          },\n        });\n      },\n    };\n  },\n};\n\nmodule.exports = {\n  saveFileInBrowser,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/MSSQL.js",
    "content": "const mssql = require(\"mssql\");\nconst { ConnectionStringParser } = require(\"./utils\");\n\nclass MSSQLConnector {\n  #connected = false;\n  database_id = \"\";\n  connectionConfig = {\n    user: null,\n    password: null,\n    database: null,\n    server: null,\n    port: null,\n    pool: {\n      max: 10,\n      min: 0,\n      idleTimeoutMillis: 30000,\n    },\n    options: {\n      encrypt: false,\n      trustServerCertificate: true,\n    },\n  };\n\n  constructor(\n    config = {\n      // we will force into RFC-3986 from DB\n      // eg: mssql://user:password@server:port/database?{...opts}\n      connectionString: null, // we will force into RFC-3986\n    }\n  ) {\n    this.className = \"MSSQLConnector\";\n    this.connectionString = config.connectionString;\n    this._client = null;\n    this.#parseDatabase();\n  }\n\n  #parseDatabase() {\n    const connectionParser = new ConnectionStringParser({ scheme: \"mssql\" });\n    const parsed = connectionParser.parse(this.connectionString);\n\n    this.database_id = parsed?.endpoint;\n    this.connectionConfig = {\n      ...this.connectionConfig,\n      user: parsed?.username,\n      password: parsed?.password,\n      database: parsed?.endpoint,\n      server: parsed?.hosts?.[0]?.host,\n      port: parsed?.hosts?.[0]?.port,\n      options: {\n        ...this.connectionConfig.options,\n        encrypt: parsed?.options?.encrypt === \"true\",\n      },\n    };\n  }\n\n  async connect() {\n    this._client = await mssql.connect(this.connectionConfig);\n    this.#connected = true;\n    return this._client;\n  }\n\n  /**\n   *\n   * @param {string} queryString the SQL query to be run\n   * @param {Array} params optional parameters for prepared statement\n   * @returns {Promise<import(\".\").QueryResult>}\n   */\n  async runQuery(queryString = \"\", params = []) {\n    const result = { rows: [], count: 0, error: null };\n    try {\n      if (!this.#connected) await this.connect();\n\n      const request = this._client.request();\n      params.forEach((value, index) => {\n        request.input(`p${index}`, value);\n      });\n      const query = await request.query(queryString);\n      result.rows = query.recordset;\n      result.count = query.rowsAffected.reduce((sum, a) => sum + a, 0);\n    } catch (err) {\n      console.log(this.className, err);\n      result.error = err.message;\n    } finally {\n      // Check client is connected before closing since we use this for validation\n      if (this._client) {\n        await this._client.close();\n        this.#connected = false;\n      }\n    }\n    return result;\n  }\n\n  async validateConnection() {\n    try {\n      const result = await this.runQuery(\"SELECT 1\");\n      return { success: !result.error, error: result.error };\n    } catch (error) {\n      return { success: false, error: error.message };\n    }\n  }\n\n  getTablesSql() {\n    return `SELECT name FROM sysobjects WHERE xtype='U';`;\n  }\n\n  getTableSchemaSql(table_name) {\n    return {\n      query: `SELECT COLUMN_NAME, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @p0`,\n      params: [table_name],\n    };\n  }\n}\n\nmodule.exports.MSSQLConnector = MSSQLConnector;\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/MySQL.js",
    "content": "const mysql = require(\"mysql2/promise\");\nconst { ConnectionStringParser } = require(\"./utils\");\n\nclass MySQLConnector {\n  #connected = false;\n  database_id = \"\";\n  constructor(\n    config = {\n      connectionString: null,\n    }\n  ) {\n    this.className = \"MySQLConnector\";\n    this.connectionString = config.connectionString;\n    this._client = null;\n    this.database_id = this.#parseDatabase();\n  }\n\n  #parseDatabase() {\n    const connectionParser = new ConnectionStringParser({ scheme: \"mysql\" });\n    const parsed = connectionParser.parse(this.connectionString);\n    return parsed?.endpoint;\n  }\n\n  async connect() {\n    this._client = await mysql.createConnection({ uri: this.connectionString });\n    this.#connected = true;\n    return this._client;\n  }\n\n  /**\n   *\n   * @param {string} queryString the SQL query to be run\n   * @param {Array} params optional parameters for prepared statement\n   * @returns {Promise<import(\".\").QueryResult>}\n   */\n  async runQuery(queryString = \"\", params = []) {\n    const result = { rows: [], count: 0, error: null };\n    try {\n      if (!this.#connected) await this.connect();\n      const [query] =\n        params.length > 0\n          ? await this._client.execute(queryString, params)\n          : await this._client.query(queryString);\n      result.rows = query;\n      result.count = query?.length;\n    } catch (err) {\n      console.log(this.className, err);\n      result.error = err.message;\n    } finally {\n      // Check client is connected before closing since we use this for validation\n      if (this._client) {\n        await this._client.end();\n        this.#connected = false;\n      }\n    }\n    return result;\n  }\n\n  async validateConnection() {\n    try {\n      const result = await this.runQuery(\"SELECT 1\");\n      return { success: !result.error, error: result.error };\n    } catch (error) {\n      return { success: false, error: error.message };\n    }\n  }\n\n  getTablesSql() {\n    return {\n      query: `SELECT table_name FROM information_schema.tables WHERE table_schema = ?`,\n      params: [this.database_id],\n    };\n  }\n\n  getTableSchemaSql(table_name) {\n    return {\n      query: `SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT, EXTRA FROM information_schema.columns WHERE table_schema = ? AND table_name = ?`,\n      params: [this.database_id, table_name],\n    };\n  }\n}\n\nmodule.exports.MySQLConnector = MySQLConnector;\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/Postgresql.js",
    "content": "const pgSql = require(\"pg\");\n\nclass PostgresSQLConnector {\n  #connected = false;\n  constructor(\n    config = {\n      connectionString: null,\n      schema: null,\n    }\n  ) {\n    this.className = \"PostgresSQLConnector\";\n    this.connectionString = config.connectionString;\n    this.schema = config.schema || \"public\";\n    this._client = new pgSql.Client({\n      connectionString: this.connectionString,\n    });\n  }\n\n  async connect() {\n    await this._client.connect();\n    this.#connected = true;\n    return this._client;\n  }\n\n  /**\n   *\n   * @param {string} queryString the SQL query to be run\n   * @param {Array} params optional parameters for prepared statement\n   * @returns {Promise<import(\".\").QueryResult>}\n   */\n  async runQuery(queryString = \"\", params = []) {\n    const result = { rows: [], count: 0, error: null };\n    try {\n      if (!this.#connected) await this.connect();\n      const query = await this._client.query(queryString, params);\n      result.rows = query.rows;\n      result.count = query.rowCount;\n    } catch (err) {\n      console.log(this.className, err);\n      result.error = err.message;\n    } finally {\n      // Check client is connected before closing since we use this for validation\n      if (this._client) {\n        await this._client.end();\n        this.#connected = false;\n      }\n    }\n    return result;\n  }\n\n  async validateConnection() {\n    try {\n      const result = await this.runQuery(\"SELECT 1\");\n      return { success: !result.error, error: result.error };\n    } catch (error) {\n      return { success: false, error: error.message };\n    }\n  }\n\n  getTablesSql() {\n    return {\n      query: `SELECT * FROM pg_catalog.pg_tables WHERE schemaname = $1`,\n      params: [this.schema],\n    };\n  }\n\n  getTableSchemaSql(table_name) {\n    return {\n      query: `SELECT column_name, data_type, character_maximum_length, column_default, is_nullable FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = $1 AND table_schema = $2`,\n      params: [table_name, this.schema],\n    };\n  }\n}\n\nmodule.exports.PostgresSQLConnector = PostgresSQLConnector;\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/index.js",
    "content": "const { SystemSettings } = require(\"../../../../../../models/systemSettings\");\nconst { safeJsonParse } = require(\"../../../../../http\");\n\n/**\n * @typedef {('postgresql'|'mysql'|'sql-server')} SQLEngine\n */\n\n/**\n * @typedef {Object} QueryResult\n * @property {[number]} rows - The query result rows\n * @property {number} count - Number of rows the query returned/changed\n * @property {string|null} error - Error string if there was an issue\n */\n\n/**\n * A valid database SQL connection object\n * @typedef {Object} SQLConnection\n * @property {string} database_id - Unique identifier of the database connection\n * @property {SQLEngine} engine - Engine used by connection\n * @property {string} connectionString - RFC connection string for db\n */\n\n/**\n * @param {SQLEngine} identifier\n * @param {object} connectionConfig\n * @returns Database Connection Engine Class for SQLAgent or throws error\n */\nfunction getDBClient(identifier = \"\", connectionConfig = {}) {\n  switch (identifier) {\n    case \"mysql\":\n      const { MySQLConnector } = require(\"./MySQL\");\n      return new MySQLConnector(connectionConfig);\n    case \"postgresql\":\n      const { PostgresSQLConnector } = require(\"./Postgresql\");\n      return new PostgresSQLConnector(connectionConfig);\n    case \"sql-server\":\n      const { MSSQLConnector } = require(\"./MSSQL\");\n      return new MSSQLConnector(connectionConfig);\n    default:\n      throw new Error(\n        `There is no supported database connector for ${identifier}`\n      );\n  }\n}\n\n/**\n * Lists all of the known database connection that can be used by the agent.\n * @returns {Promise<[SQLConnection]>}\n */\nasync function listSQLConnections() {\n  return safeJsonParse(\n    (await SystemSettings.get({ label: \"agent_sql_connections\" }))?.value,\n    []\n  );\n}\n\n/**\n * Validates a SQL connection by attempting to connect and run a simple query\n * @param {SQLEngine} identifier - The SQL engine type\n * @param {object} connectionConfig - The connection configuration\n * @returns {Promise<{success: boolean, error: string|null}>}\n */\nasync function validateConnection(identifier = \"\", connectionConfig = {}) {\n  try {\n    const client = getDBClient(identifier, connectionConfig);\n    return await client.validateConnection();\n  } catch {\n    console.log(`Failed to connect to ${identifier} database.`);\n    return {\n      success: false,\n      error: `Unable to connect to ${identifier}. Please verify your connection details.`,\n    };\n  }\n}\n\nmodule.exports = {\n  getDBClient,\n  listSQLConnections,\n  validateConnection,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/utils.js",
    "content": "// Credit: https://github.com/sindilevich/connection-string-parser\n\n/**\n * @typedef {Object} ConnectionStringParserOptions\n * @property {'mssql' | 'mysql' | 'postgresql' | 'db'} [scheme] - The scheme of the connection string\n */\n\n/**\n * @typedef {Object} ConnectionStringObject\n * @property {string} scheme - The scheme of the connection string eg: mongodb, mssql, mysql, postgresql, etc.\n * @property {string} username - The username of the connection string\n * @property {string} password - The password of the connection string\n * @property {{host: string, port: number}[]} hosts - The hosts of the connection string\n * @property {string} endpoint - The endpoint (database name) of the connection string\n * @property {Object} options - The options of the connection string\n */\nclass ConnectionStringParser {\n  static DEFAULT_SCHEME = \"db\";\n\n  /**\n   * @param {ConnectionStringParserOptions} options\n   */\n  constructor(options = {}) {\n    this.scheme =\n      (options && options.scheme) || ConnectionStringParser.DEFAULT_SCHEME;\n  }\n\n  /**\n   * Takes a connection string object and returns a URI string of the form:\n   *\n   * scheme://[username[:password]@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[endpoint]][?options]\n   * @param {Object} connectionStringObject The object that describes connection string parameters\n   */\n  format(connectionStringObject) {\n    if (!connectionStringObject) {\n      return this.scheme + \"://localhost\";\n    }\n    if (\n      this.scheme &&\n      connectionStringObject.scheme &&\n      this.scheme !== connectionStringObject.scheme\n    ) {\n      throw new Error(`Scheme not supported: ${connectionStringObject.scheme}`);\n    }\n\n    let uri =\n      (this.scheme ||\n        connectionStringObject.scheme ||\n        ConnectionStringParser.DEFAULT_SCHEME) + \"://\";\n\n    if (connectionStringObject.username) {\n      uri += encodeURIComponent(connectionStringObject.username);\n      // Allow empty passwords\n      if (connectionStringObject.password) {\n        uri += \":\" + encodeURIComponent(connectionStringObject.password);\n      }\n      uri += \"@\";\n    }\n    uri += this._formatAddress(connectionStringObject);\n    // Only put a slash when there is an endpoint\n    if (connectionStringObject.endpoint) {\n      uri += \"/\" + encodeURIComponent(connectionStringObject.endpoint);\n    }\n    if (\n      connectionStringObject.options &&\n      Object.keys(connectionStringObject.options).length > 0\n    ) {\n      uri +=\n        \"?\" +\n        Object.keys(connectionStringObject.options)\n          .map(\n            (option) =>\n              encodeURIComponent(option) +\n              \"=\" +\n              encodeURIComponent(connectionStringObject.options[option])\n          )\n          .join(\"&\");\n    }\n    return uri;\n  }\n\n  /**\n   * Where scheme and hosts will always be present. Other fields will only be present in the result if they were\n   * present in the input.\n   * @param {string} uri The connection string URI\n   * @returns {ConnectionStringObject} The connection string object\n   */\n  parse(uri) {\n    const connectionStringParser = new RegExp(\n      \"^\\\\s*\" + // Optional whitespace padding at the beginning of the line\n        \"([^:]+)://\" + // Scheme (Group 1)\n        \"(?:([^:@,/?=&]+)(?::([^:@,/?=&]+))?@)?\" + // User (Group 2) and Password (Group 3)\n        \"([^@/?=&]+)\" + // Host address(es) (Group 4)\n        \"(?:/([^:@,/?=&]+)?)?\" + // Endpoint (Group 5)\n        \"(?:\\\\?([^:@,/?]+)?)?\" + // Options (Group 6)\n        \"\\\\s*$\", // Optional whitespace padding at the end of the line\n      \"gi\"\n    );\n    const connectionStringObject = {};\n\n    if (!uri.includes(\"://\")) {\n      throw new Error(`No scheme found in URI ${uri}`);\n    }\n\n    const tokens = connectionStringParser.exec(uri);\n\n    if (Array.isArray(tokens)) {\n      connectionStringObject.scheme = tokens[1];\n      if (this.scheme && this.scheme !== connectionStringObject.scheme) {\n        throw new Error(`URI must start with '${this.scheme}://'`);\n      }\n      connectionStringObject.username = tokens[2]\n        ? decodeURIComponent(tokens[2])\n        : tokens[2];\n      connectionStringObject.password = tokens[3]\n        ? decodeURIComponent(tokens[3])\n        : tokens[3];\n      connectionStringObject.hosts = this._parseAddress(tokens[4]);\n      connectionStringObject.endpoint = tokens[5]\n        ? decodeURIComponent(tokens[5])\n        : tokens[5];\n      connectionStringObject.options = tokens[6]\n        ? this._parseOptions(tokens[6])\n        : tokens[6];\n    }\n    return connectionStringObject;\n  }\n\n  /**\n   * Formats the address portion of a connection string\n   * @param {Object} connectionStringObject The object that describes connection string parameters\n   */\n  _formatAddress(connectionStringObject) {\n    return connectionStringObject.hosts\n      .map(\n        (address) =>\n          encodeURIComponent(address.host) +\n          (address.port\n            ? \":\" + encodeURIComponent(address.port.toString(10))\n            : \"\")\n      )\n      .join(\",\");\n  }\n\n  /**\n   * Parses an address\n   * @param {string} addresses The address(es) to process\n   */\n  _parseAddress(addresses) {\n    return addresses.split(\",\").map((address) => {\n      const i = address.indexOf(\":\");\n\n      return i >= 0\n        ? {\n            host: decodeURIComponent(address.substring(0, i)),\n            port: +address.substring(i + 1),\n          }\n        : { host: decodeURIComponent(address) };\n    });\n  }\n\n  /**\n   * Parses options\n   * @param {string} options The options to process\n   */\n  _parseOptions(options) {\n    const result = {};\n\n    options.split(\"&\").forEach((option) => {\n      const i = option.indexOf(\"=\");\n\n      if (i >= 0) {\n        result[decodeURIComponent(option.substring(0, i))] = decodeURIComponent(\n          option.substring(i + 1)\n        );\n      }\n    });\n    return result;\n  }\n}\n\nmodule.exports = { ConnectionStringParser };\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/sql-agent/get-table-schema.js",
    "content": "module.exports.SqlAgentGetTableSchema = {\n  name: \"sql-get-table-schema\",\n  plugin: function () {\n    const {\n      listSQLConnections,\n      getDBClient,\n    } = require(\"./SQLConnectors/index.js\");\n\n    function formatQueryForDisplay(query, params = []) {\n      if (!params.length) return query;\n      let formatted = query;\n      params.forEach((param, index) => {\n        const value = typeof param === \"string\" ? `'${param}'` : param;\n        formatted = formatted.replace(`$${index + 1}`, value);\n        formatted = formatted.replace(`@p${index}`, value);\n        formatted = formatted.replace(\"?\", value);\n      });\n      return formatted;\n    }\n\n    return {\n      name: \"sql-get-table-schema\",\n      setup(aibitat) {\n        aibitat.function({\n          super: aibitat,\n          name: this.name,\n          description:\n            \"Gets the table schema in SQL for a given `table` and `database_id`\",\n          examples: [\n            {\n              prompt: \"What does the customers table in access-logs look like?\",\n              call: JSON.stringify({\n                database_id: \"access-logs\",\n                table_name: \"customers\",\n              }),\n            },\n            {\n              prompt:\n                \"Get me the full name of a company in records-main, the table should be call comps\",\n              call: JSON.stringify({\n                database_id: \"records-main\",\n                table_name: \"comps\",\n              }),\n            },\n          ],\n          parameters: {\n            $schema: \"http://json-schema.org/draft-07/schema#\",\n            type: \"object\",\n            properties: {\n              database_id: {\n                type: \"string\",\n                description:\n                  \"The database identifier for which we will connect to to query the table schema. This is a required field.\",\n              },\n              table_name: {\n                type: \"string\",\n                description:\n                  \"The database identifier for the table name we want the schema for. This is a required field.\",\n              },\n            },\n            additionalProperties: false,\n          },\n          required: [\"database_id\", \"table_name\"],\n          handler: async function ({ database_id = \"\", table_name = \"\" }) {\n            this.super.handlerProps.log(`Using the sql-get-table-schema tool.`);\n            try {\n              const databaseConfig = (await listSQLConnections()).find(\n                (db) => db.database_id === database_id\n              );\n              if (!databaseConfig) {\n                this.super.handlerProps.log(\n                  `sql-get-table-schema to find config!`,\n                  database_id\n                );\n                return `No database connection for ${database_id} was found!`;\n              }\n\n              const db = getDBClient(databaseConfig.engine, databaseConfig);\n              this.super.introspect(\n                `${this.caller}: Querying the table schema for ${table_name} in the ${databaseConfig.database_id} database.`\n              );\n\n              const sqlQuery = db.getTableSchemaSql(table_name);\n              const isParameterized =\n                typeof sqlQuery === \"object\" && sqlQuery.query;\n              const queryString = isParameterized ? sqlQuery.query : sqlQuery;\n              const queryParams = isParameterized ? sqlQuery.params : [];\n\n              this.super.introspect(\n                `Running SQL: ${formatQueryForDisplay(queryString, queryParams)}`\n              );\n              const result = await db.runQuery(queryString, queryParams);\n\n              if (result.error) {\n                this.super.handlerProps.log(\n                  `sql-get-table-schema tool reported error`,\n                  result.error\n                );\n                this.super.introspect(`Error: ${result.error}`);\n                return `There was an error running the query: ${result.error}`;\n              }\n\n              return JSON.stringify(result);\n            } catch (e) {\n              this.super.handlerProps.log(\n                `sql-get-table-schema raised an error. ${e.message}`\n              );\n              return e.message;\n            }\n          },\n        });\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/sql-agent/index.js",
    "content": "const { SqlAgentGetTableSchema } = require(\"./get-table-schema\");\nconst { SqlAgentListDatabase } = require(\"./list-database\");\nconst { SqlAgentListTables } = require(\"./list-table\");\nconst { SqlAgentQuery } = require(\"./query\");\n\nconst sqlAgent = {\n  name: \"sql-agent\",\n  startupConfig: {\n    params: {},\n  },\n  plugin: [\n    SqlAgentListDatabase,\n    SqlAgentListTables,\n    SqlAgentGetTableSchema,\n    SqlAgentQuery,\n  ],\n};\n\nmodule.exports = {\n  sqlAgent,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/sql-agent/list-database.js",
    "content": "module.exports.SqlAgentListDatabase = {\n  name: \"sql-list-databases\",\n  plugin: function () {\n    const { listSQLConnections } = require(\"./SQLConnectors\");\n    return {\n      name: \"sql-list-databases\",\n      setup(aibitat) {\n        aibitat.function({\n          super: aibitat,\n          name: this.name,\n          description:\n            \"List all available databases via `list_databases` you currently have access to. Returns a unique string identifier `database_id` that can be used for future calls.\",\n          examples: [\n            {\n              prompt: \"What databases can you access?\",\n              call: JSON.stringify({}),\n            },\n            {\n              prompt: \"What databases can you tell me about?\",\n              call: JSON.stringify({}),\n            },\n            {\n              prompt: \"Is there a database named erp-logs you can access?\",\n              call: JSON.stringify({}),\n            },\n          ],\n          parameters: {\n            $schema: \"http://json-schema.org/draft-07/schema#\",\n            type: \"object\",\n            properties: {},\n            additionalProperties: false,\n          },\n          handler: async function () {\n            this.super.handlerProps.log(`Using the sql-list-databases tool.`);\n            this.super.introspect(\n              `${this.caller}: Checking what are the available databases.`\n            );\n\n            const connections = (await listSQLConnections()).map((conn) => {\n              const { connectionString: _connectionString, ...rest } = conn;\n              return rest;\n            });\n            return JSON.stringify(connections);\n          },\n        });\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/sql-agent/list-table.js",
    "content": "module.exports.SqlAgentListTables = {\n  name: \"sql-list-tables\",\n  plugin: function () {\n    const {\n      listSQLConnections,\n      getDBClient,\n    } = require(\"./SQLConnectors/index.js\");\n\n    function formatQueryForDisplay(query, params = []) {\n      if (!params.length) return query;\n      let formatted = query;\n      params.forEach((param, index) => {\n        const value = typeof param === \"string\" ? `'${param}'` : param;\n        formatted = formatted.replace(`$${index + 1}`, value);\n        formatted = formatted.replace(`@p${index}`, value);\n        formatted = formatted.replace(\"?\", value);\n      });\n      return formatted;\n    }\n\n    return {\n      name: \"sql-list-tables\",\n      setup(aibitat) {\n        aibitat.function({\n          super: aibitat,\n          name: this.name,\n          description:\n            \"List all available tables in a database via its `database_id`.\",\n          examples: [\n            {\n              prompt: \"What tables are there in the `access-logs` database?\",\n              call: JSON.stringify({ database_id: \"access-logs\" }),\n            },\n            {\n              prompt:\n                \"What information can you access in the customer_accts postgres db?\",\n              call: JSON.stringify({ database_id: \"customer_accts\" }),\n            },\n            {\n              prompt: \"Can you tell me what is in the primary-logs db?\",\n              call: JSON.stringify({ database_id: \"primary-logs\" }),\n            },\n          ],\n          parameters: {\n            $schema: \"http://json-schema.org/draft-07/schema#\",\n            type: \"object\",\n            properties: {\n              database_id: {\n                type: \"string\",\n                description:\n                  \"The database identifier for which we will list all tables for. This is a required parameter\",\n              },\n            },\n            additionalProperties: false,\n          },\n          required: [\"database_id\"],\n          handler: async function ({ database_id = \"\" }) {\n            try {\n              this.super.handlerProps.log(`Using the sql-list-tables tool.`);\n              const databaseConfig = (await listSQLConnections()).find(\n                (db) => db.database_id === database_id\n              );\n              if (!databaseConfig) {\n                this.super.handlerProps.log(\n                  `sql-list-tables failed to find config!`,\n                  database_id\n                );\n                return `No database connection for ${database_id} was found!`;\n              }\n\n              const db = getDBClient(databaseConfig.engine, databaseConfig);\n              this.super.introspect(\n                `${this.caller}: Checking what are the available tables in the ${databaseConfig.database_id} database.`\n              );\n\n              const sqlQuery = db.getTablesSql();\n              const isParameterized =\n                typeof sqlQuery === \"object\" && sqlQuery.query;\n              const queryString = isParameterized ? sqlQuery.query : sqlQuery;\n              const queryParams = isParameterized ? sqlQuery.params : [];\n\n              this.super.introspect(\n                `Running SQL: ${formatQueryForDisplay(queryString, queryParams)}`\n              );\n              const result = await db.runQuery(queryString, queryParams);\n              if (result.error) {\n                this.super.handlerProps.log(\n                  `sql-list-tables tool reported error`,\n                  result.error\n                );\n                this.super.introspect(`Error: ${result.error}`);\n                return `There was an error running the query: ${result.error}`;\n              }\n\n              return JSON.stringify(result);\n            } catch (e) {\n              console.error(e);\n              return e.message;\n            }\n          },\n        });\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/sql-agent/query.js",
    "content": "module.exports.SqlAgentQuery = {\n  name: \"sql-query\",\n  plugin: function () {\n    const {\n      getDBClient,\n      listSQLConnections,\n    } = require(\"./SQLConnectors/index.js\");\n\n    return {\n      name: \"sql-query\",\n      setup(aibitat) {\n        aibitat.function({\n          super: aibitat,\n          name: this.name,\n          description:\n            \"Run a read-only SQL query on a `database_id` which will return up rows of data related to the query. The query must only be SELECT statements which do not modify the table data. There should be a reasonable LIMIT on the return quantity to prevent long-running or queries which crash the db.\",\n          examples: [\n            {\n              prompt: \"How many customers are in dvd-rentals?\",\n              call: JSON.stringify({\n                database_id: \"dvd-rentals\",\n                sql_query: \"SELECT * FROM customers\",\n              }),\n            },\n            {\n              prompt: \"Can you tell me the total volume of sales last month?\",\n              call: JSON.stringify({\n                database_id: \"sales-db\",\n                sql_query:\n                  \"SELECT SUM(sale_amount) AS total_sales FROM sales WHERE sale_date >= DATEADD(month, -1, DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1)) AND sale_date < DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1)\",\n              }),\n            },\n            {\n              prompt:\n                \"Do we have anyone in the staff table for our production db named 'sam'? \",\n              call: JSON.stringify({\n                database_id: \"production\",\n                sql_query:\n                  \"SElECT * FROM staff WHERE first_name='sam%' OR last_name='sam%'\",\n              }),\n            },\n          ],\n          parameters: {\n            $schema: \"http://json-schema.org/draft-07/schema#\",\n            type: \"object\",\n            properties: {\n              database_id: {\n                type: \"string\",\n                description:\n                  \"The database identifier for which we will connect to to query the table schema. This is required to run the SQL query.\",\n              },\n              sql_query: {\n                type: \"string\",\n                description:\n                  \"The raw SQL query to run. Should be a query which does not modify the table and will return results.\",\n              },\n            },\n            additionalProperties: false,\n          },\n          required: [\"database_id\", \"table_name\"],\n          handler: async function ({ database_id = \"\", sql_query = \"\" }) {\n            this.super.handlerProps.log(`Using the sql-query tool.`);\n            try {\n              const databaseConfig = (await listSQLConnections()).find(\n                (db) => db.database_id === database_id\n              );\n              if (!databaseConfig) {\n                this.super.handlerProps.log(\n                  `sql-query failed to find config!`,\n                  database_id\n                );\n                return `No database connection for ${database_id} was found!`;\n              }\n\n              this.super.introspect(\n                `${this.caller}: Im going to run a query on the ${database_id} to get an answer.`\n              );\n              const db = getDBClient(databaseConfig.engine, databaseConfig);\n\n              this.super.introspect(`Running SQL: ${sql_query}`);\n              const result = await db.runQuery(sql_query);\n              if (result.error) {\n                this.super.handlerProps.log(\n                  `sql-query tool reported error`,\n                  result.error\n                );\n                this.super.introspect(`Error: ${result.error}`);\n                return `There was an error running the query: ${result.error}`;\n              }\n\n              return JSON.stringify(result);\n            } catch (e) {\n              console.error(e);\n              return e.message;\n            }\n          },\n        });\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/summarize.js",
    "content": "const { Document } = require(\"../../../../models/documents\");\nconst { safeJsonParse } = require(\"../../../http\");\nconst { summarizeContent } = require(\"../utils/summarize\");\nconst Provider = require(\"../providers/ai-provider\");\n\nconst docSummarizer = {\n  name: \"document-summarizer\",\n  startupConfig: {\n    params: {},\n  },\n  plugin: function () {\n    return {\n      name: this.name,\n      setup(aibitat) {\n        aibitat.function({\n          super: aibitat,\n          name: this.name,\n          controller: new AbortController(),\n          description:\n            \"List all documents in the workspace or summarize a specific document. See what files are available, get a summary of a document's contents, or read and condense a file into key points.\",\n          examples: [\n            {\n              prompt: \"List my files\",\n              call: JSON.stringify({ action: \"list\", document_filename: null }),\n            },\n            {\n              prompt: \"Summarize the readme file\",\n              call: JSON.stringify({\n                action: \"summarize\",\n                document_filename: \"readme.md\",\n              }),\n            },\n            {\n              prompt: \"Give me a summary of example.txt\",\n              call: JSON.stringify({\n                action: \"summarize\",\n                document_filename: \"example.txt\",\n              }),\n            },\n          ],\n          parameters: {\n            $schema: \"http://json-schema.org/draft-07/schema#\",\n            type: \"object\",\n            properties: {\n              action: {\n                type: \"string\",\n                enum: [\"list\", \"summarize\"],\n                description:\n                  \"The action to take. 'list' will return all files available with their filename and descriptions. 'summarize' will open and summarize the file by the a document name.\",\n              },\n              document_filename: {\n                type: \"string\",\n                \"x-nullable\": true,\n                description:\n                  \"The file name of the document you want to get the full content of.\",\n              },\n            },\n            additionalProperties: false,\n          },\n          handler: async function ({ action, document_filename }) {\n            if (action === \"list\") return await this.listDocuments();\n            if (action === \"summarize\")\n              return await this.summarizeDoc(document_filename);\n            return \"There is nothing we can do. This function call returns no information.\";\n          },\n\n          /**\n           * List all documents available in a workspace\n           * @returns List of files and their descriptions if available.\n           */\n          listDocuments: async function () {\n            try {\n              this.super.introspect(\n                `${this.caller}: Looking at the available documents.`\n              );\n              const documents = await Document.where({\n                workspaceId: this.super.handlerProps.invocation.workspace_id,\n              });\n              if (documents.length === 0)\n                return \"No documents found - nothing can be done. Stop.\";\n\n              this.super.introspect(\n                `${this.caller}: Found ${documents.length} documents`\n              );\n              const foundDocuments = documents.map((doc) => {\n                const metadata = safeJsonParse(doc.metadata, {});\n                return {\n                  document_id: doc.docId,\n                  filename: metadata?.title ?? \"unknown.txt\",\n                  description: metadata?.description ?? \"no description\",\n                };\n              });\n\n              return JSON.stringify(foundDocuments);\n            } catch (error) {\n              this.super.handlerProps.log(\n                `document-summarizer.list raised an error. ${error.message}`\n              );\n              return `Let the user know this action was not successful. An error was raised while listing available files. ${error.message}`;\n            }\n          },\n\n          summarizeDoc: async function (filename) {\n            try {\n              const availableDocs = safeJsonParse(\n                await this.listDocuments(),\n                []\n              );\n              if (!availableDocs.length) {\n                this.super.handlerProps.log(\n                  `${this.caller}: No available documents to summarize.`\n                );\n                return \"No documents were found.\";\n              }\n\n              const docInfo = availableDocs.find(\n                (info) => info.filename === filename\n              );\n              if (!docInfo) {\n                this.super.handlerProps.log(\n                  `${this.caller}: No available document by the name \"${filename}\".`\n                );\n                return `No available document by the name \"${filename}\".`;\n              }\n\n              const document = await Document.content(docInfo.document_id);\n              this.super.introspect(\n                `${this.caller}: Grabbing all content for ${\n                  filename ?? \"a discovered file.\"\n                }`\n              );\n\n              if (!document.content || document.content.length === 0) {\n                throw new Error(\n                  \"This document has no readable content that could be found.\"\n                );\n              }\n\n              // Report citation for the document being summarized\n              this.super.addCitation?.({\n                id: docInfo.document_id,\n                title: document.title || filename,\n                text: document.content,\n                chunkSource: null,\n                score: null,\n              });\n\n              const { TokenManager } = require(\"../../../helpers/tiktoken\");\n              if (\n                new TokenManager(this.super.model).countFromString(\n                  document.content\n                ) < Provider.contextLimit(this.super.provider, this.super.model)\n              ) {\n                return document.content;\n              }\n\n              this.super.introspect(\n                `${this.caller}: Summarizing ${filename ?? \"\"}...`\n              );\n\n              this.super.onAbort(() => {\n                this.super.handlerProps.log(\n                  \"Abort was triggered, exiting summarization early.\"\n                );\n                this.controller.abort();\n              });\n\n              return await summarizeContent({\n                provider: this.super.provider,\n                model: this.super.model,\n                controllerSignal: this.controller.signal,\n                content: document.content,\n              });\n            } catch (error) {\n              this.super.handlerProps.log(\n                `document-summarizer.summarizeDoc raised an error. ${error.message}`\n              );\n              return `Let the user know this action was not successful. An error was raised while summarizing the file. ${error.message}`;\n            }\n          },\n        });\n      },\n    };\n  },\n};\n\nmodule.exports = {\n  docSummarizer,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/web-browsing.js",
    "content": "const { SystemSettings } = require(\"../../../../models/systemSettings\");\nconst { TokenManager } = require(\"../../../helpers/tiktoken\");\nconst tiktoken = new TokenManager();\n\nconst webBrowsing = {\n  name: \"web-browsing\",\n  startupConfig: {\n    params: {},\n  },\n  plugin: function () {\n    return {\n      name: this.name,\n      setup(aibitat) {\n        aibitat.function({\n          super: aibitat,\n          name: this.name,\n          countTokens: (string) =>\n            tiktoken\n              .countFromString(string)\n              .toString()\n              .replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\"),\n          description:\n            \"Search the internet for real-time information. Look online for current news, recent updates, latest changes, or any information not available locally. Browse the web to find answers about current events, prices, weather, or live data.\",\n          examples: [\n            {\n              prompt: \"Look online for recent changes to AnythingLLM\",\n              call: JSON.stringify({\n                query: \"AnythingLLM recent changes updates\",\n              }),\n            },\n            {\n              prompt: \"Search the internet for the latest news\",\n              call: JSON.stringify({ query: \"latest news today\" }),\n            },\n            {\n              prompt: \"What's the current weather in NYC?\",\n              call: JSON.stringify({ query: \"current weather New York City\" }),\n            },\n          ],\n          parameters: {\n            $schema: \"http://json-schema.org/draft-07/schema#\",\n            type: \"object\",\n            properties: {\n              query: {\n                type: \"string\",\n                description: \"A search query.\",\n              },\n            },\n            additionalProperties: false,\n          },\n          handler: async function ({ query }) {\n            try {\n              if (query) return await this.search(query);\n              return \"There is nothing we can do. This function call returns no information.\";\n            } catch (error) {\n              return `There was an error while calling the function. No data or response was found. Let the user know this was the error: ${error.message}`;\n            }\n          },\n\n          /**\n           * Use Google Custom Search Engines\n           * Free to set up, easy to use, 100 calls/day!\n           * https://programmablesearchengine.google.com/controlpanel/create\n           */\n          search: async function (query) {\n            const provider =\n              (await SystemSettings.get({ label: \"agent_search_provider\" }))\n                ?.value ?? \"unknown\";\n            let engine;\n            switch (provider) {\n              case \"serpapi\":\n                engine = \"_serpApi\";\n                break;\n              case \"searchapi\":\n                engine = \"_searchApi\";\n                break;\n              case \"serper-dot-dev\":\n                engine = \"_serperDotDev\";\n                break;\n              case \"bing-search\":\n                engine = \"_bingWebSearch\";\n                break;\n              case \"serply-engine\":\n                engine = \"_serplyEngine\";\n                break;\n              case \"searxng-engine\":\n                engine = \"_searXNGEngine\";\n                break;\n              case \"tavily-search\":\n                engine = \"_tavilySearch\";\n                break;\n              case \"duckduckgo-engine\":\n                engine = \"_duckDuckGoEngine\";\n                break;\n              case \"exa-search\":\n                engine = \"_exaSearch\";\n                break;\n              case \"perplexity-search\":\n                engine = \"_perplexitySearch\";\n                break;\n              default:\n                engine = \"_duckDuckGoEngine\";\n            }\n            return await this[engine](query);\n          },\n\n          /**\n           * Utility function to truncate a string to a given length for debugging\n           * calls to the API while keeping the actual values mostly intact\n           * @param {string} str - The string to truncate\n           * @param {number} length - The length to truncate the string to\n           * @returns {string} The truncated string\n           */\n          middleTruncate(str, length = 5) {\n            if (str.length <= length) return str;\n            return `${str.slice(0, length)}...${str.slice(-length)}`;\n          },\n\n          /**\n           * Report citations for an array of search results.\n           * Uses title, link, and snippet directly from result data.\n           * @param {Array<{title?: string, link?: string, snippet?: string}>} results - Search results to report as citations\n           */\n          reportSearchResultsCitations: function (results) {\n            if (!Array.isArray(results)) return;\n            const citations = [];\n            for (const result of results) {\n              const fallbackUrl =\n                result.link ||\n                result.url ||\n                result.website ||\n                result.product_link ||\n                result.patent_link ||\n                result.link_clean;\n\n              citations.push({\n                id: result.link || fallbackUrl,\n                title: result.title || fallbackUrl,\n                text: result.snippet || result.description || result.text || \"\",\n                chunkSource: result.link\n                  ? `link://${result.link}`\n                  : `link://${fallbackUrl}`,\n                score: null,\n              });\n            }\n            this.super.addCitation?.(citations);\n          },\n\n          /**\n           * Use SerpApi\n           * SerpApi supports dozens of search engines across the major platforms including Google, DuckDuckGo, Bing, eBay, Amazon, Baidu, Yandex, and more.\n           * https://serpapi.com/\n           */\n          _serpApi: async function (query) {\n            if (!process.env.AGENT_SERPAPI_API_KEY) {\n              this.super.introspect(\n                `${this.caller}: I can't use SerpApi searching because the user has not defined the required API key.\\nVisit: https://serpapi.com/ to create the API key for free.`\n              );\n              return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;\n            }\n\n            this.super.introspect(\n              `${this.caller}: Using SerpApi to search for \"${\n                query.length > 100 ? `${query.slice(0, 100)}...` : query\n              }\"`\n            );\n\n            const engine = process.env.AGENT_SERPAPI_ENGINE;\n            const queryParamKey = engine === \"amazon\" ? \"k\" : \"q\";\n\n            const params = new URLSearchParams({\n              engine: engine,\n              [queryParamKey]: query,\n              api_key: process.env.AGENT_SERPAPI_API_KEY,\n            });\n\n            const url = `https://serpapi.com/search.json?${params.toString()}`;\n            const { response, error } = await fetch(url, {\n              method: \"GET\",\n              headers: {},\n            })\n              .then((res) => {\n                if (res.ok) return res.json();\n                throw new Error(\n                  `${res.status} - ${res.statusText}. params: ${JSON.stringify({ auth: this.middleTruncate(process.env.AGENT_SERPAPI_API_KEY, 5), q: query })}`\n                );\n              })\n              .then((data) => {\n                return { response: data, error: null };\n              })\n              .catch((e) => {\n                this.super.handlerProps.log(`SerpApi Error: ${e.message}`);\n                return { response: null, error: e.message };\n              });\n            if (error)\n              return `There was an error searching for content. ${error}`;\n\n            const data = [];\n\n            switch (engine) {\n              case \"google\":\n                if (response.hasOwnProperty(\"knowledge_graph\"))\n                  data.push(response.knowledge_graph);\n                if (response.hasOwnProperty(\"answer_box\"))\n                  data.push(response.answer_box);\n                response.organic_results?.forEach((searchResult) => {\n                  const { title, link, snippet } = searchResult;\n                  data.push({\n                    title,\n                    link,\n                    snippet,\n                  });\n                });\n                response.local_results?.forEach((searchResult) => {\n                  const {\n                    title,\n                    rating,\n                    reviews,\n                    description,\n                    address,\n                    website,\n                    extensions,\n                  } = searchResult;\n                  data.push({\n                    title,\n                    rating,\n                    reviews,\n                    description,\n                    address,\n                    website,\n                    extensions,\n                  });\n                });\n                break;\n              case \"google_maps\":\n                response.local_results?.slice(0, 10).forEach((searchResult) => {\n                  const {\n                    title,\n                    rating,\n                    reviews,\n                    description,\n                    address,\n                    website,\n                    extensions,\n                  } = searchResult;\n                  data.push({\n                    title,\n                    rating,\n                    reviews,\n                    description,\n                    address,\n                    website,\n                    extensions,\n                  });\n                });\n                break;\n              case \"google_images_light\":\n                response.images_results\n                  ?.slice(0, 10)\n                  .forEach((searchResult) => {\n                    const { title, source, link, thumbnail } = searchResult;\n                    data.push({\n                      title,\n                      source,\n                      link,\n                      thumbnail,\n                    });\n                  });\n                break;\n              case \"google_shopping_light\":\n                response.shopping_results\n                  ?.slice(0, 10)\n                  .forEach((searchResult) => {\n                    const {\n                      title,\n                      source,\n                      price,\n                      rating,\n                      reviews,\n                      snippet,\n                      thumbnail,\n                      product_link,\n                    } = searchResult;\n                    data.push({\n                      title,\n                      source,\n                      price,\n                      rating,\n                      reviews,\n                      snippet,\n                      thumbnail,\n                      product_link,\n                    });\n                  });\n                break;\n              case \"google_news_light\":\n                response.news_results?.slice(0, 10).forEach((searchResult) => {\n                  const { title, link, source, thumbnail, snippet, date } =\n                    searchResult;\n                  data.push({\n                    title,\n                    link,\n                    source,\n                    thumbnail,\n                    snippet,\n                    date,\n                  });\n                });\n                break;\n              case \"google_jobs\":\n                response.jobs_results?.forEach((searchResult) => {\n                  const {\n                    title,\n                    company_name,\n                    location,\n                    description,\n                    apply_options,\n                    extensions,\n                  } = searchResult;\n                  data.push({\n                    title,\n                    company_name,\n                    location,\n                    description,\n                    apply_options,\n                    extensions,\n                  });\n                });\n                break;\n              case \"google_patents\":\n                response.organic_results?.forEach((searchResult) => {\n                  const {\n                    title,\n                    patent_link,\n                    snippet,\n                    inventor,\n                    assignee,\n                    publication_number,\n                  } = searchResult;\n                  data.push({\n                    title,\n                    patent_link,\n                    snippet,\n                    inventor,\n                    assignee,\n                    publication_number,\n                  });\n                });\n                break;\n              case \"google_scholar\":\n                response.organic_results?.forEach((searchResult) => {\n                  const { title, link, snippet, publication_info } =\n                    searchResult;\n                  data.push({\n                    title,\n                    link,\n                    snippet,\n                    publication_info,\n                  });\n                });\n                break;\n              case \"baidu\":\n                if (response.hasOwnProperty(\"answer_box\"))\n                  data.push(response.answer_box);\n                response.organic_results?.forEach((searchResult) => {\n                  const { title, link, snippet } = searchResult;\n                  data.push({\n                    title,\n                    link,\n                    snippet,\n                  });\n                });\n                break;\n              case \"amazon\":\n                response.organic_results\n                  ?.slice(0, 10)\n                  .forEach((searchResult) => {\n                    const {\n                      title,\n                      rating,\n                      reviews,\n                      price,\n                      link_clean,\n                      thumbnail,\n                    } = searchResult;\n                    data.push({\n                      title,\n                      rating,\n                      reviews,\n                      price,\n                      link_clean,\n                      thumbnail,\n                    });\n                  });\n            }\n\n            if (data.length === 0)\n              return `No information was found online for the search query.`;\n\n            this.reportSearchResultsCitations(data);\n            const result = JSON.stringify(data);\n            this.super.introspect(\n              `${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`\n            );\n            return result;\n          },\n\n          /**\n           * Use SearchApi\n           * SearchApi supports multiple search engines like Google Search, Bing Search, Baidu Search, Google News, YouTube, and many more.\n           * https://www.searchapi.io/\n           */\n          _searchApi: async function (query) {\n            if (!process.env.AGENT_SEARCHAPI_API_KEY) {\n              this.super.introspect(\n                `${this.caller}: I can't use SearchApi searching because the user has not defined the required API key.\\nVisit: https://www.searchapi.io/ to create the API key for free.`\n              );\n              return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;\n            }\n\n            this.super.introspect(\n              `${this.caller}: Using SearchApi to search for \"${\n                query.length > 100 ? `${query.slice(0, 100)}...` : query\n              }\"`\n            );\n\n            const engine = process.env.AGENT_SEARCHAPI_ENGINE;\n            const params = new URLSearchParams({\n              engine: engine,\n              q: query,\n            });\n\n            const url = `https://www.searchapi.io/api/v1/search?${params.toString()}`;\n            const { response, error } = await fetch(url, {\n              method: \"GET\",\n              headers: {\n                Authorization: `Bearer ${process.env.AGENT_SEARCHAPI_API_KEY}`,\n                \"Content-Type\": \"application/json\",\n                \"X-SearchApi-Source\": \"AnythingLLM\",\n              },\n            })\n              .then((res) => {\n                if (res.ok) return res.json();\n                throw new Error(\n                  `${res.status} - ${res.statusText}. params: ${JSON.stringify({ auth: this.middleTruncate(process.env.AGENT_SEARCHAPI_API_KEY, 5), q: query })}`\n                );\n              })\n              .then((data) => {\n                return { response: data, error: null };\n              })\n              .catch((e) => {\n                this.super.handlerProps.log(`SearchApi Error: ${e.message}`);\n                return { response: null, error: e.message };\n              });\n            if (error)\n              return `There was an error searching for content. ${error}`;\n\n            const data = [];\n            if (response.hasOwnProperty(\"knowledge_graph\"))\n              data.push(response.knowledge_graph?.description);\n            if (response.hasOwnProperty(\"answer_box\"))\n              data.push(response.answer_box?.answer);\n            response.organic_results?.forEach((searchResult) => {\n              const { title, link, snippet } = searchResult;\n              data.push({\n                title,\n                link,\n                snippet,\n              });\n            });\n\n            if (data.length === 0)\n              return `No information was found online for the search query.`;\n\n            this.reportSearchResultsCitations(data);\n            const result = JSON.stringify(data);\n            this.super.introspect(\n              `${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`\n            );\n            return result;\n          },\n\n          /**\n           * Use Serper.dev\n           * Free to set up, easy to use, 2,500 calls for free one-time\n           * https://serper.dev\n           */\n          _serperDotDev: async function (query) {\n            if (!process.env.AGENT_SERPER_DEV_KEY) {\n              this.super.introspect(\n                `${this.caller}: I can't use Serper.dev searching because the user has not defined the required API key.\\nVisit: https://serper.dev to create the API key for free.`\n              );\n              return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;\n            }\n\n            this.super.introspect(\n              `${this.caller}: Using Serper.dev to search for \"${\n                query.length > 100 ? `${query.slice(0, 100)}...` : query\n              }\"`\n            );\n            const { response, error } = await fetch(\n              \"https://google.serper.dev/search\",\n              {\n                method: \"POST\",\n                headers: {\n                  \"X-API-KEY\": process.env.AGENT_SERPER_DEV_KEY,\n                  \"Content-Type\": \"application/json\",\n                },\n                body: JSON.stringify({ q: query }),\n                redirect: \"follow\",\n              }\n            )\n              .then((res) => {\n                if (res.ok) return res.json();\n                throw new Error(\n                  `${res.status} - ${res.statusText}. params: ${JSON.stringify({ auth: this.middleTruncate(process.env.AGENT_SERPER_DEV_KEY, 5), q: query })}`\n                );\n              })\n              .then((data) => {\n                return { response: data, error: null };\n              })\n              .catch((e) => {\n                this.super.handlerProps.log(`Serper.dev Error: ${e.message}`);\n                return { response: null, error: e.message };\n              });\n            if (error)\n              return `There was an error searching for content. ${error}`;\n\n            const data = [];\n            if (response.hasOwnProperty(\"knowledgeGraph\"))\n              data.push(response.knowledgeGraph);\n            response.organic?.forEach((searchResult) => {\n              const { title, link, snippet } = searchResult;\n              data.push({\n                title,\n                link,\n                snippet,\n              });\n            });\n\n            if (data.length === 0)\n              return `No information was found online for the search query.`;\n\n            this.reportSearchResultsCitations(data);\n            const result = JSON.stringify(data);\n            this.super.introspect(\n              `${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`\n            );\n            return result;\n          },\n          _bingWebSearch: async function (query) {\n            if (!process.env.AGENT_BING_SEARCH_API_KEY) {\n              this.super.introspect(\n                `${this.caller}: I can't use Bing Web Search because the user has not defined the required API key.\\nVisit: https://portal.azure.com/ to create the API key.`\n              );\n              return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;\n            }\n\n            const searchURL = new URL(\n              \"https://api.bing.microsoft.com/v7.0/search\"\n            );\n            searchURL.searchParams.append(\"q\", query);\n\n            this.super.introspect(\n              `${this.caller}: Using Bing Web Search to search for \"${\n                query.length > 100 ? `${query.slice(0, 100)}...` : query\n              }\"`\n            );\n\n            const searchResponse = await fetch(searchURL, {\n              headers: {\n                \"Ocp-Apim-Subscription-Key\":\n                  process.env.AGENT_BING_SEARCH_API_KEY,\n              },\n            })\n              .then((res) => {\n                if (res.ok) return res.json();\n                throw new Error(\n                  `${res.status} - ${res.statusText}. params: ${JSON.stringify({ auth: this.middleTruncate(process.env.AGENT_BING_SEARCH_API_KEY, 5), q: query })}`\n                );\n              })\n              .then((data) => {\n                const searchResults = data.webPages?.value || [];\n                return searchResults.map((result) => ({\n                  title: result.name,\n                  link: result.url,\n                  snippet: result.snippet,\n                }));\n              })\n              .catch((e) => {\n                this.super.handlerProps.log(\n                  `Bing Web Search Error: ${e.message}`\n                );\n                return [];\n              });\n\n            if (searchResponse.length === 0)\n              return `No information was found online for the search query.`;\n\n            this.reportSearchResultsCitations(searchResponse);\n            const result = JSON.stringify(searchResponse);\n            this.super.introspect(\n              `${this.caller}: I found ${searchResponse.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`\n            );\n            return result;\n          },\n          _serplyEngine: async function (\n            query,\n            language = \"en\",\n            hl = \"us\",\n            //eslint-disable-next-line\n            limit = 100,\n            device_type = \"desktop\",\n            proxy_location = \"US\"\n          ) {\n            //  query (str): The query to search for\n            //  hl (str): Host Language code to display results in (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages)\n            //  limit (int): The maximum number of results to return [10-100, defaults to 100]\n            //  device_type: get results based on desktop/mobile (defaults to desktop)\n\n            if (!process.env.AGENT_SERPLY_API_KEY) {\n              this.super.introspect(\n                `${this.caller}: I can't use Serply.io searching because the user has not defined the required API key.\\nVisit: https://serply.io to create the API key for free.`\n              );\n              return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;\n            }\n\n            this.super.introspect(\n              `${this.caller}: Using Serply to search for \"${\n                query.length > 100 ? `${query.slice(0, 100)}...` : query\n              }\"`\n            );\n\n            const params = new URLSearchParams({\n              q: query,\n              language: language,\n              hl,\n              gl: proxy_location.toUpperCase(),\n            });\n            const url = `https://api.serply.io/v1/search/${params.toString()}`;\n            const { response, error } = await fetch(url, {\n              method: \"GET\",\n              headers: {\n                \"X-API-KEY\": process.env.AGENT_SERPLY_API_KEY,\n                \"Content-Type\": \"application/json\",\n                \"User-Agent\": \"anything-llm\",\n                \"X-Proxy-Location\": proxy_location,\n                \"X-User-Agent\": device_type,\n              },\n            })\n              .then((res) => {\n                if (res.ok) return res.json();\n                throw new Error(\n                  `${res.status} - ${res.statusText}. params: ${JSON.stringify({ auth: this.middleTruncate(process.env.AGENT_SERPLY_API_KEY, 5), q: query })}`\n                );\n              })\n              .then((data) => {\n                if (data?.message === \"Unauthorized\")\n                  throw new Error(\n                    \"Unauthorized. Please double check your AGENT_SERPLY_API_KEY\"\n                  );\n                return { response: data, error: null };\n              })\n              .catch((e) => {\n                this.super.handlerProps.log(`Serply Error: ${e.message}`);\n                return { response: null, error: e.message };\n              });\n\n            if (error)\n              return `There was an error searching for content. ${error}`;\n\n            const data = [];\n            response.results?.forEach((searchResult) => {\n              const { title, link, description } = searchResult;\n              data.push({\n                title,\n                link,\n                snippet: description,\n              });\n            });\n\n            if (data.length === 0)\n              return `No information was found online for the search query.`;\n\n            this.reportSearchResultsCitations(data);\n            const result = JSON.stringify(data);\n            this.super.introspect(\n              `${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`\n            );\n            return result;\n          },\n          _searXNGEngine: async function (query) {\n            let searchURL;\n            if (!process.env.AGENT_SEARXNG_API_URL) {\n              this.super.introspect(\n                `${this.caller}: I can't use SearXNG searching because the user has not defined the required base URL.\\nPlease set this value in the agent skill settings.`\n              );\n              return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;\n            }\n\n            try {\n              searchURL = new URL(process.env.AGENT_SEARXNG_API_URL);\n              searchURL.searchParams.append(\"q\", encodeURIComponent(query));\n              searchURL.searchParams.append(\"format\", \"json\");\n            } catch (e) {\n              this.super.handlerProps.log(`SearXNG Search: ${e.message}`);\n              this.super.introspect(\n                `${this.caller}: I can't use SearXNG searching because the url provided is not a valid URL.`\n              );\n              return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;\n            }\n\n            this.super.introspect(\n              `${this.caller}: Using SearXNG to search for \"${\n                query.length > 100 ? `${query.slice(0, 100)}...` : query\n              }\"`\n            );\n\n            const { response, error } = await fetch(searchURL.toString(), {\n              method: \"GET\",\n              headers: {\n                \"Content-Type\": \"application/json\",\n                \"User-Agent\": \"anything-llm\",\n              },\n            })\n              .then((res) => {\n                if (res.ok) return res.json();\n                throw new Error(\n                  `${res.status} - ${res.statusText}. params: ${JSON.stringify({ url: searchURL.toString() })}`\n                );\n              })\n              .then((data) => {\n                return { response: data, error: null };\n              })\n              .catch((e) => {\n                this.super.handlerProps.log(\n                  `SearXNG Search Error: ${e.message}`\n                );\n                return { response: null, error: e.message };\n              });\n            if (error)\n              return `There was an error searching for content. ${error}`;\n\n            const data = [];\n            response.results?.forEach((searchResult) => {\n              const { url, title, content, publishedDate } = searchResult;\n              data.push({\n                title,\n                link: url,\n                snippet: content,\n                publishedDate,\n              });\n            });\n\n            if (data.length === 0)\n              return `No information was found online for the search query.`;\n\n            this.reportSearchResultsCitations(data);\n            const result = JSON.stringify(data);\n            this.super.introspect(\n              `${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`\n            );\n            return result;\n          },\n          _tavilySearch: async function (query) {\n            if (!process.env.AGENT_TAVILY_API_KEY) {\n              this.super.introspect(\n                `${this.caller}: I can't use Tavily searching because the user has not defined the required API key.\\nVisit: https://tavily.com/ to create the API key.`\n              );\n              return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;\n            }\n\n            this.super.introspect(\n              `${this.caller}: Using Tavily to search for \"${\n                query.length > 100 ? `${query.slice(0, 100)}...` : query\n              }\"`\n            );\n\n            const url = \"https://api.tavily.com/search\";\n            const { response, error } = await fetch(url, {\n              method: \"POST\",\n              headers: {\n                \"Content-Type\": \"application/json\",\n              },\n              body: JSON.stringify({\n                api_key: process.env.AGENT_TAVILY_API_KEY,\n                query: query,\n              }),\n            })\n              .then((res) => {\n                if (res.ok) return res.json();\n                throw new Error(\n                  `${res.status} - ${res.statusText}. params: ${JSON.stringify({ auth: this.middleTruncate(process.env.AGENT_TAVILY_API_KEY, 5), q: query })}`\n                );\n              })\n              .then((data) => {\n                return { response: data, error: null };\n              })\n              .catch((e) => {\n                this.super.handlerProps.log(\n                  `Tavily Search Error: ${e.message}`\n                );\n                return { response: null, error: e.message };\n              });\n\n            if (error)\n              return `There was an error searching for content. ${error}`;\n\n            const data = [];\n            response.results?.forEach((searchResult) => {\n              const { title, url, content } = searchResult;\n              data.push({\n                title,\n                link: url,\n                snippet: content,\n              });\n            });\n\n            if (data.length === 0)\n              return `No information was found online for the search query.`;\n\n            this.reportSearchResultsCitations(data);\n            const result = JSON.stringify(data);\n            this.super.introspect(\n              `${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`\n            );\n            return result;\n          },\n          _duckDuckGoEngine: async function (query) {\n            /**\n             * Extract the actual destination URL from a DuckDuckGo redirect link.\n             * DDG links look like: //duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com&rut=...\n             * @param {string} ddgLink - The DuckDuckGo redirect link\n             * @returns {string} The actual destination URL\n             */\n            function extractUrl(ddgLink) {\n              if (!ddgLink) return ddgLink;\n              try {\n                const fullUrl = ddgLink.startsWith(\"//\")\n                  ? `https:${ddgLink}`\n                  : ddgLink;\n                const url = new URL(fullUrl);\n                const actualUrl = url.searchParams.get(\"uddg\");\n                return actualUrl ? decodeURIComponent(actualUrl) : ddgLink;\n              } catch {\n                return ddgLink;\n              }\n            }\n\n            this.super.introspect(\n              `${this.caller}: Using DuckDuckGo to search for \"${\n                query.length > 100 ? `${query.slice(0, 100)}...` : query\n              }\"`\n            );\n\n            const searchURL = new URL(\"https://html.duckduckgo.com/html\");\n            searchURL.searchParams.append(\"q\", query);\n\n            const response = await fetch(searchURL.toString())\n              .then((res) => {\n                if (res.ok) return res.text();\n                throw new Error(\n                  `${res.status} - ${res.statusText}. params: ${JSON.stringify({ url: searchURL.toString() })}`\n                );\n              })\n              .catch((e) => {\n                this.super.handlerProps.log(\n                  `DuckDuckGo Search Error: ${e.message}`\n                );\n                return null;\n              });\n\n            if (!response) return `There was an error searching DuckDuckGo.`;\n            const html = response;\n            const data = [];\n            const results = html.split('<div class=\"result results_links');\n\n            // Skip first element since it's before the first result\n            for (let i = 1; i < results.length; i++) {\n              const result = results[i];\n\n              // Extract title\n              const titleMatch = result.match(\n                /<a[^>]*class=\"result__a\"[^>]*>(.*?)<\\/a>/\n              );\n              const title = titleMatch ? titleMatch[1].trim() : \"\";\n\n              // Extract URL and clean DDG redirect\n              const urlMatch = result.match(\n                /<a[^>]*class=\"result__a\"[^>]*href=\"([^\"]*)\">/\n              );\n              const link = extractUrl(urlMatch ? urlMatch[1] : \"\");\n\n              // Extract snippet\n              const snippetMatch = result.match(\n                /<a[^>]*class=\"result__snippet\"[^>]*>(.*?)<\\/a>/\n              );\n              const snippet = snippetMatch\n                ? snippetMatch[1].replace(/<\\/?b>/g, \"\").trim()\n                : \"\";\n\n              if (title && link && snippet) {\n                data.push({ title, link, snippet });\n              }\n            }\n\n            if (data.length === 0) {\n              return `No information was found online for the search query.`;\n            }\n\n            this.reportSearchResultsCitations(data);\n            const result = JSON.stringify(data);\n            this.super.introspect(\n              `${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`\n            );\n            return result;\n          },\n          _exaSearch: async function (query) {\n            if (!process.env.AGENT_EXA_API_KEY) {\n              this.super.introspect(\n                `${this.caller}: I can't use Exa searching because the user has not defined the required API key.\\nVisit: https://exa.ai to create the API key.`\n              );\n              return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;\n            }\n\n            this.super.introspect(\n              `${this.caller}: Using Exa to search for \"${\n                query.length > 100 ? `${query.slice(0, 100)}...` : query\n              }\"`\n            );\n\n            const url = \"https://api.exa.ai/search\";\n            const { response, error } = await fetch(url, {\n              method: \"POST\",\n              headers: {\n                \"Content-Type\": \"application/json\",\n                \"x-api-key\": process.env.AGENT_EXA_API_KEY,\n              },\n              body: JSON.stringify({\n                query: query,\n                type: \"auto\",\n                numResults: 10,\n                contents: {\n                  text: true,\n                },\n              }),\n            })\n              .then((res) => {\n                if (res.ok) return res.json();\n                throw new Error(\n                  `${res.status} - ${res.statusText}. params: ${JSON.stringify({ auth: this.middleTruncate(process.env.AGENT_EXA_API_KEY, 5), q: query })}`\n                );\n              })\n              .then((data) => {\n                return { response: data, error: null };\n              })\n              .catch((e) => {\n                this.super.handlerProps.log(`Exa Search Error: ${e.message}`);\n                return { response: null, error: e.message };\n              });\n\n            if (error)\n              return `There was an error searching for content. ${error}`;\n\n            const data = [];\n            response.results?.forEach((searchResult) => {\n              const { title, url, text, publishedDate } = searchResult;\n              data.push({\n                title,\n                link: url,\n                snippet: text,\n                publishedDate,\n              });\n            });\n\n            if (data.length === 0)\n              return `No information was found online for the search query.`;\n\n            this.reportSearchResultsCitations(data);\n            const result = JSON.stringify(data);\n            this.super.introspect(\n              `${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`\n            );\n            return result;\n          },\n\n          _perplexitySearch: async function (query) {\n            if (!process.env.AGENT_PERPLEXITY_API_KEY) {\n              this.super.introspect(\n                `${this.caller}: I can't use Perplexity searching because the user has not defined the required API key.\\nVisit: [https://console.perplexity.ai](https://console.perplexity.ai) to create the API key.`\n              );\n              return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;\n            }\n\n            this.super.introspect(\n              `${this.caller}: Using Perplexity to search for \"${\n                query.length > 100 ? `${query.slice(0, 100)}...` : query\n              }\"`\n            );\n\n            const { response, error } = await fetch(\n              \"https://api.perplexity.ai/search\",\n              {\n                method: \"POST\",\n                headers: {\n                  \"Content-Type\": \"application/json\",\n                  Authorization: `Bearer ${process.env.AGENT_PERPLEXITY_API_KEY}`,\n                },\n                body: JSON.stringify({\n                  query: query,\n                  max_results: 5,\n                  max_tokens_per_page: 2048,\n                }),\n              }\n            )\n              .then((res) => {\n                if (res.ok) return res.json();\n                throw new Error(\n                  `${res.status} - ${res.statusText}. params: ${JSON.stringify({\n                    auth: this.middleTruncate(\n                      process.env.AGENT_PERPLEXITY_API_KEY,\n                      5\n                    ),\n                    q: query,\n                  })}`\n                );\n              })\n              .then((data) => {\n                return { response: data, error: null };\n              })\n              .catch((e) => {\n                this.super.handlerProps.log(\n                  `Perplexity Search Error: ${e.message}`\n                );\n                return { response: null, error: e.message };\n              });\n\n            if (error)\n              return `There was an error searching for content. ${error}`;\n\n            const data = [];\n            if (response.results) {\n              response.results.forEach((result) => {\n                data.push({\n                  title: result.title,\n                  link: result.url,\n                  snippet: result.snippet || \"\",\n                });\n              });\n            }\n\n            if (data.length === 0)\n              return \"No information was found online for the search query.\";\n\n            this.reportSearchResultsCitations(data);\n\n            const result = JSON.stringify(data);\n            this.super.introspect(\n              `${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`\n            );\n\n            return result;\n          },\n        });\n      },\n    };\n  },\n};\n\nmodule.exports = {\n  webBrowsing,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/web-scraping.js",
    "content": "const { CollectorApi } = require(\"../../../collectorApi\");\nconst Provider = require(\"../providers/ai-provider\");\nconst { summarizeContent } = require(\"../utils/summarize\");\n\nconst webScraping = {\n  name: \"web-scraping\",\n  startupConfig: {\n    params: {},\n  },\n  plugin: function () {\n    return {\n      name: this.name,\n      setup(aibitat) {\n        aibitat.function({\n          super: aibitat,\n          name: this.name,\n          controller: new AbortController(),\n          description:\n            \"Read and extract content from a specific webpage URL. Fetch the text from a website, get the contents of a link, or visit a URL to see what it says. Use when you have a specific web address to read.\",\n          examples: [\n            {\n              prompt: \"Read that URL for me\",\n              call: JSON.stringify({ url: \"https://example.com\" }),\n            },\n            {\n              prompt: \"What is anythingllm.com about?\",\n              call: JSON.stringify({ url: \"https://anythingllm.com\" }),\n            },\n            {\n              prompt: \"Scrape https://example.com\",\n              call: JSON.stringify({ url: \"https://example.com\" }),\n            },\n          ],\n          parameters: {\n            $schema: \"http://json-schema.org/draft-07/schema#\",\n            type: \"object\",\n            properties: {\n              url: {\n                type: \"string\",\n                format: \"uri\",\n                description:\n                  \"A complete web address URL including protocol. Assumes https if not provided.\",\n              },\n            },\n            additionalProperties: false,\n          },\n          handler: async function ({ url }) {\n            try {\n              if (url) return await this.scrape(url);\n              return \"There is nothing we can do. This function call returns no information.\";\n            } catch (error) {\n              this.super.handlerProps.log(\n                `Web Scraping Error: ${error.message}`\n              );\n              this.super.introspect(\n                `${this.caller}: Web Scraping Error: ${error.message}`\n              );\n              return `There was an error while calling the function. No data or response was found. Let the user know this was the error: ${error.message}`;\n            }\n          },\n\n          /**\n           * Report a URL citation to be displayed in the chat UI.\n           * @param {string} url - The URL that was accessed\n           * @param {string} content - The content retrieved from the URL\n           */\n          reportUrlCitation: function (url, content) {\n            try {\n              const urlObj = new URL(url);\n              this.super.addCitation?.({\n                id: url,\n                title: urlObj.hostname + urlObj.pathname,\n                text: content,\n                chunkSource: `link://${url}`,\n                score: null,\n              });\n            } catch {\n              // URL parsing failed, still add citation without parsed title\n              this.super.addCitation?.({\n                id: url,\n                title: url,\n                text: content,\n                chunkSource: `link://${url}`,\n                score: null,\n              });\n            }\n          },\n\n          /**\n           * Scrape a website and summarize the content based on objective if the content is too large.\n           * Objective is the original objective & task that user give to the agent, url is the url of the website to be scraped.\n           * Here we can leverage the document collector to get raw website text quickly.\n           *\n           * @param url\n           * @returns\n           */\n          scrape: async function (url) {\n            this.super.introspect(\n              `${this.caller}: Scraping the content of ${url}`\n            );\n            const { success, content } =\n              await new CollectorApi().getLinkContent(url);\n\n            if (!success) {\n              this.super.introspect(\n                `${this.caller}: could not scrape ${url}. I can't use this page's content.`\n              );\n              throw new Error(\n                `URL could not be scraped and no content was found.`\n              );\n            }\n\n            if (!content || content?.length === 0) {\n              throw new Error(\"There was no content to be collected or read.\");\n            }\n\n            this.reportUrlCitation(url, content);\n            const { TokenManager } = require(\"../../../helpers/tiktoken\");\n            const tokenEstimate = new TokenManager(\n              this.super.model\n            ).countFromString(content);\n            if (\n              tokenEstimate <\n              Provider.contextLimit(this.super.provider, this.super.model)\n            ) {\n              this.super.introspect(\n                `${this.caller}: Looking over the content of the page. ~${tokenEstimate} tokens.`\n              );\n              return content;\n            }\n\n            this.super.introspect(\n              `${this.caller}: This page's content exceeds the model's context limit. Summarizing it right now.`\n            );\n            this.super.onAbort(() => {\n              this.super.handlerProps.log(\n                \"Abort was triggered, exiting summarization early.\"\n              );\n              this.controller.abort();\n            });\n\n            return summarizeContent({\n              provider: this.super.provider,\n              model: this.super.model,\n              controllerSignal: this.controller.signal,\n              content,\n            });\n          },\n        });\n      },\n    };\n  },\n};\n\nmodule.exports = {\n  webScraping,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/plugins/websocket.js",
    "content": "const chalk = require(\"chalk\");\nconst { Telemetry } = require(\"../../../../models/telemetry\");\nconst SOCKET_TIMEOUT_MS = 300 * 1_000; // 5 mins\n\n/**\n * Websocket Interface plugin. It prints the messages on the console and asks for feedback\n * while the conversation is running in the background.\n */\n\n// export interface AIbitatWebSocket extends ServerWebSocket<unknown> {\n//   askForFeedback?: any\n//   awaitResponse?: any\n//   handleFeedback?: (message: string) => void;\n// }\n\nconst WEBSOCKET_BAIL_COMMANDS = [\n  \"exit\",\n  \"/exit\",\n  \"stop\",\n  \"/stop\",\n  \"halt\",\n  \"/halt\",\n  \"/reset\", // Will not reset but will bail. Powerusers always do this and the LLM responds.\n];\nconst websocket = {\n  name: \"websocket\",\n  startupConfig: {\n    params: {\n      socket: {\n        required: true,\n      },\n      muteUserReply: {\n        required: false,\n        default: true,\n      },\n      introspection: {\n        required: false,\n        default: true,\n      },\n    },\n  },\n  plugin: function ({\n    socket, // @type AIbitatWebSocket\n    muteUserReply = true, // Do not post messages to \"USER\" back to frontend.\n    introspection = false, // when enabled will attach socket to Aibitat object with .introspect method which reports status updates to frontend.\n  }) {\n    return {\n      name: this.name,\n      setup(aibitat) {\n        aibitat.onError(async (error) => {\n          let errorMessage =\n            error?.message || \"An error occurred while running the agent.\";\n          console.error(chalk.red(`   error: ${errorMessage}`), error);\n          aibitat.introspect(\n            `Error encountered while running: ${errorMessage}`\n          );\n          socket.send(\n            JSON.stringify({ type: \"wssFailure\", content: errorMessage })\n          );\n          aibitat.terminate();\n        });\n\n        aibitat.introspect = (messageText) => {\n          if (!introspection) return; // Dump thoughts when not wanted.\n          socket.send(\n            JSON.stringify({\n              type: \"statusResponse\",\n              content: messageText,\n              animate: true,\n            })\n          );\n        };\n\n        // expose function for sockets across aibitat\n        // type param must be set or else msg will not be shown or handled in UI.\n        aibitat.socket = {\n          send: (type = \"__unhandled\", content = \"\") => {\n            socket.send(JSON.stringify({ type, content }));\n          },\n        };\n\n        // aibitat.onStart(() => {\n        //   console.log(\"🚀 starting chat ...\");\n        // });\n\n        aibitat.onMessage((message) => {\n          if (message.from !== \"USER\")\n            Telemetry.sendTelemetry(\"agent_chat_sent\");\n          if (message.from === \"USER\" && muteUserReply) return;\n          socket.send(JSON.stringify(message));\n        });\n\n        aibitat.onTerminate(() => {\n          // console.log(\"🚀 chat finished\");\n          socket.close();\n        });\n\n        aibitat.onInterrupt(async (node) => {\n          const { feedback, attachments } = await socket.askForFeedback(\n            socket,\n            node\n          );\n          if (WEBSOCKET_BAIL_COMMANDS.includes(feedback)) {\n            socket.close();\n            return;\n          }\n\n          await aibitat.continue(feedback, attachments);\n        });\n\n        /**\n         * Socket wait for feedback on socket\n         *\n         * @param socket The content to summarize. // AIbitatWebSocket & { receive: any, echo: any }\n         * @param node The chat node // { from: string; to: string }\n         * @returns {{ feedback: string, attachments: Array }} The feedback and any attachments.\n         */\n        socket.askForFeedback = (socket, node) => {\n          socket.awaitResponse = (question = \"waiting...\") => {\n            socket.send(JSON.stringify({ type: \"WAITING_ON_INPUT\", question }));\n\n            return new Promise(function (resolve) {\n              let socketTimeout = null;\n              socket.handleFeedback = (message) => {\n                const data = JSON.parse(message);\n                if (data.type !== \"awaitingFeedback\") return;\n                delete socket.handleFeedback;\n                clearTimeout(socketTimeout);\n                resolve({\n                  feedback: data.feedback,\n                  attachments: data.attachments || [],\n                });\n                return;\n              };\n\n              socketTimeout = setTimeout(() => {\n                console.log(\n                  chalk.red(\n                    `Client took too long to respond, chat thread is dead after ${SOCKET_TIMEOUT_MS}ms`\n                  )\n                );\n                resolve({ feedback: \"exit\", attachments: [] });\n                return;\n              }, SOCKET_TIMEOUT_MS);\n            });\n          };\n\n          return socket.awaitResponse(`Provide feedback to ${chalk.yellow(\n            node.to\n          )} as ${chalk.yellow(node.from)}.\n           Press enter to skip and use auto-reply, or type 'exit' to end the conversation: \\n`);\n        };\n        // console.log(\"🚀 WS plugin is complete.\");\n      },\n    };\n  },\n};\n\nmodule.exports = {\n  websocket,\n  WEBSOCKET_BAIL_COMMANDS,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/ai-provider.js",
    "content": "/**\n * A service that provides an AI client to create a completion.\n */\n\n/**\n * @typedef {Object} LangChainModelConfig\n * @property {(string|null)} baseURL - Override the default base URL process.env for this provider\n * @property {(string|null)} apiKey - Override the default process.env for this provider\n * @property {(number|null)} temperature - Override the default temperature\n * @property {(string|null)} model -  Overrides model used for provider.\n */\n\nconst { v4 } = require(\"uuid\");\nconst { ChatOpenAI } = require(\"@langchain/openai\");\nconst { ChatAnthropic } = require(\"@langchain/anthropic\");\nconst { ChatCohere } = require(\"@langchain/cohere\");\nconst { ChatOllama } = require(\"@langchain/community/chat_models/ollama\");\nconst { toValidNumber, safeJsonParse } = require(\"../../../http\");\nconst { getLLMProviderClass } = require(\"../../../helpers\");\nconst { parseLMStudioBasePath } = require(\"../../../AiProviders/lmStudio\");\nconst {\n  parseDockerModelRunnerEndpoint,\n} = require(\"../../../AiProviders/dockerModelRunner\");\nconst { parseFoundryBasePath } = require(\"../../../AiProviders/foundry\");\nconst {\n  SystemPromptVariables,\n} = require(\"../../../../models/systemPromptVariables\");\nconst {\n  createBedrockChatClient,\n} = require(\"../../../AiProviders/bedrock/utils\");\nconst { OllamaAILLM } = require(\"../../../AiProviders/ollama\");\n\nconst DEFAULT_WORKSPACE_PROMPT =\n  \"You are a helpful ai assistant who can assist the user and use tools available to help answer the users prompts and questions.\";\n\n/**\n * @typedef {Object} ProviderUsageMetrics\n * @property {number} prompt_tokens - Number of tokens in the prompt/input\n * @property {number} completion_tokens - Number of tokens in the completion/output\n * @property {number} total_tokens - Total tokens used\n * @property {number} duration - Duration in seconds\n * @property {number} outputTps - Output tokens per second\n * @property {string} model - Model name\n * @property {Date} timestamp - Timestamp of the completion\n */\n\nclass Provider {\n  _client;\n\n  /**\n   * The invocation object containing the user ID and other invocation details.\n   * @type {import(\"@prisma/client\").workspace_agent_invocations}\n   */\n  invocation = {};\n\n  /**\n   * The user ID for the chat completion to send to the LLM provider for user tracking.\n   * In order for this to be set, the handler props must be attached to the provider after instantiation.\n   * ex: this.attachHandlerProps({ ..., invocation: { ..., user_id: 123 } });\n   * eg: `user_123`\n   * @type {string}\n   */\n  executingUserId = \"\";\n\n  /**\n   * Stores the usage metrics from the last completion call.\n   * @type {ProviderUsageMetrics}\n   */\n  lastUsage = {\n    prompt_tokens: 0,\n    completion_tokens: 0,\n    total_tokens: 0,\n    duration: 0,\n    outputTps: 0,\n    model: null,\n    provider: null,\n    timestamp: null,\n  };\n\n  /**\n   * Timestamp when the current request started (for duration calculation).\n   * @type {number}\n   */\n  _requestStartTime = 0;\n\n  constructor(client) {\n    if (this.constructor == Provider) {\n      return;\n    }\n    this._client = client;\n  }\n\n  providerLog(text, ...args) {\n    console.log(\n      `\\x1b[36m[AgentLLM${this?.model ? ` - ${this.model}` : \"\"}]\\x1b[0m ${text}`,\n      ...args\n    );\n  }\n\n  /**\n   * Attaches handler props to the provider for reuse in the provider.\n   * - Explicitly sets the invocation object.\n   * - Explicitly sets the executing user ID from the invocation object.\n   * @param {Object} handlerProps - The handler props to attach to the provider.\n   */\n  attachHandlerProps(handlerProps = {}) {\n    this.invocation = handlerProps?.invocation || {};\n    this.executingUserId = this.invocation?.user_id\n      ? `user_${this.invocation.user_id}`\n      : \"\";\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  /**\n   * Whether this provider supports native tool calling via the ENV flag.\n   * @param {string} providerTag - The tag of the provider to check (e.g. \"bedrock\", \"openrouter\", \"groq\", etc.).\n   * @returns {boolean}\n   */\n  supportsNativeToolCallingViaEnv(providerTag = \"\") {\n    if (!(\"PROVIDER_SUPPORTS_NATIVE_TOOL_CALLING\" in process.env)) return false;\n    if (!providerTag) return false;\n    return (\n      process.env.PROVIDER_SUPPORTS_NATIVE_TOOL_CALLING?.includes(\n        providerTag\n      ) || false\n    );\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  /**\n   *\n   * @param {string} provider - the string key of the provider LLM being loaded.\n   * @param {LangChainModelConfig} config - Config to be used to override default connection object.\n   * @returns\n   */\n  static LangChainChatModel(provider = \"openai\", config = {}) {\n    switch (provider) {\n      // Cloud models\n      case \"openai\":\n        return new ChatOpenAI({\n          apiKey: process.env.OPEN_AI_KEY,\n          ...config,\n        });\n      case \"anthropic\":\n        return new ChatAnthropic({\n          apiKey: process.env.ANTHROPIC_API_KEY,\n          ...config,\n        });\n      case \"groq\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://api.groq.com/openai/v1\",\n          },\n          apiKey: process.env.GROQ_API_KEY,\n          ...config,\n        });\n      case \"mistral\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://api.mistral.ai/v1\",\n          },\n          apiKey: process.env.MISTRAL_API_KEY ?? null,\n          ...config,\n        });\n      case \"openrouter\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://openrouter.ai/api/v1\",\n            defaultHeaders: {\n              \"HTTP-Referer\": \"https://anythingllm.com\",\n              \"X-Title\": \"AnythingLLM\",\n            },\n          },\n          apiKey: process.env.OPENROUTER_API_KEY ?? null,\n          ...config,\n        });\n      case \"perplexity\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://api.perplexity.ai\",\n          },\n          apiKey: process.env.PERPLEXITY_API_KEY ?? null,\n          ...config,\n        });\n      case \"togetherai\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://api.together.xyz/v1\",\n          },\n          apiKey: process.env.TOGETHER_AI_API_KEY ?? null,\n          ...config,\n        });\n      case \"generic-openai\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: process.env.GENERIC_OPEN_AI_BASE_PATH,\n          },\n          apiKey: process.env.GENERIC_OPEN_AI_API_KEY,\n          maxTokens: toValidNumber(\n            process.env.GENERIC_OPEN_AI_MAX_TOKENS,\n            1024\n          ),\n          ...config,\n        });\n      case \"bedrock\":\n        return createBedrockChatClient(config);\n      case \"fireworksai\":\n        return new ChatOpenAI({\n          apiKey: process.env.FIREWORKS_AI_LLM_API_KEY,\n          ...config,\n        });\n      case \"apipie\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://apipie.ai/v1\",\n          },\n          apiKey: process.env.APIPIE_LLM_API_KEY ?? null,\n          ...config,\n        });\n      case \"deepseek\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://api.deepseek.com/v1\",\n          },\n          apiKey: process.env.DEEPSEEK_API_KEY ?? null,\n          ...config,\n        });\n      case \"xai\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://api.x.ai/v1\",\n          },\n          apiKey: process.env.XAI_LLM_API_KEY ?? null,\n          ...config,\n        });\n      case \"zai\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://api.z.ai/api/paas/v4\",\n          },\n          apiKey: process.env.ZAI_API_KEY ?? null,\n          ...config,\n        });\n      case \"novita\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://api.novita.ai/v3/openai\",\n          },\n          apiKey: process.env.NOVITA_LLM_API_KEY ?? null,\n          ...config,\n        });\n      case \"ppio\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://api.ppinfra.com/v3/openai\",\n          },\n          apiKey: process.env.PPIO_API_KEY ?? null,\n          ...config,\n        });\n      case \"gemini\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n          },\n          apiKey: process.env.GEMINI_API_KEY ?? null,\n          ...config,\n        });\n      case \"moonshotai\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://api.moonshot.ai/v1\",\n          },\n          apiKey: process.env.MOONSHOT_AI_API_KEY ?? null,\n          ...config,\n        });\n      case \"cometapi\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://api.cometapi.com/v1\",\n          },\n          apiKey: process.env.COMETAPI_LLM_API_KEY ?? null,\n          ...config,\n        });\n      case \"giteeai\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://ai.gitee.com/v1\",\n          },\n          apiKey: process.env.GITEE_AI_API_KEY ?? null,\n          ...config,\n        });\n      case \"cohere\":\n        return new ChatCohere({\n          apiKey: process.env.COHERE_API_KEY ?? null,\n          ...config,\n        });\n      case \"privatemode\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: process.env.PRIVATEMODE_LLM_BASE_PATH,\n          },\n          apiKey: null,\n          ...config,\n        });\n      case \"sambanova\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: \"https://api.sambanova.ai/v1\",\n          },\n          apiKey: process.env.SAMBANOVA_LLM_API_KEY ?? null,\n          ...config,\n        });\n      // OSS Model Runners\n      // case \"anythingllm_ollama\":\n      //   return new ChatOllama({\n      //     baseUrl: process.env.PLACEHOLDER,\n      //     ...config,\n      //   });\n      case \"ollama\":\n        return OllamaLangchainChatModel.create(config);\n      case \"lmstudio\": {\n        const apiKey = process.env.LMSTUDIO_AUTH_TOKEN ?? null;\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: parseLMStudioBasePath(process.env.LMSTUDIO_BASE_PATH),\n          },\n          apiKey: apiKey || \"not-used\",\n          ...config,\n        });\n      }\n      case \"koboldcpp\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: process.env.KOBOLD_CPP_BASE_PATH,\n          },\n          apiKey: \"not-used\",\n          ...config,\n        });\n      case \"localai\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: process.env.LOCAL_AI_BASE_PATH,\n          },\n          apiKey: process.env.LOCAL_AI_API_KEY ?? \"not-used\",\n          ...config,\n        });\n      case \"textgenwebui\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: process.env.TEXT_GEN_WEB_UI_BASE_PATH,\n          },\n          apiKey: process.env.TEXT_GEN_WEB_UI_API_KEY ?? \"not-used\",\n          ...config,\n        });\n      case \"litellm\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: process.env.LITE_LLM_BASE_PATH,\n          },\n          apiKey: process.env.LITE_LLM_API_KEY ?? null,\n          ...config,\n        });\n      case \"nvidia-nim\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: process.env.NVIDIA_NIM_LLM_BASE_PATH,\n          },\n          apiKey: null,\n          ...config,\n        });\n      case \"foundry\": {\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: parseFoundryBasePath(process.env.FOUNDRY_BASE_PATH),\n          },\n          apiKey: null,\n          ...config,\n        });\n      }\n      case \"docker-model-runner\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: parseDockerModelRunnerEndpoint(\n              process.env.DOCKER_MODEL_RUNNER_BASE_PATH\n            ),\n          },\n          apiKey: null,\n          ...config,\n        });\n      case \"lemonade\":\n        return new ChatOpenAI({\n          configuration: {\n            baseURL: process.env.LEMONADE_LLM_BASE_PATH,\n          },\n          apiKey: null,\n          ...config,\n        });\n      default:\n        throw new Error(`Unsupported provider ${provider} for this task.`);\n    }\n  }\n\n  /**\n   * Get the context limit for a provider/model combination using static method in AIProvider class.\n   * @param {string} provider\n   * @param {string} modelName\n   * @returns {number}\n   */\n  static contextLimit(provider = \"openai\", modelName) {\n    const llm = getLLMProviderClass({ provider });\n    if (!llm || !llm.hasOwnProperty(\"promptWindowLimit\")) return 8_000;\n    return llm.promptWindowLimit(modelName);\n  }\n\n  static defaultSystemPromptForProvider(provider = null) {\n    switch (provider) {\n      case \"lmstudio\":\n        return \"You are a helpful ai assistant who can assist the user and use tools available to help answer the users prompts and questions. Tools will be handled by another assistant and you will simply receive their responses to help answer the user prompt - always try to answer the user's prompt the best you can with the context available to you and your general knowledge.\";\n      default:\n        return DEFAULT_WORKSPACE_PROMPT;\n    }\n  }\n\n  /**\n   * Get the system prompt for a provider.\n   * @param {string} provider\n   * @param {import(\"@prisma/client\").workspaces | null} workspace\n   * @param {import(\"@prisma/client\").users | null} user\n   * @returns {Promise<string>}\n   */\n  static async systemPrompt({\n    provider = null,\n    workspace = null,\n    user = null,\n  }) {\n    if (!workspace?.openAiPrompt)\n      return Provider.defaultSystemPromptForProvider(provider);\n    return await SystemPromptVariables.expandSystemPromptVariables(\n      workspace.openAiPrompt,\n      user?.id || null,\n      workspace.id\n    );\n  }\n\n  /**\n   * Whether the provider supports agent streaming.\n   * Disabled by default and needs to be explicitly enabled in the provider\n   * This is temporary while we migrate all providers to support agent streaming\n   * @returns {boolean}\n   */\n  get supportsAgentStreaming() {\n    return false;\n  }\n\n  /**\n   * Format a single message with attachments (images) for multimodal content.\n   * Transforms a message with attachments into the OpenAI-compatible multimodal format.\n   * Can be overridden by provider subclasses for provider-specific formats.\n   * @param {Object} message - The message to format\n   * @returns {Object} - Message formatted for the API\n   */\n  formatMessageWithAttachments(message) {\n    if (!message.attachments || message.attachments.length === 0) {\n      return message;\n    }\n\n    // Transform message with attachments into multimodal format\n    const content = [{ type: \"text\", text: message.content }];\n    for (const attachment of message.attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n        },\n      });\n    }\n\n    // Return message without attachments property, with content as array\n    const { attachments: _, ...rest } = message;\n    return {\n      ...rest,\n      content,\n    };\n  }\n\n  /**\n   * Resets the usage metrics to zero and starts the request timer.\n   * Call this before each completion to ensure accurate per-call metrics.\n   */\n  resetUsage() {\n    this._requestStartTime = Date.now();\n    this.lastUsage = {\n      prompt_tokens: 0,\n      completion_tokens: 0,\n      total_tokens: 0,\n      outputTps: 0,\n      duration: 0,\n      model: null,\n      provider: null,\n      timestamp: null,\n    };\n  }\n\n  /**\n   * Formats an array of messages to handle attachments (images) for multimodal content.\n   * @param {Array<{role: string, content: string, attachments?: Array}>} messages\n   * @returns {Array} - Messages formatted for the API\n   */\n  formatMessagesWithAttachments(messages = []) {\n    return messages.map((message) =>\n      this.formatMessageWithAttachments(message)\n    );\n  }\n\n  /**\n   * Updates the stored usage metrics from a provider response.\n   * Override in subclasses to handle provider-specific usage formats.\n   * @param {Object} usage - The usage object from the provider response\n   */\n  recordUsage(usage = {}) {\n    let duration = 0;\n    if (this._requestStartTime > 0) {\n      duration = (Date.now() - this._requestStartTime) / 1000;\n    }\n\n    const promptTokens = usage.prompt_tokens || usage.input_tokens || 0;\n    const completionTokens =\n      usage.completion_tokens || usage.output_tokens || 0;\n\n    this.lastUsage = {\n      prompt_tokens: promptTokens,\n      completion_tokens: completionTokens,\n      total_tokens: usage.total_tokens || promptTokens + completionTokens,\n      outputTps:\n        completionTokens && duration > 0 ? completionTokens / duration : 0,\n      duration,\n      model: this.model,\n      provider: this.constructor.name,\n      timestamp: new Date(),\n    };\n  }\n\n  /**\n   * Get the usage metrics from the last completion.\n   * @returns {ProviderUsageMetrics} The usage metrics\n   */\n  getUsage() {\n    return { ...this.lastUsage };\n  }\n\n  /**\n   * Stream a chat completion from the LLM with tool calling\n   * Note: This using the OpenAI API format and may need to be adapted for other providers.\n   *\n   * @param {any[]} messages - The messages to send to the LLM.\n   * @param {any[]} functions - The functions to use in the LLM.\n   * @param {function} eventHandler - The event handler to use to report stream events.\n   * @returns {Promise<{ functionCall: any, textResponse: string }>} - The result of the chat completion.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    this.providerLog(\"Provider.stream - will process this chat completion.\");\n    const msgUUID = v4();\n    const formattedMessages = this.formatMessagesWithAttachments(messages);\n    const stream = await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages: formattedMessages,\n      ...(Array.isArray(functions) && functions?.length > 0\n        ? { functions }\n        : {}),\n    });\n\n    const result = {\n      functionCall: null,\n      textResponse: \"\",\n    };\n\n    for await (const chunk of stream) {\n      if (!chunk?.choices?.[0]) continue; // Skip if no choices\n      const choice = chunk.choices[0];\n\n      if (choice.delta?.content) {\n        result.textResponse += choice.delta.content;\n        eventHandler?.(\"reportStreamEvent\", {\n          type: \"textResponseChunk\",\n          uuid: msgUUID,\n          content: choice.delta.content,\n        });\n      }\n\n      if (choice.delta?.function_call) {\n        // accumulate the function call\n        if (result.functionCall)\n          result.functionCall.arguments += choice.delta.function_call.arguments;\n        else result.functionCall = choice.delta.function_call;\n\n        eventHandler?.(\"reportStreamEvent\", {\n          uuid: `${msgUUID}:tool_call_invocation`,\n          type: \"toolCallInvocation\",\n          content: `Assembling Tool Call: ${result.functionCall.name}(${result.functionCall.arguments})`,\n        });\n      }\n    }\n\n    // If there are arguments, parse them as json so that the tools can use them\n    if (!!result.functionCall?.arguments)\n      result.functionCall.arguments = safeJsonParse(\n        result.functionCall.arguments,\n        {}\n      );\n\n    return {\n      textResponse: result.textResponse,\n      functionCall: result.functionCall,\n    };\n  }\n}\n\n// Langchain Wrappers\n\n/**\n * Ollama Langchain Chat Model that supports passing in context window options\n * so that context window preferences are respected between Ollama chat/agent and in\n * Langchain tooling.\n */\nclass OllamaLangchainChatModel {\n  static create(config = {}) {\n    return new ChatOllama({\n      baseUrl: process.env.OLLAMA_BASE_PATH,\n      ...this.queryOptions(config),\n      ...config,\n    });\n  }\n\n  static queryOptions(config = {}) {\n    const model = config?.model || process.env.OLLAMA_MODEL_PREF;\n    return {\n      num_ctx: OllamaAILLM.promptWindowLimit(model),\n    };\n  }\n}\n\nmodule.exports = Provider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/anthropic.js",
    "content": "const Anthropic = require(\"@anthropic-ai/sdk\");\nconst { RetryError } = require(\"../error.js\");\nconst Provider = require(\"./ai-provider.js\");\nconst { v4 } = require(\"uuid\");\nconst { safeJsonParse } = require(\"../../../http\");\n\n/**\n * The agent provider for the Anthropic API.\n * By default, the model is set to 'claude-2'.\n */\nclass AnthropicProvider extends Provider {\n  model;\n\n  constructor(config = {}) {\n    const {\n      options = {\n        apiKey: process.env.ANTHROPIC_API_KEY,\n        maxRetries: 3,\n      },\n      model = \"claude-3-5-sonnet-20240620\",\n    } = config;\n\n    const client = new Anthropic(options);\n\n    super(client);\n    this.model = model;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * - Anthropic always supports tool calling.\n   * @returns {boolean}\n   */\n  supportsNativeToolCalling() {\n    return true;\n  }\n\n  /**\n   * Parses the cache control ENV variable\n   *\n   * If caching is enabled, we can pass less than 1024 tokens and Anthropic will just\n   * ignore it unless it is above the model's minimum. Since this feature is opt-in\n   * we can safely assume that if caching is enabled that we should just pass the content as is.\n   * https://docs.claude.com/en/docs/build-with-claude/prompt-caching#cache-limitations\n   *\n   * @param {string} value - The ENV value (5m or 1h)\n   * @returns {null|{type: \"ephemeral\", ttl: \"5m\" | \"1h\"}} Cache control configuration\n   */\n  get cacheControl() {\n    // Store result in instance variable to avoid recalculating\n    if (this._cacheControl) return this._cacheControl;\n\n    if (!process.env.ANTHROPIC_CACHE_CONTROL) this._cacheControl = null;\n    else {\n      const normalized =\n        process.env.ANTHROPIC_CACHE_CONTROL.toLowerCase().trim();\n      if ([\"5m\", \"1h\"].includes(normalized))\n        this._cacheControl = { type: \"ephemeral\", ttl: normalized };\n      else this._cacheControl = null;\n    }\n    return this._cacheControl;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Builds system parameter with cache control if applicable\n   * @param {string} systemContent - The system prompt content\n   * @returns {string|array} System parameter for API call\n   */\n  #buildSystemPrompt(systemContent) {\n    if (!systemContent || !this.cacheControl) return systemContent;\n    return [\n      {\n        type: \"text\",\n        text: systemContent,\n        cache_control: this.cacheControl,\n      },\n    ];\n  }\n\n  /**\n   * Parse a data URL into media type and base64 data\n   * @param {string} dataUrl - Data URL like \"data:image/jpeg;base64,/9j/...\"\n   * @returns {{mediaType: string, data: string}|null}\n   */\n  #parseDataUrl(dataUrl) {\n    if (!dataUrl || !dataUrl.startsWith(\"data:\")) return null;\n    const matches = dataUrl.match(/^data:([^;]+);base64,(.+)$/);\n    if (!matches) return null;\n    return { mediaType: matches[1], data: matches[2] };\n  }\n\n  #prepareMessages(messages = []) {\n    // Extract system prompt and filter out any system messages from the main chat.\n    let systemPrompt =\n      \"You are a helpful ai assistant who can assist the user and use tools available to help answer the users prompts and questions.\";\n    const chatMessages = messages.filter((msg) => {\n      if (msg.role === \"system\") {\n        systemPrompt = msg.content;\n        return false;\n      }\n      return true;\n    });\n\n    const processedMessages = chatMessages.reduce(\n      (processedMessages, message, index) => {\n        // Normalize `function` role to Anthropic's `tool_result` format.\n        if (message.role === \"function\") {\n          const prevMessage = chatMessages[index - 1];\n          if (prevMessage?.role === \"assistant\") {\n            const toolUse = prevMessage.content.find(\n              (item) => item.type === \"tool_use\"\n            );\n            if (toolUse) {\n              processedMessages.push({\n                role: \"user\",\n                content: [\n                  {\n                    type: \"tool_result\",\n                    tool_use_id: toolUse.id,\n                    content: message.content\n                      ? String(message.content)\n                      : \"Tool executed successfully.\",\n                  },\n                ],\n              });\n            }\n          }\n          return processedMessages;\n        }\n\n        // Ensure message content is in array format and filter out empty text blocks.\n        let content = Array.isArray(message.content)\n          ? message.content\n          : [{ type: \"text\", text: message.content }];\n        content = content.filter(\n          (item) =>\n            item.type !== \"text\" || (item.text && item.text.trim().length > 0)\n        );\n\n        // Add image attachments if present (for vision/multimodal support)\n        if (message.attachments && message.attachments.length > 0) {\n          for (const attachment of message.attachments) {\n            const parsed = this.#parseDataUrl(attachment.contentString);\n            if (parsed) {\n              content.push({\n                type: \"image\",\n                source: {\n                  type: \"base64\",\n                  media_type: parsed.mediaType,\n                  data: parsed.data,\n                },\n              });\n            }\n          }\n        }\n\n        if (content.length === 0) return processedMessages;\n\n        // Add a text block to assistant messages with tool use if one doesn't exist.\n        if (\n          message.role === \"assistant\" &&\n          content.some((item) => item.type === \"tool_use\") &&\n          !content.some((item) => item.type === \"text\")\n        ) {\n          content.unshift({\n            type: \"text\",\n            text: \"I'll use a tool to help answer this question.\",\n          });\n        }\n\n        const lastMessage = processedMessages[processedMessages.length - 1];\n        if (lastMessage && lastMessage.role === message.role) {\n          // Merge consecutive messages from the same role.\n          lastMessage.content.push(...content);\n        } else {\n          // Don't pass attachments to the final message object\n          const { attachments: _, ...restOfMessage } = message;\n          processedMessages.push({ ...restOfMessage, content });\n        }\n\n        return processedMessages;\n      },\n      []\n    );\n\n    // The first message must be from the user.\n    if (processedMessages.length > 0 && processedMessages[0].role !== \"user\") {\n      processedMessages.shift();\n    }\n\n    return [systemPrompt, processedMessages];\n  }\n\n  // Anthropic does not use the regular schema for functions so here we need to ensure it is in there specific format\n  // so that the call can run correctly.\n  #formatFunctions(functions = []) {\n    return functions.map((func) => {\n      const { name, description, parameters, required } = func;\n      const { type, properties } = parameters;\n      return {\n        name,\n        description,\n        input_schema: {\n          type,\n          properties,\n          required,\n        },\n      };\n    });\n  }\n\n  /**\n   * Stream a chat completion from the LLM with tool calling\n   * Note: This using the Anthropic API SDK and its implementation is specific to Anthropic.\n   *\n   * @param {any[]} messages - The messages to send to the LLM.\n   * @param {any[]} functions - The functions to use in the LLM.\n   * @param {function} eventHandler - The event handler to use to report stream events.\n   * @returns {Promise<{ functionCall: any, textResponse: string, uuid: string }>} - The result of the chat completion.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    this.resetUsage();\n\n    try {\n      const msgUUID = v4();\n      const [systemPrompt, chats] = this.#prepareMessages(messages);\n      const response = await this.client.messages.create(\n        {\n          model: this.model,\n          max_tokens: 4096,\n          system: this.#buildSystemPrompt(systemPrompt),\n          messages: chats,\n          stream: true,\n          ...(Array.isArray(functions) && functions?.length > 0\n            ? { tools: this.#formatFunctions(functions) }\n            : {}),\n        },\n        { headers: { \"anthropic-beta\": \"tools-2024-04-04\" } } // Required to we can use tools.\n      );\n\n      const result = {\n        functionCall: null,\n        textResponse: \"\",\n      };\n\n      // Track usage from streaming events\n      const usage = { input_tokens: 0, output_tokens: 0 };\n\n      for await (const chunk of response) {\n        // Capture input tokens from message_start event\n        if (chunk.type === \"message_start\" && chunk.message?.usage) {\n          usage.input_tokens = chunk.message.usage.input_tokens || 0;\n        }\n\n        // Capture output tokens from message_delta event\n        if (chunk.type === \"message_delta\" && chunk.usage) {\n          usage.output_tokens = chunk.usage.output_tokens || 0;\n        }\n\n        if (chunk.type === \"content_block_start\") {\n          if (chunk.content_block.type === \"text\") {\n            result.textResponse += chunk.content_block.text;\n            eventHandler?.(\"reportStreamEvent\", {\n              type: \"textResponseChunk\",\n              uuid: msgUUID,\n              content: chunk.content_block.text,\n            });\n          }\n\n          if (chunk.content_block.type === \"tool_use\") {\n            result.functionCall = {\n              id: chunk.content_block.id,\n              name: chunk.content_block.name,\n              // The initial arguments are empty {} (object) so we need to set it to an empty string.\n              // It is unclear if this is ALWAYS empty on the tool_use block or if it can possible be populated.\n              // This is a workaround to ensure the tool call is valid.\n              arguments: \"\",\n            };\n            eventHandler?.(\"reportStreamEvent\", {\n              type: \"toolCallInvocation\",\n              uuid: `${msgUUID}:tool_call_invocation`,\n              content: `Assembling Tool Call: ${result.functionCall.name}(${result.functionCall.arguments})`,\n            });\n          }\n        }\n\n        if (chunk.type === \"content_block_delta\") {\n          if (chunk.delta.type === \"text_delta\") {\n            result.textResponse += chunk.delta.text;\n            eventHandler?.(\"reportStreamEvent\", {\n              type: \"textResponseChunk\",\n              uuid: msgUUID,\n              content: chunk.delta.text,\n            });\n          }\n\n          if (chunk.delta.type === \"input_json_delta\") {\n            result.functionCall.arguments += chunk.delta.partial_json;\n            eventHandler?.(\"reportStreamEvent\", {\n              type: \"toolCallInvocation\",\n              uuid: `${msgUUID}:tool_call_invocation`,\n              content: `Assembling Tool Call: ${result.functionCall.name}(${result.functionCall.arguments})`,\n            });\n          }\n        }\n      }\n\n      // Record accumulated usage\n      this.recordUsage(usage);\n      if (result.functionCall) {\n        result.functionCall.arguments = safeJsonParse(\n          result.functionCall.arguments,\n          {}\n        );\n        messages.push({\n          role: \"assistant\",\n          content: [\n            { type: \"text\", text: result.textResponse },\n            {\n              type: \"tool_use\",\n              id: result.functionCall.id,\n              name: result.functionCall.name,\n              input: result.functionCall.arguments,\n            },\n          ],\n        });\n        return {\n          textResponse: result.textResponse,\n          functionCall: {\n            name: result.functionCall.name,\n            arguments: result.functionCall.arguments,\n          },\n          cost: 0,\n          uuid: msgUUID,\n        };\n      }\n\n      return {\n        textResponse: result.textResponse,\n        functionCall: null,\n        cost: 0,\n        uuid: msgUUID,\n      };\n    } catch (error) {\n      // If invalid Auth error we need to abort because no amount of waiting\n      // will make auth better.\n      if (error instanceof Anthropic.AuthenticationError) throw error;\n\n      if (\n        error instanceof Anthropic.RateLimitError ||\n        error instanceof Anthropic.InternalServerError ||\n        error instanceof Anthropic.APIError // Also will catch AuthenticationError!!!\n      ) {\n        throw new RetryError(error.message);\n      }\n\n      throw error;\n    }\n  }\n\n  /**\n   * Create a completion based on the received messages.\n   *\n   * @param messages A list of messages to send to the Anthropic API.\n   * @param functions\n   * @returns The completion.\n   */\n  async complete(messages, functions = []) {\n    this.resetUsage();\n\n    try {\n      const [systemPrompt, chats] = this.#prepareMessages(messages);\n      const response = await this.client.messages.create(\n        {\n          model: this.model,\n          max_tokens: 4096,\n          system: this.#buildSystemPrompt(systemPrompt),\n          messages: chats,\n          stream: false,\n          ...(Array.isArray(functions) && functions?.length > 0\n            ? { tools: this.#formatFunctions(functions) }\n            : {}),\n        },\n        { headers: { \"anthropic-beta\": \"tools-2024-04-04\" } } // Required to we can use tools.\n      );\n\n      // Record usage from response (Anthropic uses input_tokens/output_tokens)\n      if (response.usage) this.recordUsage(response.usage);\n\n      // We know that we need to call a tool. So we are about to recurse through completions/handleExecution\n      // https://docs.anthropic.com/claude/docs/tool-use#how-tool-use-works\n      if (response.stop_reason === \"tool_use\") {\n        // Get the tool call explicitly.\n        const toolCall = response.content.find(\n          (res) => res.type === \"tool_use\"\n        );\n\n        // Here we need the chain of thought the model may or may not have generated alongside the call.\n        // this needs to be in a very specific format so we always ensure there is a 2-item content array\n        // so that we can ensure the tool_call content is correct. For anthropic all text items must not\n        // be empty, but the api will still return empty text so we need to make 100% sure text is not empty\n        // or the tool call will fail.\n        // wtf.\n        let thought = response.content.find((res) => res.type === \"text\");\n        thought =\n          thought?.content?.length > 0\n            ? {\n                role: thought.role,\n                content: [\n                  { type: \"text\", text: thought.content },\n                  { ...toolCall },\n                ],\n              }\n            : {\n                role: \"assistant\",\n                content: [\n                  {\n                    type: \"text\",\n                    text: `Okay, im going to use ${toolCall.name} to help me.`,\n                  },\n                  { ...toolCall },\n                ],\n              };\n\n        // Modify messages forcefully by adding system thought so that tool_use/tool_result\n        // messaging works with Anthropic's disastrous tool calling API.\n        messages.push(thought);\n\n        const functionArgs = toolCall.input;\n        return {\n          result: null,\n          functionCall: {\n            name: toolCall.name,\n            arguments: functionArgs,\n          },\n          cost: 0,\n          usage: this.getUsage(),\n        };\n      }\n\n      const completion = response.content.find((msg) => msg.type === \"text\");\n      return {\n        textResponse:\n          completion?.text ??\n          \"The model failed to complete the task and return back a valid response.\",\n        cost: 0,\n        usage: this.getUsage(),\n      };\n    } catch (error) {\n      // If invalid Auth error we need to abort because no amount of waiting\n      // will make auth better.\n      if (error instanceof Anthropic.AuthenticationError) throw error;\n\n      if (\n        error instanceof Anthropic.RateLimitError ||\n        error instanceof Anthropic.InternalServerError ||\n        error instanceof Anthropic.APIError // Also will catch AuthenticationError!!!\n      ) {\n        throw new RetryError(error.message);\n      }\n\n      throw error;\n    }\n  }\n}\n\nmodule.exports = AnthropicProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/apipie.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\n/**\n * The agent provider for the OpenRouter provider.\n */\nclass ApiPieProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = \"openrouter/llama-3.1-8b-instruct\" } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: \"https://apipie.ai/v1\",\n      apiKey: process.env.APIPIE_LLM_API_KEY,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"ApiPie chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"ApiPie chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = ApiPieProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/azure.js",
    "content": "const { OpenAI } = require(\"openai\");\nconst { AzureOpenAiLLM } = require(\"../../../AiProviders/azureOpenAi\");\nconst Provider = require(\"./ai-provider.js\");\nconst { tooledStream, tooledComplete } = require(\"./helpers/tooled.js\");\nconst { RetryError } = require(\"../error.js\");\n\n/**\n * The agent provider for the Azure OpenAI API.\n * Uses the shared native tool calling helper for OpenAI-compatible tool calling.\n */\nclass AzureOpenAiProvider extends Provider {\n  model;\n\n  constructor(config = { model: null }) {\n    const client = new OpenAI({\n      apiKey: process.env.AZURE_OPENAI_KEY,\n      baseURL: AzureOpenAiLLM.formatBaseUrl(process.env.AZURE_OPENAI_ENDPOINT),\n    });\n    super(client);\n    this.model =\n      config.model ||\n      process.env.AZURE_OPENAI_MODEL_PREF ||\n      process.env.OPEN_MODEL_PREF;\n    this.verbose = true;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * - Azure OpenAI always supports tool calling.\n   * @returns {boolean}\n   */\n  supportsNativeToolCalling() {\n    return true;\n  }\n\n  /**\n   * Stream a chat completion from Azure OpenAI with tool calling.\n   *\n   * @param {any[]} messages\n   * @param {any[]} functions\n   * @param {function} eventHandler\n   * @returns {Promise<{ functionCall: any, textResponse: string, uuid: string }>}\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    this.providerLog(\"Provider.stream - will process this chat completion.\");\n\n    try {\n      return await tooledStream(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        eventHandler,\n        { provider: this }\n      );\n    } catch (error) {\n      console.error(error.message, error);\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Create a completion based on the received messages with tool calling.\n   *\n   * @param {any[]} messages\n   * @param {any[]} functions\n   * @returns The completion.\n   */\n  async complete(messages, functions = []) {\n    try {\n      const result = await tooledComplete(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        this.getCost.bind(this),\n        { provider: this }\n      );\n\n      if (result.retryWithError) {\n        return this.complete([...messages, result.retryWithError], functions);\n      }\n\n      return result;\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   * Stubbed since Azure OpenAI has no public cost basis.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = AzureOpenAiProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/bedrock.js",
    "content": "const {\n  createBedrockCredentials,\n  getBedrockAuthMethod,\n  createBedrockChatClient,\n} = require(\"../../../AiProviders/bedrock/utils.js\");\nconst { AWSBedrockLLM } = require(\"../../../AiProviders/bedrock/index.js\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { safeJsonParse } = require(\"../../../http\");\nconst { v4 } = require(\"uuid\");\nconst {\n  HumanMessage,\n  SystemMessage,\n  AIMessage,\n  ToolMessage,\n} = require(\"@langchain/core/messages\");\n\n/**\n * The agent provider for the AWS Bedrock provider.\n */\nclass AWSBedrockProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(_config = {}) {\n    super();\n    const model = process.env.AWS_BEDROCK_LLM_MODEL_PREFERENCE ?? null;\n    const client = createBedrockChatClient(\n      {},\n      this.authMethod,\n      this.credentials,\n      model\n    );\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n    this._supportsToolCalling = null;\n  }\n\n  /**\n   * Some Bedrock models (Titan, Cohere) don't support streaming.\n   * Set AWS_BEDROCK_STREAMING_DISABLED to any value to disable streaming for those models.\n   * Since this can be any model even custom models we leave it to the user to disable streaming if needed.\n   * @returns {boolean} True if streaming is supported, false otherwise.\n   */\n  get supportsAgentStreaming() {\n    if (!!process.env.AWS_BEDROCK_STREAMING_DISABLED) return false;\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native tool calling via the Bedrock Converse API.\n   * Checks the ENV to see if the provider supports tool calling.\n   * If the ENV is not set, we default to false.\n   * @returns {boolean}\n   */\n  supportsNativeToolCalling() {\n    if (this._supportsToolCalling !== null) return this._supportsToolCalling;\n    const supportsToolCalling = this.supportsNativeToolCallingViaEnv(\"bedrock\");\n    if (supportsToolCalling)\n      this.providerLog(\"AWS Bedrock native tool calling is ENABLED via ENV.\");\n    else\n      this.providerLog(\n        \"AWS Bedrock native tool calling is DISABLED via ENV. Will use UnTooled instead.\"\n      );\n    this._supportsToolCalling = supportsToolCalling;\n    return supportsToolCalling;\n  }\n\n  /**\n   * Gets the credentials for the AWS Bedrock LLM based on the authentication method provided.\n   * @returns {object} The credentials object.\n   */\n  get credentials() {\n    return createBedrockCredentials(this.authMethod);\n  }\n\n  /**\n   * Gets the configured AWS authentication method ('iam' or 'sessionToken').\n   * Defaults to 'iam' if the environment variable is invalid.\n   * @returns {\"iam\" | \"iam_role\" | \"sessionToken\"} The authentication method.\n   */\n  get authMethod() {\n    return getBedrockAuthMethod();\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  // For streaming we use Langchain's wrapper to handle weird chunks\n  // or otherwise absorb headaches that can arise from Ollama models\n  #convertToLangchainPrototypes(chats = []) {\n    const langchainChats = [];\n\n    for (const chat of chats) {\n      if (chat.role === \"system\") {\n        langchainChats.push(new SystemMessage({ content: chat.content }));\n      } else if (chat.role === \"user\") {\n        langchainChats.push(\n          new HumanMessage({\n            content: this.#formatContentWithAttachments(chat),\n          })\n        );\n      } else if (chat.role === \"assistant\") {\n        langchainChats.push(new AIMessage({ content: chat.content }));\n      }\n    }\n\n    return langchainChats;\n  }\n\n  /**\n   * Format message content with attachments for Langchain multimodal support.\n   * Transforms a message with attachments into the format Langchain expects.\n   * @param {Object} chat - The chat message\n   * @returns {string|Array} Content as string or multimodal array\n   */\n  #formatContentWithAttachments(chat) {\n    if (!chat.attachments || chat.attachments.length === 0) {\n      return chat.content;\n    }\n\n    const content = [{ type: \"text\", text: chat.content }];\n    for (const attachment of chat.attachments) {\n      content.push({\n        type: \"image_url\",\n        image_url: {\n          url: attachment.contentString,\n        },\n      });\n    }\n    return content;\n  }\n\n  /**\n   * Convert aibitat message history to Langchain message prototypes with\n   * proper tool call / tool result handling for native tool calling.\n   * role:\"function\" messages (from previous aibitat tool runs) are converted\n   * to AIMessage(tool_calls) + ToolMessage pairs that Langchain expects.\n   * Also handles image attachments for multimodal support.\n   * @param {Array} chats - The aibitat message history.\n   * @returns {Array} Langchain message instances.\n   */\n  #convertToLangchainPrototypesWithTools(chats = []) {\n    const langchainChats = [];\n\n    for (const chat of chats) {\n      if (chat.role === \"function\") {\n        if (chat.originalFunctionCall?.id) {\n          const prevMsg = langchainChats[langchainChats.length - 1];\n          if (\n            !prevMsg ||\n            !(prevMsg instanceof AIMessage) ||\n            !prevMsg.tool_calls?.length\n          ) {\n            langchainChats.push(\n              new AIMessage({\n                content: \"\",\n                tool_calls: [\n                  {\n                    name: chat.originalFunctionCall.name,\n                    args:\n                      typeof chat.originalFunctionCall.arguments === \"string\"\n                        ? safeJsonParse(chat.originalFunctionCall.arguments, {})\n                        : chat.originalFunctionCall.arguments,\n                    id: chat.originalFunctionCall.id,\n                  },\n                ],\n              })\n            );\n          }\n          langchainChats.push(\n            new ToolMessage({\n              content:\n                typeof chat.content === \"string\"\n                  ? chat.content\n                  : JSON.stringify(chat.content),\n              tool_call_id: chat.originalFunctionCall.id,\n            })\n          );\n        } else {\n          const toolCallId = `call_${v4()}`;\n          langchainChats.push(\n            new AIMessage({\n              content: \"\",\n              tool_calls: [{ name: chat.name, args: {}, id: toolCallId }],\n            })\n          );\n          langchainChats.push(\n            new ToolMessage({\n              content:\n                typeof chat.content === \"string\"\n                  ? chat.content\n                  : JSON.stringify(chat.content),\n              tool_call_id: toolCallId,\n            })\n          );\n        }\n      } else if (chat.role === \"system\") {\n        langchainChats.push(new SystemMessage({ content: chat.content }));\n      } else if (chat.role === \"user\") {\n        langchainChats.push(\n          new HumanMessage({\n            content: this.#formatContentWithAttachments(chat),\n          })\n        );\n      } else if (chat.role === \"assistant\") {\n        langchainChats.push(new AIMessage({ content: chat.content }));\n      }\n    }\n\n    return langchainChats;\n  }\n\n  /**\n   * Convert aibitat function definitions to the format expected by\n   * Langchain's ChatBedrockConverse.bindTools().\n   * @param {Array<{name: string, description: string, parameters: object}>} functions\n   * @returns {Array<{type: \"function\", function: {name: string, description: string, parameters: object}}>}\n   */\n  #formatFunctionsToLangchainTools(functions) {\n    if (!Array.isArray(functions) || functions.length === 0) return [];\n    return functions.map((func) => ({\n      type: \"function\",\n      function: {\n        name: func.name,\n        description: func.description,\n        parameters: func.parameters,\n      },\n    }));\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    const response = await this.client\n      .invoke(this.#convertToLangchainPrototypes(messages))\n      .then((res) => res)\n      .catch((e) => {\n        console.error(e);\n        return null;\n      });\n\n    return response?.content;\n  }\n\n  /**\n   * Create a streaming response from the Langchain Bedrock client and convert\n   * it to OpenAI-compatible format expected by UnTooled.\n   * @param {Object} options - The options object containing messages.\n   * @param {Array} options.messages - The messages to send to the LLM.\n   * @returns {AsyncGenerator} An async iterable yielding OpenAI-compatible chunks.\n   */\n  async #handleFunctionCallStream({ messages = [] }) {\n    const langchainMessages = this.#convertToLangchainPrototypes(messages);\n    const stream = await this.client.stream(langchainMessages);\n\n    // Wrap Langchain stream to OpenAI format expected by UnTooled\n    const self = this;\n    return {\n      async *[Symbol.asyncIterator]() {\n        try {\n          for await (const chunk of stream) {\n            // Langchain chunks have .content property directly\n            const content =\n              typeof chunk.content === \"string\" ? chunk.content : \"\";\n            if (content) {\n              yield {\n                choices: [\n                  {\n                    delta: {\n                      content: content,\n                    },\n                  },\n                ],\n              };\n            }\n          }\n        } catch (e) {\n          AWSBedrockLLM.errorToHumanReadable(e, {\n            method: \"stream\",\n            model: self.model,\n          });\n        }\n      },\n    };\n  }\n\n  /**\n   * Stream a chat completion from the Bedrock LLM with tool calling.\n   * Uses native Bedrock Converse tool calling when supported, otherwise falls back to UnTooled.\n   *\n   * @param {any[]} messages - The messages to send to the LLM.\n   * @param {any[]} functions - The functions to use in the LLM.\n   * @param {function} eventHandler - The event handler to use to report stream events.\n   * @returns {Promise<{ functionCall: any, textResponse: string }>} - The result of the chat completion.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    const useNative = functions.length > 0 && this.supportsNativeToolCalling();\n\n    if (!useNative) {\n      return await UnTooled.prototype.stream\n        .call(\n          this,\n          messages,\n          functions,\n          this.#handleFunctionCallStream.bind(this),\n          eventHandler\n        )\n        .catch((e) => {\n          AWSBedrockLLM.errorToHumanReadable(e, {\n            method: \"stream\",\n            model: this.model,\n          });\n        });\n    }\n\n    this.providerLog(\n      \"Provider.stream (tooled) - will process this chat completion.\"\n    );\n\n    try {\n      const langchainMessages =\n        this.#convertToLangchainPrototypesWithTools(messages);\n      const tools = this.#formatFunctionsToLangchainTools(functions);\n      const modelWithTools = this.client.bindTools(tools);\n      const stream = await modelWithTools.stream(langchainMessages);\n\n      const msgUUID = v4();\n      let textResponse = \"\";\n      let finalMessage = null;\n\n      for await (const chunk of stream) {\n        finalMessage =\n          finalMessage === null ? chunk : finalMessage.concat(chunk);\n\n        const content = typeof chunk.content === \"string\" ? chunk.content : \"\";\n        if (content) {\n          textResponse += content;\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"textResponseChunk\",\n            uuid: msgUUID,\n            content,\n          });\n        }\n\n        if (chunk.tool_call_chunks?.length) {\n          for (const toolChunk of chunk.tool_call_chunks) {\n            if (toolChunk.name) {\n              eventHandler?.(\"reportStreamEvent\", {\n                uuid: `${msgUUID}:tool_call_invocation`,\n                type: \"toolCallInvocation\",\n                content: `Assembling Tool Call: ${toolChunk.name}`,\n              });\n            }\n          }\n        }\n      }\n\n      if (finalMessage?.tool_calls?.length > 0) {\n        const toolCall = finalMessage.tool_calls[0];\n        return {\n          textResponse,\n          functionCall: {\n            id: toolCall.id || `call_${v4()}`,\n            name: toolCall.name,\n            arguments: toolCall.args || {},\n          },\n          cost: 0,\n        };\n      }\n\n      return { textResponse, functionCall: null, cost: 0 };\n    } catch (e) {\n      AWSBedrockLLM.errorToHumanReadable(e, {\n        method: \"stream\",\n        model: this.model,\n      });\n    }\n  }\n\n  /**\n   * Create a non-streaming completion with tool calling support.\n   * Uses native Bedrock Converse tool calling when supported, otherwise falls back to UnTooled.\n   *\n   * @param {any[]} messages A list of messages to send to the API.\n   * @param {any[]} functions The function definitions available to the model.\n   * @returns The completion.\n   */\n  async complete(messages, functions = []) {\n    const useNative = functions.length > 0 && this.supportsNativeToolCalling();\n\n    if (!useNative) {\n      return await UnTooled.prototype.complete\n        .call(\n          this,\n          messages,\n          functions,\n          this.#handleFunctionCallChat.bind(this)\n        )\n        .catch((e) => {\n          AWSBedrockLLM.errorToHumanReadable(e, {\n            method: \"complete\",\n            model: this.model,\n          });\n        });\n    }\n\n    try {\n      const langchainMessages =\n        this.#convertToLangchainPrototypesWithTools(messages);\n      const tools = this.#formatFunctionsToLangchainTools(functions);\n      const modelWithTools = this.client.bindTools(tools);\n      const response = await modelWithTools.invoke(langchainMessages);\n\n      if (response.tool_calls?.length > 0) {\n        const toolCall = response.tool_calls[0];\n        return {\n          textResponse: null,\n          functionCall: {\n            id: toolCall.id || `call_${v4()}`,\n            name: toolCall.name,\n            arguments: toolCall.args || {},\n          },\n          cost: 0,\n        };\n      }\n\n      return {\n        textResponse:\n          typeof response.content === \"string\"\n            ? response.content\n            : JSON.stringify(response.content),\n        cost: 0,\n      };\n    } catch (e) {\n      AWSBedrockLLM.errorToHumanReadable(e, {\n        method: \"complete\",\n        model: this.model,\n      });\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   * Stubbed since KoboldCPP has no cost basis.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = AWSBedrockProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/cohere.js",
    "content": "const { CohereClientV2 } = require(\"cohere-ai\");\nconst Provider = require(\"./ai-provider\");\nconst InheritMultiple = require(\"./helpers/classes\");\nconst UnTooled = require(\"./helpers/untooled\");\nconst { v4 } = require(\"uuid\");\nconst { safeJsonParse } = require(\"../../../http\");\n\n/**\n * The agent provider for the Cohere AI provider.\n * Uses the v2 API which supports OpenAI-compatible message format and vision.\n */\nclass CohereProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = process.env.COHERE_MODEL_PREF || \"command-r-08-2024\" } =\n      config;\n    super();\n    const client = new CohereClientV2({\n      token: process.env.COHERE_API_KEY,\n    });\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  /**\n   * Format a message with attachments for Cohere's v2 API.\n   * Cohere SDK uses camelCase (imageUrl) instead of snake_case (image_url).\n   * @param {Object} message - Message with potential attachments\n   * @returns {Object} Formatted message for Cohere SDK\n   */\n  formatMessageWithAttachments(message) {\n    if (!message.attachments || message.attachments.length === 0) {\n      return message;\n    }\n\n    const content = [{ type: \"text\", text: message.content }];\n    for (const attachment of message.attachments) {\n      content.push({\n        type: \"image_url\",\n        imageUrl: {\n          url: attachment.contentString,\n        },\n      });\n    }\n\n    const { attachments: _, ...rest } = message;\n    return {\n      ...rest,\n      content,\n    };\n  }\n\n  /**\n   * Stream a chat completion using the Cohere v2 API.\n   * The v2 API accepts OpenAI-compatible message format directly,\n   * including multimodal content arrays for vision support.\n   * @param {Object} options - Options containing messages array\n   * @returns {AsyncIterable} Stream of events from Cohere\n   */\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chatStream({\n      model: this.model,\n      messages: messages,\n    });\n  }\n\n  async streamingFunctionCall(\n    messages,\n    functions,\n    chatCb = null,\n    eventHandler = null\n  ) {\n    const history = [...messages].filter((msg) =>\n      [\"user\", \"assistant\"].includes(msg.role)\n    );\n    if (history[history.length - 1]?.role !== \"user\") return null;\n\n    const msgUUID = v4();\n    let textResponse = \"\";\n    const historyMessages = this.buildToolCallMessages(history, functions);\n    const stream = await chatCb({ messages: historyMessages });\n\n    eventHandler?.(\"reportStreamEvent\", {\n      type: \"statusResponse\",\n      uuid: v4(),\n      content: \"Agent is thinking...\",\n    });\n\n    for await (const event of stream) {\n      if (event.type !== \"content-delta\") continue;\n      const text = event.delta?.message?.content?.text || \"\";\n      if (!text) continue;\n      textResponse += text;\n      eventHandler?.(\"reportStreamEvent\", {\n        type: \"statusResponse\",\n        uuid: msgUUID,\n        content: text,\n      });\n    }\n\n    const call = safeJsonParse(textResponse, null);\n    if (call === null)\n      return { toolCall: null, text: textResponse, uuid: msgUUID };\n\n    const { valid, reason } = this.validFuncCall(call, functions);\n    if (!valid) {\n      this.providerLog(`Invalid function tool call: ${reason}.`);\n      eventHandler?.(\"reportStreamEvent\", {\n        type: \"removeStatusResponse\",\n        uuid: msgUUID,\n        content:\n          \"The model attempted to make an invalid function call - it was ignored.\",\n      });\n      return { toolCall: null, text: null, uuid: msgUUID };\n    }\n\n    const { isDuplicate, reason: duplicateReason } =\n      this.deduplicator.isDuplicate(call.name, call.arguments);\n    if (isDuplicate) {\n      this.providerLog(\n        `Cannot call ${call.name} again because ${duplicateReason}.`\n      );\n      eventHandler?.(\"reportStreamEvent\", {\n        type: \"removeStatusResponse\",\n        uuid: msgUUID,\n        content:\n          \"The model tried to call a function with the same arguments as a previous call - it was ignored.\",\n      });\n      return { toolCall: null, text: null, uuid: msgUUID };\n    }\n\n    eventHandler?.(\"reportStreamEvent\", {\n      uuid: `${msgUUID}:tool_call_invocation`,\n      type: \"toolCallInvocation\",\n      content: `Parsed Tool Call: ${call.name}(${JSON.stringify(call.arguments)})`,\n    });\n    return { toolCall: call, text: null, uuid: msgUUID };\n  }\n\n  /**\n   * Stream a chat completion from the LLM with tool calling\n   * Override the inherited `stream` method since Cohere uses a different API format.\n   *\n   * @param {any[]} messages - The messages to send to the LLM.\n   * @param {any[]} functions - The functions to use in the LLM.\n   * @param {function} eventHandler - The event handler to use to report stream events.\n   * @returns {Promise<{ functionCall: any, textResponse: string }>} - The result of the chat completion.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    this.providerLog(\n      \"CohereProvider.stream - will process this chat completion.\"\n    );\n    // eslint-disable-next-line\n    try {\n      let completion = { content: \"\" };\n      if (functions.length > 0) {\n        const {\n          toolCall,\n          text,\n          uuid: msgUUID,\n        } = await this.streamingFunctionCall(\n          messages,\n          functions,\n          this.#handleFunctionCallStream.bind(this),\n          eventHandler\n        );\n\n        if (toolCall !== null) {\n          this.providerLog(`Valid tool call found - running ${toolCall.name}.`);\n          this.deduplicator.trackRun(toolCall.name, toolCall.arguments, {\n            cooldown: this.isMCPTool(toolCall, functions),\n          });\n          return {\n            result: null,\n            functionCall: {\n              name: toolCall.name,\n              arguments: toolCall.arguments,\n            },\n            cost: 0,\n          };\n        }\n\n        if (text) {\n          this.providerLog(\n            `No tool call found in the response - will send as a full text response.`\n          );\n          completion.content = text;\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"removeStatusResponse\",\n            uuid: msgUUID,\n            content: \"No tool call found in the response\",\n          });\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"statusResponse\",\n            uuid: v4(),\n            content: \"Done thinking.\",\n          });\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"fullTextResponse\",\n            uuid: v4(),\n            content: text,\n          });\n        }\n      }\n\n      if (!completion?.content) {\n        eventHandler?.(\"reportStreamEvent\", {\n          type: \"statusResponse\",\n          uuid: v4(),\n          content: \"Done thinking.\",\n        });\n\n        this.providerLog(\n          \"Will assume chat completion without tool call inputs.\"\n        );\n        const msgUUID = v4();\n        completion = { content: \"\" };\n        const stream = await this.#handleFunctionCallStream({\n          messages: this.cleanMsgs(messages),\n        });\n\n        for await (const chunk of stream) {\n          if (chunk.type !== \"content-delta\") continue;\n          const text = chunk.delta?.message?.content?.text || \"\";\n          if (!text) continue;\n          completion.content += text;\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"textResponseChunk\",\n            uuid: msgUUID,\n            content: text,\n          });\n        }\n      }\n\n      this.deduplicator.reset(\"runs\");\n      return {\n        textResponse: completion.content,\n        cost: 0,\n      };\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = CohereProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/cometapi.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\n/**\n * The agent provider for the CometAPI provider.\n */\nclass CometApiProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = \"gpt-5-mini\" } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: \"https://api.cometapi.com/v1\",\n      apiKey: process.env.COMETAPI_LLM_API_KEY,\n      maxRetries: 3,\n      defaultHeaders: {\n        \"HTTP-Referer\": \"https://anythingllm.com\",\n        \"X-CometAPI-Source\": \"anythingllm\",\n      },\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return false;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"CometAPI chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"CometAPI chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   * Stubbed since CometAPI has no cost basis.\n   */\n  getCost() {\n    return 0;\n  }\n}\n\nmodule.exports = CometApiProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/deepseek.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { tooledStream, tooledComplete } = require(\"./helpers/tooled.js\");\nconst { RetryError } = require(\"../error.js\");\nconst { toValidNumber } = require(\"../../../http/index.js\");\n\nclass DeepSeekProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    super();\n    const { model = \"deepseek-chat\" } = config;\n    const client = new OpenAI({\n      baseURL: \"https://api.deepseek.com/v1\",\n      apiKey: process.env.DEEPSEEK_API_KEY ?? null,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n    this.maxTokens = process.env.DEEPSEEK_MAX_TOKENS\n      ? toValidNumber(process.env.DEEPSEEK_MAX_TOKENS, 1024)\n      : 1024;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * All current DeepSeek models (deepseek-chat and deepseek-reasoner)\n   * support native OpenAI-compatible tool calling.\n   * @returns {boolean}\n   */\n  supportsNativeToolCalling() {\n    return true;\n  }\n\n  /**\n   * DeepSeek models do not support vision/image inputs.\n   * Strip attachments from messages to prevent API errors.\n   * @param {Object} message - Message with potential attachments\n   * @returns {Object} Message without attachments\n   */\n  formatMessageWithAttachments(message) {\n    const { attachments: _, ...rest } = message;\n    return rest;\n  }\n\n  get #isThinkingModel() {\n    return this.model === \"deepseek-reasoner\";\n  }\n\n  get #tooledOptions() {\n    return {\n      provider: this,\n      ...(this.#isThinkingModel ? { injectReasoningContent: true } : {}),\n    };\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n        max_tokens: this.maxTokens,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"DeepSeek chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"DeepSeek chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  /**\n   * Strip attachments from all messages since DeepSeek doesn't support vision.\n   * @param {Array} messages - Array of messages\n   * @returns {Array} Messages with attachments removed\n   */\n  #stripAttachments(messages) {\n    let hasAttachments = false;\n    const stripped = messages.map((msg) => {\n      if (msg.attachments && msg.attachments.length > 0) {\n        hasAttachments = true;\n        const { attachments: _, ...rest } = msg;\n        return rest;\n      }\n      return msg;\n    });\n    if (hasAttachments) {\n      this.providerLog(\n        \"DeepSeek does not support vision - stripped image attachments from messages.\"\n      );\n    }\n    return stripped;\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    const useNative = functions.length > 0 && this.supportsNativeToolCalling();\n    const cleanedMessages = this.#stripAttachments(messages);\n\n    if (!useNative) {\n      return await UnTooled.prototype.stream.call(\n        this,\n        cleanedMessages,\n        functions,\n        this.#handleFunctionCallStream.bind(this),\n        eventHandler\n      );\n    }\n\n    this.providerLog(\n      \"Provider.stream (tooled) - will process this chat completion.\"\n    );\n\n    try {\n      return await tooledStream(\n        this.client,\n        this.model,\n        cleanedMessages,\n        functions,\n        eventHandler,\n        this.#tooledOptions\n      );\n    } catch (error) {\n      console.error(error.message, error);\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  async complete(messages, functions = []) {\n    const useNative = functions.length > 0 && this.supportsNativeToolCalling();\n    const cleanedMessages = this.#stripAttachments(messages);\n\n    if (!useNative) {\n      return await UnTooled.prototype.complete.call(\n        this,\n        cleanedMessages,\n        functions,\n        this.#handleFunctionCallChat.bind(this)\n      );\n    }\n\n    try {\n      const result = await tooledComplete(\n        this.client,\n        this.model,\n        cleanedMessages,\n        functions,\n        this.getCost.bind(this),\n        this.#tooledOptions\n      );\n\n      if (result.retryWithError) {\n        return this.complete([...messages, result.retryWithError], functions);\n      }\n\n      return result;\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = DeepSeekProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/dellProAiStudio.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst {\n  DellProAiStudioLLM,\n} = require(\"../../../AiProviders/dellProAiStudio/index.js\");\n\n/**\n * The agent provider for Dell Pro AI Studio.\n */\nclass DellProAiStudioProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  /**\n   *\n   * @param {{model?: string}} config\n   */\n  constructor(config = {}) {\n    super();\n    const model = config?.model || process.env.DPAIS_LLM_MODEL_PREF;\n    const client = new OpenAI({\n      baseURL: DellProAiStudioLLM.parseBasePath(), // Will use process.env.DPAIS_LLM_BASE_PATH if not provided\n      apiKey: null,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"DellProAiStudio chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"DellProAiStudio chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   * Stubbed since LMStudio has no cost basis.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = DellProAiStudioProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/dockerModelRunner.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { tooledStream, tooledComplete } = require(\"./helpers/tooled.js\");\nconst { RetryError } = require(\"../error.js\");\nconst {\n  DockerModelRunnerLLM,\n  parseDockerModelRunnerEndpoint,\n} = require(\"../../../AiProviders/dockerModelRunner/index.js\");\n\n/**\n * The agent provider for the Docker Model Runner.\n */\nclass DockerModelRunnerProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  /**\n   *\n   * @param {{model?: string}} config\n   */\n  constructor(config = {}) {\n    super();\n    const model =\n      config?.model || process.env.DOCKER_MODEL_RUNNER_LLM_MODEL_PREF || null;\n    const client = new OpenAI({\n      baseURL: parseDockerModelRunnerEndpoint(\n        process.env.DOCKER_MODEL_RUNNER_BASE_PATH\n      ),\n      apiKey: null,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n    this._supportsToolCalling = null;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  async supportsNativeToolCalling() {\n    if (this._supportsToolCalling !== null) return this._supportsToolCalling;\n    const dmr = new DockerModelRunnerLLM(null, this.model);\n    const capabilities = await dmr.getModelCapabilities();\n    this._supportsToolCalling = capabilities.tools === true;\n    return this._supportsToolCalling;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"Docker Model Runner chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"Docker Model Runner chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  /**\n   * Stream a chat completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to UnTooled.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.stream.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallStream.bind(this),\n        eventHandler\n      );\n    }\n\n    this.providerLog(\n      \"Provider.stream (tooled) - will process this chat completion.\"\n    );\n\n    try {\n      return await tooledStream(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        eventHandler,\n        { provider: this }\n      );\n    } catch (error) {\n      console.error(error.message, error);\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Create a non-streaming completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to UnTooled.\n   */\n  async complete(messages, functions = []) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.complete.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallChat.bind(this)\n      );\n    }\n\n    try {\n      const result = await tooledComplete(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        this.getCost.bind(this),\n        { provider: this }\n      );\n\n      if (result.retryWithError) {\n        return this.complete([...messages, result.retryWithError], functions);\n      }\n\n      return result;\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   * Stubbed since Docker Model Runner has no cost basis.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = DockerModelRunnerProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/fireworksai.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\n/**\n * The agent provider for the FireworksAI provider.\n * We wrap FireworksAI in UnTooled because its tool-calling may not be supported for specific models and this normalizes that.\n */\nclass FireworksAIProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = \"accounts/fireworks/models/llama-v3p1-8b-instruct\" } =\n      config;\n    super();\n    const client = new OpenAI({\n      baseURL: \"https://api.fireworks.ai/inference/v1\",\n      apiKey: process.env.FIREWORKS_AI_LLM_API_KEY,\n      maxRetries: 0,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"FireworksAI chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"FireworksAI chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = FireworksAIProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/foundry.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst {\n  parseFoundryBasePath,\n  FoundryLLM,\n} = require(\"../../../AiProviders/foundry/index.js\");\n\n/**\n * The agent provider for the Foundry provider.\n * Uses untooled because it doesn't support tool calling.\n */\nclass FoundryProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = process.env.FOUNDRY_MODEL_PREF } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: parseFoundryBasePath(process.env.FOUNDRY_BASE_PATH),\n      apiKey: null,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  /**\n   * Get the client.\n   * @returns {OpenAI.OpenAI}\n   */\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    await FoundryLLM.cacheContextWindows();\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n        max_completion_tokens: FoundryLLM.promptWindowLimit(this.model),\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"Microsoft Foundry Local chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"Microsoft Foundry Local chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    await FoundryLLM.cacheContextWindows();\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n      max_completion_tokens: FoundryLLM.promptWindowLimit(this.model),\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = FoundryProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/gemini.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst { RetryError } = require(\"../error.js\");\nconst { safeJsonParse } = require(\"../../../http\");\nconst { v4 } = require(\"uuid\");\n\n/**\n * The agent provider for the Gemini provider.\n * We wrap Gemini in UnTooled because its tool-calling is not supported via the dedicated OpenAI API.\n */\nclass GeminiProvider extends Provider {\n  model;\n\n  constructor(config = {}) {\n    const { model = \"gemini-2.0-flash-lite\" } = config;\n    super();\n    this.className = \"GeminiProvider\";\n    const client = new OpenAI({\n      baseURL: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n      apiKey: process.env.GEMINI_API_KEY,\n      maxRetries: 0,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  /**\n   * Whether this provider supports agent streaming.\n   * - Tool call streaming results in a 400/503 error for all non-gemini models\n   * using the compatible v1beta/openai/ endpoint\n   * @returns {boolean}\n   */\n  get supportsAgentStreaming() {\n    if (!this.model.startsWith(\"gemini\")) {\n      this.providerLog(\n        `Gemini: ${this.model} does not support tool call streaming.`\n      );\n      return false;\n    }\n    return true;\n  }\n\n  get supportsToolCalling() {\n    if (!this.model.startsWith(\"gemini\")) return false;\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * - Gemini only supports tool calling for Gemini models.\n   * @returns {boolean}\n   */\n  supportsNativeToolCalling() {\n    return this.supportsToolCalling;\n  }\n\n  /**\n   * Gemini specifcally will throw an error if the tool call's function name\n   * starts with a non-alpha character. So we need to prefix the function names\n   * with a valid prefix to ensure they are always valid and then strip them back\n   * so they may properly be used in the tool call.\n   *\n   * So for all tools, we force the prefix to be gtc__ to avoid issues\n   * Agent flows are already prefixed with flow__ but since we strip the prefix\n   * anyway pre and post-reply, we do it anyway to ensure consistency across all tools.\n   *\n   * This specifically impacts the custom Agent Skills since they can be a short alphanumeric\n   * and cant definitely start with a number. eg: '12xdaya31bas' -> invalid in gemini tools.\n   *\n   * Even if the tool is never called, if it is in the `tools` array and this prefix\n   * patch is not applied, gemini will throw an error.\n   *\n   * This is undocumented by google, but it is the only way to ensure that tool calls\n   * are valid.\n   *\n   * @param {string} functionName - The name of the function to prefix.\n   * @param {'add' | 'strip'} action - The action to take.\n   * @returns {string} The prefixed function name.\n   * @returns {string} The prefix to use for tool call ids.\n   */\n  prefixToolCall(functionName, action = \"add\") {\n    if (action === \"add\") return `gtc__${functionName}`;\n    // must start with gtc__ to be valid and we only strip the first instance\n    return functionName.startsWith(\"gtc__\")\n      ? functionName.split(\"gtc__\")[1]\n      : functionName;\n  }\n\n  /**\n   * Format the messages to the Gemini API Responses format.\n   * - Gemini has some loosely documented format for tool calls and it can change at any time.\n   * - We need to map the function call to the correct id and Gemini will throw an error if it does not.\n   * - Gemini requires a `thought_signature` (via `extra_content.google.thought_signature`) on function call\n   *   parts in multi-turn tool conversations. This is an encrypted token Gemini attaches to every tool call\n   *   it makes, and it must be passed back when sending tool results or Gemini rejects the request with a 400.\n   *   See: https://ai.google.dev/gemini-api/docs/thought-signatures\n   * @param {any[]} messages - The messages to format.\n   * @returns {OpenAI.OpenAI.Responses.ResponseInput[]} The formatted messages.\n   */\n  #formatMessages(messages) {\n    let formattedMessages = [];\n    messages.forEach((message) => {\n      if (message.role === \"function\") {\n        // If the message does not have an originalFunctionCall we cannot\n        // map it to a function call id and Gemini will throw an error.\n        // so if this does not carry over - log and skip\n        if (!message.hasOwnProperty(\"originalFunctionCall\")) {\n          this.providerLog(\n            \"[Gemini.#formatMessages]: message did not pass back the originalFunctionCall. We need this to map the function call to the correct id.\",\n            { message: JSON.stringify(message, null, 2) }\n          );\n          return;\n        }\n\n        const prefixedName = this.prefixToolCall(\n          message.originalFunctionCall.name,\n          \"add\"\n        );\n        formattedMessages.push(\n          {\n            role: \"assistant\",\n            content: \"\",\n            tool_calls: [\n              {\n                type: \"function\",\n                ...(message.originalFunctionCall.extra_content\n                  ? {\n                      extra_content: message.originalFunctionCall.extra_content,\n                    }\n                  : {}),\n                function: {\n                  arguments: JSON.stringify(\n                    message.originalFunctionCall.arguments\n                  ),\n                  name: prefixedName,\n                },\n                id: message.originalFunctionCall.id,\n              },\n            ],\n          },\n          {\n            role: \"tool\",\n            tool_call_id: message.originalFunctionCall.id,\n            name: prefixedName,\n            content: message.content,\n          }\n        );\n        return;\n      }\n\n      // Handle messages with attachments (images) for multimodal support\n      if (message.attachments && message.attachments.length > 0) {\n        const content = [{ type: \"text\", text: message.content }];\n        for (const attachment of message.attachments) {\n          content.push({\n            type: \"image_url\",\n            image_url: {\n              url: attachment.contentString,\n            },\n          });\n        }\n        formattedMessages.push({\n          role: message.role,\n          content,\n        });\n        return;\n      }\n\n      formattedMessages.push({\n        role: message.role,\n        content: message.content,\n      });\n    });\n\n    return formattedMessages;\n  }\n\n  #formatFunctions(functions) {\n    return functions.map((func) => ({\n      type: \"function\",\n      function: {\n        name: this.prefixToolCall(func.name, \"add\"),\n        description: func.description,\n        parameters: func.parameters,\n      },\n    }));\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    if (!this.supportsToolCalling)\n      throw new Error(`Gemini: ${this.model} does not support tool calling.`);\n    this.providerLog(\"Gemini.stream - will process this chat completion.\");\n    this.resetUsage();\n\n    try {\n      const msgUUID = v4();\n      /** @type {OpenAI.OpenAI.Chat.ChatCompletion} */\n      const response = await this.client.chat.completions.create({\n        model: this.model,\n        messages: this.#formatMessages(messages),\n        stream: true,\n        stream_options: { include_usage: true },\n        ...(Array.isArray(functions) && functions?.length > 0\n          ? { tools: this.#formatFunctions(functions), tool_choice: \"auto\" }\n          : {}),\n      });\n\n      const completion = {\n        content: \"\",\n        /** @type {null|{name: string, call_id: string, arguments: string|object}} */\n        functionCall: null,\n      };\n\n      for await (const streamEvent of response) {\n        /** @type {OpenAI.OpenAI.Chat.ChatCompletionChunk} */\n        const chunk = streamEvent;\n\n        // Capture usage from final chunk (when stream_options.include_usage is true)\n        if (chunk?.usage) this.recordUsage(chunk.usage);\n        const { content, tool_calls } = chunk?.choices?.[0]?.delta || {};\n\n        if (content) {\n          completion.content += content;\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"textResponseChunk\",\n            uuid: msgUUID,\n            content,\n          });\n        }\n\n        if (tool_calls) {\n          const toolCall = tool_calls[0];\n          completion.functionCall = {\n            name: this.prefixToolCall(toolCall.function.name, \"strip\"),\n            call_id: toolCall.id,\n            arguments: toolCall.function.arguments,\n            // Preserve Gemini's thought_signature so it can be passed back in #formatMessages\n            extra_content: toolCall.extra_content ?? null,\n          };\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"toolCallInvocation\",\n            uuid: `${msgUUID}:tool_call_invocation`,\n            content: `Assembling Tool Call: ${completion.functionCall.name}(${completion.functionCall.arguments})`,\n          });\n        }\n      }\n\n      if (completion.functionCall) {\n        completion.functionCall.arguments = safeJsonParse(\n          completion.functionCall.arguments,\n          {}\n        );\n        return {\n          textResponse: completion.content,\n          functionCall: {\n            id: completion.functionCall.call_id,\n            name: completion.functionCall.name,\n            arguments: completion.functionCall.arguments,\n            extra_content: completion.functionCall.extra_content,\n          },\n          cost: this.getCost(),\n          uuid: msgUUID,\n        };\n      }\n\n      return {\n        textResponse: completion.content,\n        functionCall: null,\n        cost: this.getCost(),\n        uuid: msgUUID,\n      };\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError // Also will catch AuthenticationError!!!\n      ) {\n        throw new RetryError(error.message);\n      }\n\n      throw error;\n    }\n  }\n\n  /**\n   * Create a completion based on the received messages.\n   *\n   * @param messages A list of messages to send to the Gemini API.\n   * @param functions\n   * @returns The completion.\n   */\n  async complete(messages, functions = []) {\n    if (!this.supportsToolCalling)\n      throw new Error(`Gemini: ${this.model} does not support tool calling.`);\n    this.providerLog(\"Gemini.complete - will process this chat completion.\");\n    this.resetUsage();\n\n    try {\n      const response = await this.client.chat.completions.create({\n        model: this.model,\n        stream: false,\n        messages: this.#formatMessages(messages),\n        ...(Array.isArray(functions) && functions?.length > 0\n          ? { tools: this.#formatFunctions(functions), tool_choice: \"auto\" }\n          : {}),\n      });\n\n      if (response.usage) this.recordUsage(response.usage);\n\n      /** @type {OpenAI.OpenAI.Chat.ChatCompletionMessage} */\n      const completion = response.choices[0].message;\n      const cost = this.getCost(response.usage);\n      if (completion?.tool_calls?.length > 0) {\n        const toolCall = completion.tool_calls[0];\n        let functionArgs = safeJsonParse(toolCall.function.arguments, {});\n        return {\n          textResponse: null,\n          functionCall: {\n            name: this.prefixToolCall(toolCall.function.name, \"strip\"),\n            arguments: functionArgs,\n            id: toolCall.id,\n            // Preserve Gemini's thought_signature so it can be passed back in #formatMessages\n            extra_content: toolCall.extra_content ?? null,\n          },\n          cost,\n          usage: this.getUsage(),\n        };\n      }\n\n      return {\n        textResponse: completion.content,\n        cost,\n        usage: this.getUsage(),\n      };\n    } catch (error) {\n      // If invalid Auth error we need to abort because no amount of waiting\n      // will make auth better.\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError // Also will catch AuthenticationError!!!\n      ) {\n        throw new RetryError(error.message);\n      }\n\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = GeminiProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/genericOpenAi.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { tooledStream, tooledComplete } = require(\"./helpers/tooled.js\");\nconst { RetryError } = require(\"../error.js\");\nconst { toValidNumber } = require(\"../../../http/index.js\");\nconst { getAnythingLLMUserAgent } = require(\"../../../../endpoints/utils\");\nconst { GenericOpenAiLLM } = require(\"../../../AiProviders/genericOpenAi\");\n\n/**\n * The agent provider for the Generic OpenAI provider.\n * Since we cannot promise the generic provider even supports tool calling\n * which is nearly 100% likely it does not, we can just wrap it in untooled\n * which often is far better anyway.\n */\nclass GenericOpenAiProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    super();\n    const { model = \"gpt-3.5-turbo\" } = config;\n    const client = new OpenAI({\n      baseURL: process.env.GENERIC_OPEN_AI_BASE_PATH,\n      apiKey: process.env.GENERIC_OPEN_AI_API_KEY ?? null,\n      maxRetries: 3,\n      defaultHeaders: {\n        \"User-Agent\": getAnythingLLMUserAgent(),\n        ...GenericOpenAiLLM.parseCustomHeaders(),\n      },\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n    this._supportsToolCalling = null;\n    this.maxTokens = process.env.GENERIC_OPEN_AI_MAX_TOKENS\n      ? toValidNumber(process.env.GENERIC_OPEN_AI_MAX_TOKENS, 1024)\n      : 1024;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    // Honor streaming being disabled via ENV via user preference.\n    if (process.env.GENERIC_OPENAI_STREAMING_DISABLED === \"true\") return false;\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * - This can be any OpenAI compatible provider that supports tool calling\n   * - We check the ENV to see if the provider supports tool calling.\n   * - If the ENV is not set, we default to false.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    if (this._supportsToolCalling !== null) return this._supportsToolCalling;\n    const supportsToolCalling =\n      this.supportsNativeToolCallingViaEnv(\"generic-openai\");\n    if (supportsToolCalling)\n      this.providerLog(\n        \"Generic OpenAI supports native tool calling is ENABLED via ENV.\"\n      );\n    else\n      this.providerLog(\n        \"Generic OpenAI supports native tool calling is DISABLED via ENV. Will use UnTooled instead.\"\n      );\n    this._supportsToolCalling = supportsToolCalling;\n    return supportsToolCalling;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        temperature: 0,\n        messages,\n        max_tokens: this.maxTokens,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"Generic OpenAI chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"Generic OpenAI chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  /**\n   * Stream a chat completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to UnTooled.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.stream.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallStream.bind(this),\n        eventHandler\n      );\n    }\n\n    this.providerLog(\n      \"Provider.stream (tooled) - will process this chat completion.\"\n    );\n\n    try {\n      return await tooledStream(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        eventHandler,\n        { provider: this }\n      );\n    } catch (error) {\n      console.error(error.message, error);\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Create a non-streaming completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to UnTooled.\n   */\n  async complete(messages, functions = []) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.complete.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallChat.bind(this)\n      );\n    }\n\n    try {\n      const result = await tooledComplete(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        this.getCost.bind(this),\n        { provider: this }\n      );\n\n      if (result.retryWithError) {\n        return this.complete([...messages, result.retryWithError], functions);\n      }\n\n      return result;\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = GenericOpenAiProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/giteeai.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\nclass GiteeAIProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    super();\n    const { model = \"DeepSeek-R1\" } = config;\n    this._client = new OpenAI({\n      baseURL: \"https://ai.gitee.com/v1\",\n      apiKey: process.env.GITEE_AI_API_KEY ?? null,\n      maxRetries: 3,\n    });\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"GiteeAI chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"GiteeAI chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = GiteeAIProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/groq.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { tooledStream, tooledComplete } = require(\"./helpers/tooled.js\");\nconst { RetryError } = require(\"../error.js\");\n\n/**\n * The agent provider for the GroqAI provider.\n * Supports true OpenAI-compatible tool calling when enabled via ENV,\n * falling back to the UnTooled prompt-based approach otherwise.\n */\nclass GroqProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = \"llama3-8b-8192\" } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: \"https://api.groq.com/openai/v1\",\n      apiKey: process.env.GROQ_API_KEY,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n    this._supportsToolCalling = null;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * - Since Groq models vary in tool calling support, we check the ENV.\n   * - If the ENV is not set, we default to false.\n   * @returns {boolean}\n   */\n  supportsNativeToolCalling() {\n    if (this._supportsToolCalling !== null) return this._supportsToolCalling;\n    const supportsToolCalling = this.supportsNativeToolCallingViaEnv(\"groq\");\n    if (supportsToolCalling)\n      this.providerLog(\"Groq supports native tool calling is ENABLED via ENV.\");\n    else\n      this.providerLog(\n        \"Groq supports native tool calling is DISABLED via ENV. Will use UnTooled instead.\"\n      );\n    this._supportsToolCalling = supportsToolCalling;\n    return supportsToolCalling;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"GroqAI chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"GroqAI chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  /**\n   * Stream a chat completion with tool calling support.\n   * Uses native tool calling when enabled via ENV, otherwise falls back to UnTooled.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.stream.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallStream.bind(this),\n        eventHandler\n      );\n    }\n\n    this.providerLog(\n      \"Provider.stream (tooled) - will process this chat completion.\"\n    );\n\n    try {\n      return await tooledStream(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        eventHandler,\n        { provider: this }\n      );\n    } catch (error) {\n      console.error(error.message, error);\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Create a non-streaming completion with tool calling support.\n   * Uses native tool calling when enabled via ENV, otherwise falls back to UnTooled.\n   */\n  async complete(messages, functions = []) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.complete.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallChat.bind(this)\n      );\n    }\n\n    try {\n      const result = await tooledComplete(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        this.getCost.bind(this),\n        { provider: this }\n      );\n\n      if (result.retryWithError) {\n        return this.complete([...messages, result.retryWithError], functions);\n      }\n\n      return result;\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = GroqProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/helpers/classes.js",
    "content": "function InheritMultiple(bases = []) {\n  class Bases {\n    constructor() {\n      bases.forEach((base) => Object.assign(this, new base()));\n    }\n  }\n\n  bases.forEach((base) => {\n    Object.getOwnPropertyNames(base.prototype)\n      .filter((prop) => prop != \"constructor\")\n      .forEach((prop) => (Bases.prototype[prop] = base.prototype[prop]));\n  });\n  return Bases;\n}\n\nmodule.exports = InheritMultiple;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/helpers/tooled.js",
    "content": "const { v4 } = require(\"uuid\");\nconst { safeJsonParse } = require(\"../../../../http\");\n\n/**\n * Shared native OpenAI-compatible tool calling utilities.\n * Any provider with an OpenAI-compatible client can use these functions\n * instead of the UnTooled prompt-based approach when the model supports\n * native tool calling.\n *\n * Usage in a provider:\n *   const { tooledStream, tooledComplete } = require(\"./helpers/tooled.js\");\n *\n *   async stream(messages, functions, eventHandler) {\n *     if (functions.length > 0 && await this.supportsNativeToolCalling()) {\n *       return tooledStream(this.client, this.model, messages, functions, eventHandler);\n *     }\n *     // ... fallback to UnTooled ...\n *   }\n */\n\n/**\n * Convert aibitat function definitions to the OpenAI tools format.\n * @param {Array<{name: string, description: string, parameters: object}>} functions\n * @returns {Array<{type: \"function\", function: {name: string, description: string, parameters: object}}>}\n */\nfunction formatFunctionsToTools(functions) {\n  if (!Array.isArray(functions) || functions.length === 0) return [];\n  return functions.map((func) => ({\n    type: \"function\",\n    function: {\n      name: func.name,\n      description: func.description,\n      parameters: func.parameters,\n    },\n  }));\n}\n\n/**\n * Format message content with attachments (images) for multimodal support.\n * Transforms a message with attachments into the OpenAI-compatible format.\n * @param {Object} message - The message to format\n * @returns {Object} Message with content formatted for the API\n */\nfunction formatMessageWithAttachments(message) {\n  if (!message.attachments || message.attachments.length === 0) {\n    return message;\n  }\n\n  // Transform message with attachments into multimodal format\n  const content = [{ type: \"text\", text: message.content }];\n  for (const attachment of message.attachments) {\n    content.push({\n      type: \"image_url\",\n      image_url: {\n        url: attachment.contentString,\n      },\n    });\n  }\n\n  // Return message without attachments property, with content as array\n  const { attachments: _, ...rest } = message;\n  return {\n    ...rest,\n    content,\n  };\n}\n\n/**\n * Convert the aibitat message history (which uses role:\"function\" with\n * `originalFunctionCall` metadata) into the OpenAI tool-calling message\n * format (assistant `tool_calls` + role:\"tool\" pairs).\n * Also handles image attachments for multimodal support.\n * @param {Array} messages\n * @param {{injectReasoningContent?: boolean}} options\n *   - injectReasoningContent: when true, ensures every assistant message has\n *     a `reasoning_content` field (required by DeepSeek thinking-mode models).\n * @returns {Array} Messages formatted for the OpenAI tools API\n */\nfunction formatMessagesForTools(messages, options = {}) {\n  const formattedMessages = [];\n  const { injectReasoningContent = false } = options;\n\n  for (const message of messages) {\n    if (message.role === \"function\") {\n      if (message.originalFunctionCall?.id) {\n        const prevMsg = formattedMessages[formattedMessages.length - 1];\n        if (!prevMsg || prevMsg.role !== \"assistant\" || !prevMsg.tool_calls) {\n          formattedMessages.push({\n            role: \"assistant\",\n            content: null,\n            ...(injectReasoningContent ? { reasoning_content: \"\" } : {}),\n            tool_calls: [\n              {\n                id: message.originalFunctionCall.id,\n                type: \"function\",\n                function: {\n                  name: message.originalFunctionCall.name,\n                  arguments:\n                    typeof message.originalFunctionCall.arguments === \"string\"\n                      ? message.originalFunctionCall.arguments\n                      : JSON.stringify(message.originalFunctionCall.arguments),\n                },\n              },\n            ],\n          });\n        }\n        formattedMessages.push({\n          role: \"tool\",\n          tool_call_id: message.originalFunctionCall.id,\n          content:\n            typeof message.content === \"string\"\n              ? message.content\n              : JSON.stringify(message.content),\n        });\n      } else {\n        const toolCallId = `call_${v4()}`;\n        formattedMessages.push({\n          role: \"assistant\",\n          content: null,\n          ...(injectReasoningContent ? { reasoning_content: \"\" } : {}),\n          tool_calls: [\n            {\n              id: toolCallId,\n              type: \"function\",\n              function: {\n                name: message.name,\n                arguments: \"{}\",\n              },\n            },\n          ],\n        });\n        formattedMessages.push({\n          role: \"tool\",\n          tool_call_id: toolCallId,\n          content:\n            typeof message.content === \"string\"\n              ? message.content\n              : JSON.stringify(message.content),\n        });\n      }\n    } else if (\n      injectReasoningContent &&\n      message.role === \"assistant\" &&\n      !(\"reasoning_content\" in message)\n    ) {\n      formattedMessages.push(\n        formatMessageWithAttachments({ ...message, reasoning_content: \"\" })\n      );\n    } else {\n      formattedMessages.push(formatMessageWithAttachments(message));\n    }\n  }\n\n  return formattedMessages;\n}\n\n/**\n * Stream a chat completion using native OpenAI-compatible tool calling.\n * Handles parallel tool calls by tracking each tool call by its streaming\n * index, then returning only the first one for the agent framework to process.\n *\n * @param {import(\"openai\").OpenAI} client - OpenAI-compatible client\n * @param {string} model - Model identifier\n * @param {Array} messages - Raw aibitat message history\n * @param {Array} functions - Aibitat function definitions\n * @param {function|null} eventHandler - Stream event handler\n * @param {{injectReasoningContent?: boolean, provider?: object}} options - Provider-specific options\n *   - provider: If passed, automatically handles usage tracking via provider.resetUsage()/recordUsage()\n * @returns {Promise<{textResponse: string, functionCall: object|null, uuid: string, usage: object|null}>}\n */\nasync function tooledStream(\n  client,\n  model,\n  messages,\n  functions = [],\n  eventHandler = null,\n  options = {}\n) {\n  const { provider, ...formatOptions } = options;\n\n  // Auto-reset usage if provider is passed\n  if (provider?.resetUsage) {\n    try {\n      provider.resetUsage();\n    } catch {}\n  }\n\n  const msgUUID = v4();\n  const formattedMessages = formatMessagesForTools(messages, formatOptions);\n  const tools = formatFunctionsToTools(functions);\n\n  const stream = await client.chat.completions.create({\n    model,\n    stream: true,\n    stream_options: { include_usage: true },\n    messages: formattedMessages,\n    ...(tools.length > 0 ? { tools } : {}),\n  });\n\n  const result = {\n    functionCall: null,\n    textResponse: \"\",\n  };\n\n  const toolCallsByIndex = {};\n  let usage = null;\n\n  for await (const chunk of stream) {\n    // Capture usage from final chunk (some providers send usage after finish_reason)\n    if (chunk?.usage) {\n      usage = chunk.usage;\n    }\n\n    if (!chunk?.choices?.[0]) continue;\n    const choice = chunk.choices[0];\n\n    if (choice.delta?.content) {\n      result.textResponse += choice.delta.content;\n      eventHandler?.(\"reportStreamEvent\", {\n        type: \"textResponseChunk\",\n        uuid: msgUUID,\n        content: choice.delta.content,\n      });\n    }\n\n    if (choice.delta?.tool_calls) {\n      for (const toolCall of choice.delta.tool_calls) {\n        const idx = toolCall.index ?? 0;\n\n        if (toolCall.id) {\n          toolCallsByIndex[idx] = {\n            id: toolCall.id,\n            name: toolCall.function?.name || \"\",\n            arguments: toolCall.function?.arguments || \"\",\n          };\n        } else if (toolCallsByIndex[idx]) {\n          if (toolCall.function?.name) {\n            toolCallsByIndex[idx].name += toolCall.function.name;\n          }\n          if (toolCall.function?.arguments) {\n            toolCallsByIndex[idx].arguments += toolCall.function.arguments;\n          }\n        }\n\n        if (toolCallsByIndex[idx]) {\n          eventHandler?.(\"reportStreamEvent\", {\n            uuid: `${msgUUID}:tool_call_invocation`,\n            type: \"toolCallInvocation\",\n            content: `Assembling Tool Call: ${toolCallsByIndex[idx].name}(${toolCallsByIndex[idx].arguments})`,\n          });\n        }\n      }\n    }\n  }\n\n  // Auto-record usage if provider is passed and usage is available\n  if (provider?.recordUsage && usage) {\n    try {\n      provider.recordUsage(usage);\n    } catch {}\n  }\n\n  const toolCallIndices = Object.keys(toolCallsByIndex).map(Number);\n  if (toolCallIndices.length > 0) {\n    const firstToolCall = toolCallsByIndex[Math.min(...toolCallIndices)];\n    result.functionCall = {\n      id: firstToolCall.id,\n      name: firstToolCall.name,\n      arguments: safeJsonParse(firstToolCall.arguments, {}),\n    };\n  }\n\n  return {\n    textResponse: result.textResponse,\n    functionCall: result.functionCall,\n    uuid: msgUUID,\n    usage,\n  };\n}\n\n/**\n * Non-streaming chat completion using native OpenAI-compatible tool calling.\n * Returns the first tool call if the model requests any, otherwise the text response.\n *\n * @param {import(\"openai\").OpenAI} client - OpenAI-compatible client\n * @param {string} model - Model identifier\n * @param {Array} messages - Raw aibitat message history\n * @param {Array} functions - Aibitat function definitions\n * @param {function} getCostFn - Provider's getCost function\n * @param {{injectReasoningContent?: boolean, provider?: object}} options - Provider-specific options\n *   - provider: If passed, automatically handles usage tracking via provider.resetUsage()/recordUsage()\n * @returns {Promise<{textResponse: string|null, functionCall: object|null, cost: number, usage: object|null}>}\n */\nasync function tooledComplete(\n  client,\n  model,\n  messages,\n  functions = [],\n  getCostFn = () => 0,\n  options = {}\n) {\n  const { provider, ...formatOptions } = options;\n\n  // Auto-reset usage if provider is passed\n  if (provider?.resetUsage) {\n    try {\n      provider.resetUsage();\n    } catch {}\n  }\n\n  const formattedMessages = formatMessagesForTools(messages, formatOptions);\n  const tools = formatFunctionsToTools(functions);\n\n  const response = await client.chat.completions.create({\n    model,\n    stream: false,\n    messages: formattedMessages,\n    ...(tools.length > 0 ? { tools } : {}),\n  });\n\n  const completion = response.choices[0].message;\n  const cost = getCostFn(response.usage);\n  const usage = response.usage || null;\n\n  // Auto-record usage if provider is passed and usage is available\n  if (provider?.recordUsage && usage) {\n    try {\n      provider.recordUsage(usage);\n    } catch {}\n  }\n\n  if (completion.tool_calls && completion.tool_calls.length > 0) {\n    const toolCall = completion.tool_calls[0];\n    const functionArgs = safeJsonParse(toolCall.function.arguments, null);\n\n    if (functionArgs === null) {\n      return {\n        textResponse: null,\n        retryWithError: {\n          role: \"function\",\n          name: toolCall.function.name,\n          content: `Failed to parse tool call arguments as JSON. Raw arguments: ${toolCall.function.arguments}`,\n          originalFunctionCall: {\n            id: toolCall.id,\n            name: toolCall.function.name,\n            arguments: toolCall.function.arguments,\n          },\n        },\n        cost,\n        usage,\n      };\n    }\n\n    return {\n      textResponse: null,\n      functionCall: {\n        id: toolCall.id,\n        name: toolCall.function.name,\n        arguments: functionArgs,\n      },\n      cost,\n      usage,\n    };\n  }\n\n  return {\n    textResponse: completion.content,\n    cost,\n    usage,\n  };\n}\n\nmodule.exports = {\n  formatFunctionsToTools,\n  formatMessagesForTools,\n  tooledStream,\n  tooledComplete,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/helpers/untooled.js",
    "content": "const { safeJsonParse } = require(\"../../../../http\");\nconst { Deduplicator } = require(\"../../utils/dedupe\");\nconst { v4 } = require(\"uuid\");\n\n// Useful inheritance class for a model which supports OpenAi schema for API requests\n// but does not have tool-calling or JSON output support.\nclass UnTooled {\n  constructor() {\n    this.deduplicator = new Deduplicator();\n  }\n\n  cleanMsgs(messages) {\n    const modifiedMessages = [];\n    messages.forEach((msg) => {\n      if (msg.role === \"function\") {\n        const prevMsg = modifiedMessages[modifiedMessages.length - 1].content;\n        modifiedMessages[modifiedMessages.length - 1].content =\n          `${prevMsg}\\n${msg.content}`;\n        return;\n      }\n      // Format messages with attachments for multimodal support\n      // Uses formatMessageWithAttachments inherited from Provider base class\n      modifiedMessages.push(this.formatMessageWithAttachments(msg));\n    });\n    return modifiedMessages;\n  }\n\n  showcaseFunctions(functions = []) {\n    let output = \"\";\n    functions.forEach((def) => {\n      let shotExample = `-----------\nFunction name: ${def.name}\nFunction Description: ${def.description}\nFunction parameters in JSON format:\n${JSON.stringify(def.parameters.properties, null, 4)}\\n`;\n\n      if (Array.isArray(def.examples)) {\n        def.examples.forEach(({ prompt, call }) => {\n          shotExample += `Query: \"${prompt}\"\\nJSON: ${JSON.stringify({\n            name: def.name,\n            arguments: safeJsonParse(call, {}),\n          })}\\n`;\n        });\n      }\n      output += `${shotExample}-----------\\n`;\n    });\n    return output;\n  }\n\n  /**\n   * Check if a function call is an MCP tool.\n   * We do this because some MCP tools dont return values and will cause infinite loops in calling for Untooled to call the same function over and over again.\n   * Any MCP tool is automatically marked with a cooldown to prevent infinite loops of the same function over and over again.\n   *\n   * This can lead to unexpected behavior if you want a model using Untooled to call a repeat action multiple times.\n   * eg: Create 3 Jira tickets about x, y, and z. -> will skip y and z if you don't disable the cooldown.\n   *\n   * You can disable this check by setting the `MCP_NO_COOLDOWN` flag to any value in the ENV.\n   *\n   * @param {{name: string, arguments: Object}} functionCall - The function call to check.\n   * @param {Object[]} functions - The list of functions definitions to check against.\n   * @return {boolean} - True if the function call is an MCP tool, false otherwise.\n   */\n  isMCPTool(functionCall = {}, functions = []) {\n    if (process.env.MCP_NO_COOLDOWN) return false;\n\n    const foundFunc = functions.find(\n      (def) => def?.name?.toLowerCase() === functionCall.name?.toLowerCase()\n    );\n    if (!foundFunc) return false;\n    return foundFunc?.isMCPTool || false;\n  }\n\n  /**\n   * Validate a function call against a list of functions.\n   * @param {{name: string, arguments: Object}} functionCall - The function call to validate.\n   * @param {Object[]} functions - The list of functions definitions to validate against.\n   * @return {{valid: boolean, reason: string|null}} - The validation result.\n   */\n  validFuncCall(functionCall = {}, functions = []) {\n    if (\n      !functionCall ||\n      !functionCall?.hasOwnProperty(\"name\") ||\n      !functionCall?.hasOwnProperty(\"arguments\")\n    ) {\n      return {\n        valid: false,\n        reason: \"Missing name or arguments in function call.\",\n      };\n    }\n\n    const foundFunc = functions.find((def) => def.name === functionCall.name);\n    if (!foundFunc)\n      return { valid: false, reason: \"Function name does not exist.\" };\n\n    const schemaProps = Object.keys(foundFunc?.parameters?.properties || {});\n    const requiredProps = foundFunc?.parameters?.required || [];\n    const providedProps = Object.keys(functionCall.arguments);\n\n    for (const requiredProp of requiredProps) {\n      if (!providedProps.includes(requiredProp)) {\n        return {\n          valid: false,\n          reason: `Missing required argument: ${requiredProp}`,\n        };\n      }\n    }\n\n    // Ensure all provided arguments are valid for the schema\n    // This is to prevent the model from hallucinating or providing invalid additional arguments.\n    for (const providedProp of providedProps) {\n      if (!schemaProps.includes(providedProp)) {\n        return {\n          valid: false,\n          reason: `Unknown argument: ${providedProp} provided but not in schema.`,\n        };\n      }\n    }\n\n    return { valid: true, reason: null };\n  }\n\n  buildToolCallMessages(history = [], functions = []) {\n    // Format history messages with attachments for multimodal support\n    const formattedHistory = history.map((msg) =>\n      this.formatMessageWithAttachments(msg)\n    );\n\n    return [\n      {\n        content: `You are a program which picks the most optimal function and parameters to call.\n      DO NOT HAVE TO PICK A FUNCTION IF IT WILL NOT HELP ANSWER OR FULFILL THE USER'S QUERY.\n      When a function is selection, respond in JSON with no additional text.\n      When there is no relevant function to call - return with a regular chat text response.\n      Your task is to pick a **single** function that we will use to call, if any seem useful or relevant for the user query.\n\n      All JSON responses should have two keys.\n      'name': this is the name of the function name to call. eg: 'web-scraper', 'rag-memory', etc..\n      'arguments': this is an object with the function properties to invoke the function.\n      DO NOT INCLUDE ANY OTHER KEYS IN JSON RESPONSES.\n\n      Here are the available tools you can use an examples of a query and response so you can understand how each one works.\n      ${this.showcaseFunctions(functions)}\n\n      Now pick a function if there is an appropriate one to use given the last user message and the given conversation so far.`,\n        role: \"system\",\n      },\n      ...formattedHistory,\n    ];\n  }\n\n  async functionCall(messages, functions, chatCb = null) {\n    const history = [...messages].filter((msg) =>\n      [\"user\", \"assistant\"].includes(msg.role)\n    );\n    if (history[history.length - 1].role !== \"user\") return null;\n    const historyMessages = this.buildToolCallMessages(history, functions);\n    const response = await chatCb({ messages: historyMessages });\n\n    const call = safeJsonParse(response, null);\n    if (call === null) return { toolCall: null, text: response }; // failed to parse, so must be text.\n\n    const { valid, reason } = this.validFuncCall(call, functions);\n    if (!valid) {\n      this.providerLog(`Invalid function tool call: ${reason}.`);\n      return { toolCall: null, text: null };\n    }\n\n    const { isDuplicate, reason: duplicateReason } =\n      this.deduplicator.isDuplicate(call.name, call.arguments);\n    if (isDuplicate) {\n      this.providerLog(\n        `Cannot call ${call.name} again because ${duplicateReason}.`\n      );\n      return { toolCall: null, text: null };\n    }\n\n    return { toolCall: call, text: null };\n  }\n\n  async streamingFunctionCall(\n    messages,\n    functions,\n    chatCb = null,\n    eventHandler = null\n  ) {\n    const history = [...messages].filter((msg) =>\n      [\"user\", \"assistant\"].includes(msg.role)\n    );\n    if (history[history.length - 1].role !== \"user\") return null;\n\n    const msgUUID = v4();\n    let textResponse = \"\";\n    const historyMessages = this.buildToolCallMessages(history, functions);\n    const stream = await chatCb({ messages: historyMessages });\n\n    eventHandler?.(\"reportStreamEvent\", {\n      type: \"statusResponse\",\n      uuid: v4(),\n      content: \"Agent is thinking...\",\n    });\n\n    for await (const chunk of stream) {\n      if (!chunk?.choices?.[0]) continue; // Skip if no choices\n      const choice = chunk.choices[0];\n\n      if (choice.delta?.content) {\n        textResponse += choice.delta.content;\n        eventHandler?.(\"reportStreamEvent\", {\n          type: \"statusResponse\",\n          uuid: msgUUID,\n          content: choice.delta.content,\n        });\n      }\n    }\n\n    const call = safeJsonParse(textResponse, null);\n    if (call === null)\n      return { toolCall: null, text: textResponse, uuid: msgUUID }; // failed to parse, so must be regular text response.\n\n    const { valid, reason } = this.validFuncCall(call, functions);\n    if (!valid) {\n      this.providerLog(`Invalid function tool call: ${reason}.`);\n      eventHandler?.(\"reportStreamEvent\", {\n        type: \"removeStatusResponse\",\n        uuid: msgUUID,\n        content:\n          \"The model attempted to make an invalid function call - it was ignored.\",\n      });\n      return { toolCall: null, text: null, uuid: msgUUID };\n    }\n\n    const { isDuplicate, reason: duplicateReason } =\n      this.deduplicator.isDuplicate(call.name, call.arguments);\n    if (isDuplicate) {\n      this.providerLog(\n        `Cannot call ${call.name} again because ${duplicateReason}.`\n      );\n      eventHandler?.(\"reportStreamEvent\", {\n        type: \"removeStatusResponse\",\n        uuid: msgUUID,\n        content:\n          \"The model tried to call a function with the same arguments as a previous call - it was ignored.\",\n      });\n      return { toolCall: null, text: null, uuid: msgUUID };\n    }\n\n    eventHandler?.(\"reportStreamEvent\", {\n      uuid: `${msgUUID}:tool_call_invocation`,\n      type: \"toolCallInvocation\",\n      content: `Parsed Tool Call: ${call.name}(${JSON.stringify(call.arguments)})`,\n    });\n    return { toolCall: call, text: null, uuid: msgUUID };\n  }\n\n  /**\n   * Stream a chat completion from the LLM with tool calling\n   * Note: This using the OpenAI API format and may need to be adapted for other providers.\n   *\n   * @param {any[]} messages - The messages to send to the LLM.\n   * @param {any[]} functions - The functions to use in the LLM.\n   * @param {function} chatCallback - A callback function to handle the chat completion.\n   * @param {function} eventHandler - The event handler to use to report stream events.\n   * @returns {Promise<{ functionCall: any, textResponse: string }>} - The result of the chat completion.\n   */\n  async stream(\n    messages,\n    functions = [],\n    chatCallback = null,\n    eventHandler = null\n  ) {\n    this.providerLog(\"Untooled.stream - will process this chat completion.\");\n    // eslint-disable-next-line\n    try {\n      let completion = { content: \"\" };\n      if (functions.length > 0) {\n        const {\n          toolCall,\n          text,\n          uuid: msgUUID,\n        } = await this.streamingFunctionCall(\n          messages,\n          functions,\n          chatCallback,\n          eventHandler\n        );\n\n        if (toolCall !== null) {\n          this.providerLog(`Valid tool call found - running ${toolCall.name}.`);\n          this.deduplicator.trackRun(toolCall.name, toolCall.arguments, {\n            cooldown: this.isMCPTool(toolCall, functions),\n          });\n          return {\n            result: null,\n            functionCall: {\n              name: toolCall.name,\n              arguments: toolCall.arguments,\n            },\n            cost: 0,\n          };\n        }\n\n        if (text) {\n          this.providerLog(\n            `No tool call found in the response - will send as a full text response.`\n          );\n          completion.content = text;\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"removeStatusResponse\",\n            uuid: msgUUID,\n            content: \"No tool call found in the response\",\n          });\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"statusResponse\",\n            uuid: v4(),\n            content: \"Done thinking.\",\n          });\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"fullTextResponse\",\n            uuid: v4(),\n            content: text,\n          });\n        }\n      }\n\n      if (!completion?.content) {\n        eventHandler?.(\"reportStreamEvent\", {\n          type: \"statusResponse\",\n          uuid: v4(),\n          content: \"Done thinking.\",\n        });\n\n        this.providerLog(\n          \"Will assume chat completion without tool call inputs.\"\n        );\n        const msgUUID = v4();\n        completion = { content: \"\" };\n        const stream = await chatCallback({\n          messages: this.cleanMsgs(messages),\n        });\n\n        for await (const chunk of stream) {\n          if (!chunk?.choices?.[0]) continue; // Skip if no choices\n          const choice = chunk.choices[0];\n          if (choice.delta?.content) {\n            completion.content += choice.delta.content;\n            eventHandler?.(\"reportStreamEvent\", {\n              type: \"textResponseChunk\",\n              uuid: msgUUID,\n              content: choice.delta.content,\n            });\n          }\n        }\n      }\n\n      // The UnTooled class inherited Deduplicator is mostly useful to prevent the agent\n      // from calling the exact same function over and over in a loop within a single chat exchange\n      // _but_ we should enable it to call previously used tools in a new chat interaction.\n      this.deduplicator.reset(\"runs\");\n      return {\n        textResponse: completion.content,\n        cost: 0,\n      };\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  /**\n   * Create a completion based on the received messages.\n   *\n   * @param messages A list of messages to send to the API.\n   * @param functions\n   * @param chatCallback - A callback function to handle the chat completion.\n   * @returns The completion.\n   */\n  async complete(messages, functions = [], chatCallback = null) {\n    this.providerLog(\"Untooled.complete - will process this chat completion.\");\n    // eslint-disable-next-line\n    try {\n      let completion = { content: \"\" };\n      if (functions.length > 0) {\n        const { toolCall, text } = await this.functionCall(\n          messages,\n          functions,\n          chatCallback\n        );\n\n        if (toolCall !== null) {\n          this.providerLog(`Valid tool call found - running ${toolCall.name}.`);\n          this.deduplicator.trackRun(toolCall.name, toolCall.arguments, {\n            cooldown: this.isMCPTool(toolCall, functions),\n          });\n          return {\n            result: null,\n            functionCall: {\n              name: toolCall.name,\n              arguments: toolCall.arguments,\n            },\n            cost: 0,\n          };\n        }\n        completion.content = text;\n      }\n\n      // If there are no functions, we want to run a normal chat completion.\n      if (!completion?.content) {\n        this.providerLog(\n          \"Will assume chat completion without tool call inputs.\"\n        );\n        const response = await chatCallback({\n          messages: this.cleanMsgs(messages),\n        });\n        // If the response from the callback is the raw OpenAI Spec response object, we can use that directly.\n        // Otherwise, we will assume the response is just the string output we wanted (see: `#handleFunctionCallChat` which returns the content only)\n        // This handles both streaming and non-streaming completions.\n        completion =\n          typeof response === \"string\"\n            ? { content: response }\n            : response.choices?.[0]?.message;\n      }\n\n      // The UnTooled class inherited Deduplicator is mostly useful to prevent the agent\n      // from calling the exact same function over and over in a loop within a single chat exchange\n      // _but_ we should enable it to call previously used tools in a new chat interaction.\n      this.deduplicator.reset(\"runs\");\n      return {\n        textResponse: completion.content,\n        cost: 0,\n      };\n    } catch (error) {\n      throw error;\n    }\n  }\n}\n\nmodule.exports = UnTooled;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/index.js",
    "content": "const OpenAIProvider = require(\"./openai.js\");\nconst AnthropicProvider = require(\"./anthropic.js\");\nconst LMStudioProvider = require(\"./lmstudio.js\");\nconst OllamaProvider = require(\"./ollama.js\");\nconst GroqProvider = require(\"./groq.js\");\nconst TogetherAIProvider = require(\"./togetherai.js\");\nconst AzureOpenAiProvider = require(\"./azure.js\");\nconst KoboldCPPProvider = require(\"./koboldcpp.js\");\nconst LocalAIProvider = require(\"./localai.js\");\nconst OpenRouterProvider = require(\"./openrouter.js\");\nconst MistralProvider = require(\"./mistral.js\");\nconst GenericOpenAiProvider = require(\"./genericOpenAi.js\");\nconst PerplexityProvider = require(\"./perplexity.js\");\nconst TextWebGenUiProvider = require(\"./textgenwebui.js\");\nconst AWSBedrockProvider = require(\"./bedrock.js\");\nconst FireworksAIProvider = require(\"./fireworksai.js\");\nconst DeepSeekProvider = require(\"./deepseek.js\");\nconst LiteLLMProvider = require(\"./litellm.js\");\nconst ApiPieProvider = require(\"./apipie.js\");\nconst XAIProvider = require(\"./xai.js\");\nconst ZAIProvider = require(\"./zai.js\");\nconst NovitaProvider = require(\"./novita.js\");\nconst NvidiaNimProvider = require(\"./nvidiaNim.js\");\nconst PPIOProvider = require(\"./ppio.js\");\nconst GeminiProvider = require(\"./gemini.js\");\nconst DellProAiStudioProvider = require(\"./dellProAiStudio.js\");\nconst MoonshotAiProvider = require(\"./moonshotAi.js\");\nconst CometApiProvider = require(\"./cometapi.js\");\nconst FoundryProvider = require(\"./foundry.js\");\nconst GiteeAIProvider = require(\"./giteeai.js\");\nconst CohereProvider = require(\"./cohere.js\");\nconst DockerModelRunnerProvider = require(\"./dockerModelRunner.js\");\nconst PrivatemodeProvider = require(\"./privatemode.js\");\nconst SambaNovaProvider = require(\"./sambanova.js\");\nconst LemonadeProvider = require(\"./lemonade.js\");\n\nmodule.exports = {\n  OpenAIProvider,\n  AnthropicProvider,\n  LMStudioProvider,\n  OllamaProvider,\n  GroqProvider,\n  TogetherAIProvider,\n  AzureOpenAiProvider,\n  KoboldCPPProvider,\n  LocalAIProvider,\n  OpenRouterProvider,\n  MistralProvider,\n  GenericOpenAiProvider,\n  DeepSeekProvider,\n  PerplexityProvider,\n  TextWebGenUiProvider,\n  AWSBedrockProvider,\n  FireworksAIProvider,\n  LiteLLMProvider,\n  ApiPieProvider,\n  XAIProvider,\n  ZAIProvider,\n  NovitaProvider,\n  CometApiProvider,\n  NvidiaNimProvider,\n  PPIOProvider,\n  GeminiProvider,\n  DellProAiStudioProvider,\n  MoonshotAiProvider,\n  FoundryProvider,\n  GiteeAIProvider,\n  CohereProvider,\n  DockerModelRunnerProvider,\n  PrivatemodeProvider,\n  SambaNovaProvider,\n  LemonadeProvider,\n};\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/koboldcpp.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\n/**\n * The agent provider for the KoboldCPP provider.\n */\nclass KoboldCPPProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(_config = {}) {\n    super();\n    const model = process.env.KOBOLD_CPP_MODEL_PREF ?? null;\n    const client = new OpenAI({\n      baseURL: process.env.KOBOLD_CPP_BASE_PATH?.replace(/\\/+$/, \"\"),\n      apiKey: null,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.maxTokens = Number(process.env.KOBOLD_CPP_MAX_TOKENS) || 2048;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n        max_tokens: this.maxTokens,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"KoboldCPP chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"KoboldCPP chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n      max_tokens: this.maxTokens,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   * Stubbed since KoboldCPP has no cost basis.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = KoboldCPPProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/lemonade.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { tooledStream, tooledComplete } = require(\"./helpers/tooled.js\");\nconst { RetryError } = require(\"../error.js\");\nconst {\n  LemonadeLLM,\n  parseLemonadeServerEndpoint,\n} = require(\"../../../AiProviders/lemonade/index.js\");\n\n/**\n * The agent provider for the Lemonade.\n */\nclass LemonadeProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  /**\n   *\n   * @param {{model?: string}} config\n   */\n  constructor(config = {}) {\n    super();\n    const model = config?.model || process.env.LEMONADE_LLM_MODEL_PREF || null;\n    const client = new OpenAI({\n      baseURL: parseLemonadeServerEndpoint(\n        process.env.LEMONADE_LLM_BASE_PATH,\n        \"openai\"\n      ),\n      apiKey: null,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n    this.preloaded = false;\n    this._supportsToolCalling = null;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  async preloadModel() {\n    if (this.preloaded) return;\n    await LemonadeLLM.loadModel(this.model);\n    this.preloaded = true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * - Since Lemonade models vary in tool calling support, we check the ENV.\n   * - If the ENV is not set and the capabilities are not set, we default to false.\n   * - To enable tool calling for a model, set the ENV flag for `PROVIDER_SUPPORTS_NATIVE_TOOL_CALLING` to include `lemonade`.\n   * - or update the label in the Lemonade server to include `tool-calling`.\n   * @returns {boolean|Promise<boolean>}\n   */\n  async supportsNativeToolCalling() {\n    if (this._supportsToolCalling !== null) return this._supportsToolCalling;\n    const lemonade = new LemonadeLLM(null, this.model);\n\n    // Labels can be missing for tool calling models, so we also check if ENV flag is set\n    const supportsToolCallingFlag =\n      this.supportsNativeToolCallingViaEnv(\"lemonade\");\n    if (supportsToolCallingFlag) {\n      this.providerLog(\n        \"Lemonade supports native tool calling is ENABLED via ENV.\"\n      );\n      this._supportsToolCalling = true;\n      return this._supportsToolCalling;\n    }\n\n    const capabilities = await lemonade.getModelCapabilities();\n    this._supportsToolCalling = capabilities.tools === true;\n    return this._supportsToolCalling;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"Lemonade chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"Lemonade chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  /**\n   * Stream a chat completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to UnTooled.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    await this.preloadModel();\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.stream.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallStream.bind(this),\n        eventHandler\n      );\n    }\n\n    this.providerLog(\n      \"LemonadeProvider.stream (tooled) - will process this chat completion.\"\n    );\n\n    try {\n      return await tooledStream(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        eventHandler,\n        { provider: this }\n      );\n    } catch (error) {\n      console.error(error.message, error);\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Create a non-streaming completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to UnTooled.\n   */\n  async complete(messages, functions = []) {\n    await this.preloadModel();\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.complete.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallChat.bind(this)\n      );\n    }\n\n    try {\n      const result = await tooledComplete(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        this.getCost.bind(this),\n        { provider: this }\n      );\n\n      if (result.retryWithError) {\n        return this.complete([...messages, result.retryWithError], functions);\n      }\n\n      return result;\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   * Stubbed since Lemonade has no cost basis.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = LemonadeProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/litellm.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { tooledStream, tooledComplete } = require(\"./helpers/tooled.js\");\nconst { RetryError } = require(\"../error.js\");\n\n/**\n * The agent provider for LiteLLM.\n * Supports true OpenAI-compatible tool calling when enabled via ENV,\n * falling back to the UnTooled prompt-based approach otherwise.\n */\nclass LiteLLMProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    super();\n    const { model = null } = config;\n    const client = new OpenAI({\n      baseURL: process.env.LITE_LLM_BASE_PATH,\n      apiKey: process.env.LITE_LLM_API_KEY ?? null,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model || process.env.LITE_LLM_MODEL_PREF;\n    this.verbose = true;\n    this._supportsToolCalling = null;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * - Since LiteLLM models vary in tool calling support, we check the ENV.\n   * - If the ENV is not set, we default to false.\n   * @returns {boolean}\n   */\n  supportsNativeToolCalling() {\n    if (this._supportsToolCalling !== null) return this._supportsToolCalling;\n    const supportsToolCalling = this.supportsNativeToolCallingViaEnv(\"litellm\");\n    if (supportsToolCalling)\n      this.providerLog(\n        \"LiteLLM supports native tool calling is ENABLED via ENV.\"\n      );\n    else\n      this.providerLog(\n        \"LiteLLM supports native tool calling is DISABLED via ENV. Will use UnTooled instead.\"\n      );\n    this._supportsToolCalling = supportsToolCalling;\n    return supportsToolCalling;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"LiteLLM chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"LiteLLM chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  /**\n   * Stream a chat completion with tool calling support.\n   * Uses native tool calling when enabled via ENV, otherwise falls back to UnTooled.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.stream.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallStream.bind(this),\n        eventHandler\n      );\n    }\n\n    this.providerLog(\n      \"Provider.stream (tooled) - will process this chat completion.\"\n    );\n\n    try {\n      return await tooledStream(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        eventHandler,\n        { provider: this }\n      );\n    } catch (error) {\n      console.error(error.message, error);\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Create a non-streaming completion with tool calling support.\n   * Uses native tool calling when enabled via ENV, otherwise falls back to UnTooled.\n   */\n  async complete(messages, functions = []) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.complete.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallChat.bind(this)\n      );\n    }\n\n    try {\n      const result = await tooledComplete(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        this.getCost.bind(this),\n        { provider: this }\n      );\n\n      if (result.retryWithError) {\n        return this.complete([...messages, result.retryWithError], functions);\n      }\n\n      return result;\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = LiteLLMProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/lmstudio.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { tooledStream, tooledComplete } = require(\"./helpers/tooled.js\");\nconst { RetryError } = require(\"../error.js\");\nconst {\n  LMStudioLLM,\n  parseLMStudioBasePath,\n} = require(\"../../../AiProviders/lmStudio/index.js\");\n\n/**\n * The agent provider for the LMStudio.\n * Supports true OpenAI-compatible tool calling when the model supports it,\n * falling back to the UnTooled prompt-based approach otherwise.\n */\nclass LMStudioProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  /**\n   * @param {{model?: string}} config\n   */\n  constructor(config = {}) {\n    super();\n    const model = config?.model || process.env.LMSTUDIO_MODEL_PREF;\n    if (!model) throw new Error(\"LMStudio must have a valid model set.\");\n\n    const apiKey = process.env.LMSTUDIO_AUTH_TOKEN ?? null;\n    const client = new OpenAI({\n      baseURL: parseLMStudioBasePath(process.env.LMSTUDIO_BASE_PATH),\n      apiKey,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n    this._supportsToolCalling = null;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether the loaded model supports native OpenAI-compatible tool calling.\n   * Checks the LMStudio /api/v1/models endpoint for the model's capabilities.\n   * @returns {Promise<boolean>}\n   */\n  async supportsNativeToolCalling() {\n    if (this._supportsToolCalling !== null) return this._supportsToolCalling;\n    const lmstudio = new LMStudioLLM(null, this.model);\n    const capabilities = await lmstudio.getModelCapabilities();\n    this._supportsToolCalling = capabilities.tools === true;\n    return this._supportsToolCalling;\n  }\n\n  // ---- UnTooled callbacks (used when native tool calling is not supported) ----\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    await LMStudioLLM.cacheContextWindows();\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"LMStudio chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"LMStudio chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    await LMStudioLLM.cacheContextWindows();\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  /**\n   * Stream a chat completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to UnTooled.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.stream.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallStream.bind(this),\n        eventHandler\n      );\n    }\n\n    this.providerLog(\n      \"Provider.stream (tooled) - will process this chat completion.\"\n    );\n\n    try {\n      await LMStudioLLM.cacheContextWindows();\n      return await tooledStream(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        eventHandler,\n        { provider: this }\n      );\n    } catch (error) {\n      console.error(error.message, error);\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Create a non-streaming completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to UnTooled.\n   */\n  async complete(messages, functions = []) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.complete.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallChat.bind(this)\n      );\n    }\n\n    try {\n      await LMStudioLLM.cacheContextWindows();\n      const result = await tooledComplete(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        this.getCost.bind(this),\n        { provider: this }\n      );\n\n      if (result.retryWithError) {\n        return this.complete([...messages, result.retryWithError], functions);\n      }\n\n      return result;\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   * Stubbed since LMStudio has no cost basis.\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = LMStudioProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/localai.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { tooledStream, tooledComplete } = require(\"./helpers/tooled.js\");\nconst { RetryError } = require(\"../error.js\");\n\n/**\n * The agent provider for the LocalAI provider.\n * Supports native OpenAI-compatible tool calling when enabled via ENV,\n * falling back to the UnTooled prompt-based approach otherwise.\n */\nclass LocalAiProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = null } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: process.env.LOCAL_AI_BASE_PATH,\n      apiKey: process.env.LOCAL_AI_API_KEY ?? null,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n    this._supportsToolCalling = null;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Since LocalAI does not expose model capabilities via API, we check\n   * the PROVIDER_SUPPORTS_NATIVE_TOOL_CALLING ENV flag for \"localai\".\n   * @returns {boolean}\n   */\n  supportsNativeToolCalling() {\n    if (this._supportsToolCalling !== null) return this._supportsToolCalling;\n    const supportsToolCalling = this.supportsNativeToolCallingViaEnv(\"localai\");\n    if (supportsToolCalling)\n      this.providerLog(\n        \"LocalAI supports native tool calling is ENABLED via ENV.\"\n      );\n    else\n      this.providerLog(\n        \"LocalAI supports native tool calling is DISABLED via ENV. Will use UnTooled instead.\"\n      );\n    this._supportsToolCalling = supportsToolCalling;\n    return supportsToolCalling;\n  }\n\n  // ---- UnTooled callbacks (used when native tool calling is not supported) ----\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"LocalAI chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"LocalAI chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  /**\n   * Stream a chat completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to UnTooled.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.stream.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallStream.bind(this),\n        eventHandler\n      );\n    }\n\n    this.providerLog(\n      \"Provider.stream (tooled) - will process this chat completion.\"\n    );\n\n    try {\n      return await tooledStream(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        eventHandler,\n        { provider: this }\n      );\n    } catch (error) {\n      console.error(error.message, error);\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Create a non-streaming completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to UnTooled.\n   */\n  async complete(messages, functions = []) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.complete.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallChat.bind(this)\n      );\n    }\n\n    try {\n      const result = await tooledComplete(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        this.getCost.bind(this),\n        { provider: this }\n      );\n\n      if (result.retryWithError) {\n        return this.complete([...messages, result.retryWithError], functions);\n      }\n\n      return result;\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   * Stubbed since LocalAI has no cost basis.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = LocalAiProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/mistral.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\n/**\n * The agent provider for the Mistral provider.\n * Mistral limits what models can call tools and even when using those\n * the model names change and dont match docs. When you do have the right model\n * it still fails and is not truly OpenAI compatible so its easier to just wrap\n * this with Untooled which 100% works since its just text & works far more reliably\n */\nclass MistralProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    super();\n    const { model = \"mistral-medium\" } = config;\n    const client = new OpenAI({\n      baseURL: \"https://api.mistral.ai/v1\",\n      apiKey: process.env.MISTRAL_API_KEY,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"LMStudio chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"LMStudio chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = MistralProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/moonshotAi.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\nclass MoonshotAiProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = \"moonshot-v1-32k\" } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: \"https://api.moonshot.ai/v1\",\n      apiKey: process.env.MOONSHOT_AI_API_KEY,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  /**\n   * Create a completion based on the received messages.\n   *\n   * @param messages A list of messages to send to the API.\n   * @param functions\n   * @returns The completion.\n   */\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"Moonshot chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"Moonshot chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = MoonshotAiProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/novita.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { tooledStream, tooledComplete } = require(\"./helpers/tooled.js\");\nconst { RetryError } = require(\"../error.js\");\nconst { NovitaLLM } = require(\"../../../AiProviders/novita/index.js\");\n\n/**\n * The agent provider for the Novita AI provider.\n * Supports true OpenAI-compatible tool calling when the model supports it,\n * falling back to the UnTooled prompt-based approach otherwise.\n */\nclass NovitaProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = \"deepseek/deepseek-r1\" } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: \"https://api.novita.ai/v3/openai\",\n      apiKey: process.env.NOVITA_LLM_API_KEY,\n      maxRetries: 3,\n      defaultHeaders: {\n        \"HTTP-Referer\": \"https://anythingllm.com\",\n        \"X-Novita-Source\": \"anythingllm\",\n      },\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n    this._supportsToolCalling = null;\n  }\n\n  /**\n   * Get the Novita client.\n   * @returns {import(\"openai\").OpenAI}\n   */\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether the loaded model supports native OpenAI-compatible tool calling.\n   * Checks the Novita model capabilities and caches the result.\n   * @returns {Promise<boolean>}\n   */\n  async supportsNativeToolCalling() {\n    if (this._supportsToolCalling !== null) return this._supportsToolCalling;\n    const novita = new NovitaLLM(null, this.model);\n    const capabilities = await novita.getModelCapabilities();\n    this._supportsToolCalling = capabilities.tools === true;\n    return this._supportsToolCalling;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"Novita chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"Novita chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  /**\n   * Stream a chat completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to UnTooled.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.stream.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallStream.bind(this),\n        eventHandler\n      );\n    }\n\n    this.providerLog(\n      \"Provider.stream (tooled) - will process this chat completion.\"\n    );\n\n    try {\n      return await tooledStream(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        eventHandler,\n        { provider: this }\n      );\n    } catch (error) {\n      console.error(error.message, error);\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Create a non-streaming completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to UnTooled.\n   */\n  async complete(messages, functions = []) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.complete.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallChat.bind(this)\n      );\n    }\n\n    try {\n      const result = await tooledComplete(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        this.getCost.bind(this),\n        { provider: this }\n      );\n\n      if (result.retryWithError) {\n        return this.complete([...messages, result.retryWithError], functions);\n      }\n\n      return result;\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   * Stubbed since Novita AI has no cost basis.\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = NovitaProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/nvidiaNim.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { parseNvidiaNimBasePath } = require(\"../../../AiProviders/nvidiaNim\");\n\n/**\n * The agent provider for the Nvidia NIM provider.\n * We wrap Nvidia NIM in UnTooled because its tool-calling may not be supported for specific models and this normalizes that.\n */\nclass NvidiaNimProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: parseNvidiaNimBasePath(process.env.NVIDIA_NIM_LLM_BASE_PATH),\n      apiKey: null,\n      maxRetries: 0,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"NVIDIA NIM chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"NVIDIA NIM chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = NvidiaNimProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/ollama.js",
    "content": "const Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { formatFunctionsToTools } = require(\"./helpers/tooled.js\");\nconst { OllamaAILLM } = require(\"../../../AiProviders/ollama\");\nconst { Ollama } = require(\"ollama\");\nconst { v4 } = require(\"uuid\");\nconst { safeJsonParse } = require(\"../../../http\");\n\n/**\n * The agent provider for the Ollama provider.\n * Supports true OpenAI-compatible tool calling when the model supports it,\n * falling back to the UnTooled prompt-based approach otherwise.\n */\nclass OllamaProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const {\n      // options = {},\n      model = null,\n    } = config;\n\n    super();\n    const authToken = process.env.OLLAMA_AUTH_TOKEN;\n    const basePath = process.env.OLLAMA_BASE_PATH;\n    const headers = authToken ? { Authorization: `Bearer ${authToken}` } : {};\n    this._client = new Ollama({\n      host: basePath,\n      headers: headers,\n      fetch: OllamaAILLM.applyOllamaFetch(),\n    });\n    this.model = model;\n    this.verbose = true;\n    this._supportsToolCalling = null;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  async supportsNativeToolCalling() {\n    if (this._supportsToolCalling !== null) return this._supportsToolCalling;\n    const ollama = new OllamaAILLM(null, this.model);\n    const capabilities = await ollama.getModelCapabilities();\n    this._supportsToolCalling = capabilities.tools === true;\n    return this._supportsToolCalling;\n  }\n\n  get queryOptions() {\n    this.providerLog(\n      `${this.model} is using a max context window of ${OllamaAILLM.promptWindowLimit(this.model)}/${OllamaAILLM.maxContextWindow(this.model)} tokens.`\n    );\n    return {\n      num_ctx: OllamaAILLM.promptWindowLimit(this.model),\n    };\n  }\n\n  /**\n   * Handle a chat completion with tool calling\n   *\n   * @param messages\n   * @returns {Promise<string|null>} The completion.\n   */\n  async #handleFunctionCallChat({ messages = [] }) {\n    await OllamaAILLM.cacheContextWindows();\n    const response = await this.client.chat({\n      model: this.model,\n      messages,\n      options: this.queryOptions,\n    });\n    return response?.message?.content || null;\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    await OllamaAILLM.cacheContextWindows();\n    return await this.client.chat({\n      model: this.model,\n      messages,\n      stream: true,\n      options: this.queryOptions,\n    });\n  }\n\n  /**\n   * Parse a data URL into base64 data for Ollama images\n   * @param {string} dataUrl - Data URL like \"data:image/jpeg;base64,/9j/...\"\n   * @returns {string|null} Base64 encoded image data\n   */\n  #parseImageDataUrl(dataUrl) {\n    if (!dataUrl || !dataUrl.startsWith(\"data:\")) return null;\n    const matches = dataUrl.match(/^data:[^;]+;base64,(.+)$/);\n    if (!matches) return null;\n    return matches[1];\n  }\n\n  /**\n   * Override formatMessageWithAttachments for Ollama's specific format.\n   * Ollama expects images in a separate 'images' array with base64 data (no data URI prefix),\n   * not the OpenAI-style content array format.\n   * **This is only used for Ollama:untooled fallback mode.**\n   * @param {Object} message - Message with potential attachments\n   * @returns {Object} Formatted message for Ollama\n   */\n  formatMessageWithAttachments(message) {\n    if (!message.attachments || message.attachments.length === 0) {\n      return message;\n    }\n\n    const images = [];\n    for (const attachment of message.attachments) {\n      const imageData = this.#parseImageDataUrl(attachment.contentString);\n      if (imageData) {\n        images.push(imageData);\n      }\n    }\n\n    const { attachments: _, ...restOfMessage } = message;\n    return {\n      ...restOfMessage,\n      ...(images.length > 0 ? { images } : {}),\n    };\n  }\n\n  /**\n   * Convert aibitat's internal message history (which uses role:\"function\" with\n   * originalFunctionCall metadata) into the Ollama tool-calling message format\n   * (assistant tool_calls + role:\"tool\" result pairs).\n   * Handles image attachments for vision/multimodal support.\n   * @param {Array} messages\n   * @returns {Array}\n   */\n  #formatMessagesForOllamaTools(messages) {\n    const formatted = [];\n    for (const message of messages) {\n      if (message.role === \"function\") {\n        const funcName =\n          message.originalFunctionCall?.name || message.name || \"unknown\";\n        const funcArgs = message.originalFunctionCall?.arguments || {};\n        formatted.push({\n          role: \"assistant\",\n          content: \"\",\n          tool_calls: [\n            {\n              function: {\n                name: funcName,\n                arguments:\n                  typeof funcArgs === \"string\"\n                    ? safeJsonParse(funcArgs, {})\n                    : funcArgs,\n              },\n            },\n          ],\n        });\n        formatted.push({\n          role: \"tool\",\n          content:\n            typeof message.content === \"string\"\n              ? message.content\n              : JSON.stringify(message.content),\n        });\n      } else {\n        // Handle messages with attachments (images) for multimodal support\n        if (message.attachments && message.attachments.length > 0) {\n          const images = [];\n          for (const attachment of message.attachments) {\n            const imageData = this.#parseImageDataUrl(attachment.contentString);\n            if (imageData) images.push(imageData);\n          }\n          const { attachments: _, ...restOfMessage } = message;\n          formatted.push({\n            ...restOfMessage,\n            ...(images.length > 0 ? { images } : {}),\n          });\n        } else {\n          formatted.push(message);\n        }\n      }\n    }\n    return formatted;\n  }\n\n  async streamingFunctionCall(\n    messages,\n    functions,\n    chatCb = null,\n    eventHandler = null\n  ) {\n    const history = [...messages].filter((msg) =>\n      [\"user\", \"assistant\"].includes(msg.role)\n    );\n    if (history[history.length - 1].role !== \"user\") return null;\n\n    const msgUUID = v4();\n    let token = \"\";\n    let textResponse = \"\";\n    let reasoningText = \"\";\n    const historyMessages = this.buildToolCallMessages(history, functions);\n    const stream = await chatCb({ messages: historyMessages });\n\n    eventHandler?.(\"reportStreamEvent\", {\n      type: \"statusResponse\",\n      uuid: v4(),\n      content: \"Agent is thinking...\",\n    });\n\n    for await (const chunk of stream) {\n      if (!chunk.hasOwnProperty(\"message\")) continue;\n\n      const content = chunk.message?.content;\n      const reasoningToken = chunk.message?.thinking;\n      if (reasoningToken) {\n        if (reasoningText.length === 0) {\n          reasoningText = `Thinking:\\n\\n${reasoningToken}`;\n          token = reasoningText;\n        } else {\n          reasoningText += reasoningToken;\n          token = reasoningToken;\n        }\n      } else if (content.length > 0) {\n        if (reasoningText.length > 0) {\n          token = `\\n\\nDone thinking.\\n\\n${content}`;\n          reasoningText = \"\";\n        } else {\n          token = content;\n        }\n        textResponse += content;\n      }\n\n      eventHandler?.(\"reportStreamEvent\", {\n        type: \"statusResponse\",\n        uuid: msgUUID,\n        content: token,\n      });\n    }\n\n    const call = safeJsonParse(textResponse, null);\n    if (call === null)\n      return { toolCall: null, text: textResponse, uuid: msgUUID }; // failed to parse, so must be regular text response.\n\n    const { valid, reason } = this.validFuncCall(call, functions);\n    if (!valid) {\n      this.providerLog(`Invalid function tool call: ${reason}.`);\n      eventHandler?.(\"reportStreamEvent\", {\n        type: \"removeStatusResponse\",\n        uuid: msgUUID,\n        content:\n          \"The model attempted to make an invalid function call - it was ignored.\",\n      });\n      return { toolCall: null, text: null, uuid: msgUUID };\n    }\n\n    const { isDuplicate, reason: duplicateReason } =\n      this.deduplicator.isDuplicate(call.name, call.arguments);\n    if (isDuplicate) {\n      this.providerLog(\n        `Cannot call ${call.name} again because ${duplicateReason}.`\n      );\n      eventHandler?.(\"reportStreamEvent\", {\n        type: \"removeStatusResponse\",\n        uuid: msgUUID,\n        content:\n          \"The model tried to call a function with the same arguments as a previous call - it was ignored.\",\n      });\n      return { toolCall: null, text: null, uuid: msgUUID };\n    }\n\n    eventHandler?.(\"reportStreamEvent\", {\n      uuid: `${msgUUID}:tool_call_invocation`,\n      type: \"toolCallInvocation\",\n      content: `Parsed Tool Call: ${call.name}(${JSON.stringify(call.arguments)})`,\n    });\n    return { toolCall: call, text: null, uuid: msgUUID };\n  }\n\n  /**\n   * Stream a chat completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to the\n   * Ollama SDK + UnTooled prompt-based approach.\n   *\n   * @param messages A list of messages to send to the API.\n   * @param functions\n   * @param eventHandler\n   * @returns The completion.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (useNative) {\n      this.providerLog(\n        \"OllamaProvider.stream (tooled) - will process this chat completion.\"\n      );\n      this.resetUsage();\n      await OllamaAILLM.cacheContextWindows();\n      const msgUUID = v4();\n      const formattedMessages = this.#formatMessagesForOllamaTools(messages);\n      const tools = formatFunctionsToTools(functions);\n\n      const stream = await this.client.chat({\n        model: this.model,\n        messages: formattedMessages,\n        tools,\n        stream: true,\n        options: this.queryOptions,\n      });\n\n      let textResponse = \"\";\n      let toolCalls = null;\n\n      for await (const chunk of stream) {\n        // Capture usage from final chunk (Ollama sends usage when done=true)\n        if (chunk.done === true) {\n          this.recordUsage({\n            prompt_tokens: chunk.prompt_eval_count || 0,\n            completion_tokens: chunk.eval_count || 0,\n          });\n        }\n\n        if (!chunk?.message) continue;\n\n        if (chunk.message.content) {\n          textResponse += chunk.message.content;\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"textResponseChunk\",\n            uuid: msgUUID,\n            content: chunk.message.content,\n          });\n        }\n\n        if (chunk.message.tool_calls?.length > 0) {\n          toolCalls = chunk.message.tool_calls;\n          eventHandler?.(\"reportStreamEvent\", {\n            uuid: `${msgUUID}:tool_call_invocation`,\n            type: \"toolCallInvocation\",\n            content: `Tool Call: ${toolCalls[0].function.name}(${JSON.stringify(toolCalls[0].function.arguments)})`,\n          });\n        }\n      }\n\n      if (toolCalls && toolCalls.length > 0) {\n        const toolCall = toolCalls[0];\n        const args =\n          typeof toolCall.function.arguments === \"string\"\n            ? safeJsonParse(toolCall.function.arguments, {})\n            : toolCall.function.arguments || {};\n\n        return {\n          textResponse,\n          functionCall: {\n            id: `ollama_${v4()}`,\n            name: toolCall.function.name,\n            arguments: args,\n          },\n          cost: 0,\n          uuid: msgUUID,\n        };\n      }\n\n      return { textResponse, functionCall: null, cost: 0, uuid: msgUUID };\n    }\n\n    // Fallback: UnTooled prompt-based approach via the native Ollama SDK\n    this.providerLog(\n      \"OllamaProvider.stream - will process this chat completion.\"\n    );\n    // eslint-disable-next-line\n    try {\n      let completion = { content: \"\" };\n      if (functions.length > 0) {\n        const {\n          toolCall,\n          text,\n          uuid: msgUUID,\n        } = await this.streamingFunctionCall(\n          messages,\n          functions,\n          this.#handleFunctionCallStream.bind(this),\n          eventHandler\n        );\n\n        if (toolCall !== null) {\n          this.providerLog(`Valid tool call found - running ${toolCall.name}.`);\n          this.deduplicator.trackRun(toolCall.name, toolCall.arguments, {\n            cooldown: this.isMCPTool(toolCall, functions),\n          });\n          return {\n            result: null,\n            functionCall: {\n              name: toolCall.name,\n              arguments: toolCall.arguments,\n            },\n            cost: 0,\n          };\n        }\n\n        if (text) {\n          this.providerLog(\n            `No tool call found in the response - will send as a full text response.`\n          );\n          completion.content = text;\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"removeStatusResponse\",\n            uuid: msgUUID,\n            content: \"No tool call found in the response\",\n          });\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"statusResponse\",\n            uuid: v4(),\n            content: \"Done thinking.\",\n          });\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"fullTextResponse\",\n            uuid: v4(),\n            content: text,\n          });\n        }\n      }\n\n      if (!completion?.content) {\n        eventHandler?.(\"reportStreamEvent\", {\n          type: \"statusResponse\",\n          uuid: v4(),\n          content: \"Done thinking.\",\n        });\n        this.providerLog(\n          \"Will assume chat completion without tool call inputs.\"\n        );\n        const msgUUID = v4();\n        completion = { content: \"\" };\n        let reasoningText = \"\";\n        let token = \"\";\n        const stream = await this.#handleFunctionCallStream({\n          messages: this.cleanMsgs(messages),\n        });\n\n        for await (const chunk of stream) {\n          if (!chunk.hasOwnProperty(\"message\")) continue;\n\n          const content = chunk.message?.content;\n          const reasoningToken = chunk.message?.thinking;\n          if (reasoningToken) {\n            if (reasoningText.length === 0) {\n              reasoningText = `<think>${reasoningToken}`;\n              token = `<think>${reasoningToken}`;\n            } else {\n              reasoningText += reasoningToken;\n              token = reasoningToken;\n            }\n          } else if (content.length > 0) {\n            if (reasoningText.length > 0) {\n              token = `</think>${content}`;\n              reasoningText = \"\";\n            } else {\n              token = content;\n            }\n          }\n\n          completion.content += token;\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"textResponseChunk\",\n            uuid: msgUUID,\n            content: token,\n          });\n        }\n      }\n\n      this.deduplicator.reset(\"runs\");\n      return {\n        textResponse: completion.content,\n        cost: 0,\n      };\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  /**\n   * Create a non-streaming completion with tool calling support.\n   * Uses native tool calling when supported, otherwise falls back to UnTooled.\n   *\n   * @param messages A list of messages to send to the API.\n   * @param functions\n   * @returns The completion.\n   */\n  async complete(messages, functions = []) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (useNative) {\n      this.resetUsage();\n      await OllamaAILLM.cacheContextWindows();\n      const formattedMessages = this.#formatMessagesForOllamaTools(messages);\n      const tools = formatFunctionsToTools(functions);\n\n      const response = await this.client.chat({\n        model: this.model,\n        messages: formattedMessages,\n        tools,\n        options: this.queryOptions,\n      });\n\n      // Record usage (Ollama uses prompt_eval_count/eval_count)\n      this.recordUsage({\n        prompt_tokens: response.prompt_eval_count || 0,\n        completion_tokens: response.eval_count || 0,\n      });\n\n      if (response.message?.tool_calls?.length > 0) {\n        const toolCall = response.message.tool_calls[0];\n        const args =\n          typeof toolCall.function.arguments === \"string\"\n            ? safeJsonParse(toolCall.function.arguments, {})\n            : toolCall.function.arguments || {};\n\n        return {\n          textResponse: null,\n          functionCall: {\n            id: `ollama_${v4()}`,\n            name: toolCall.function.name,\n            arguments: args,\n          },\n          cost: 0,\n          usage: this.getUsage(),\n        };\n      }\n\n      return {\n        textResponse: response.message?.content || null,\n        cost: 0,\n        usage: this.getUsage(),\n      };\n    }\n\n    // Fallback: UnTooled prompt-based approach via the native Ollama SDK\n    this.providerLog(\n      \"OllamaProvider.complete - will process this chat completion.\"\n    );\n    // eslint-disable-next-line\n    try {\n      let completion = { content: \"\" };\n      if (functions.length > 0) {\n        const { toolCall, text } = await this.functionCall(\n          messages,\n          functions,\n          this.#handleFunctionCallChat.bind(this)\n        );\n\n        if (toolCall !== null) {\n          this.providerLog(`Valid tool call found - running ${toolCall.name}.`);\n          this.deduplicator.trackRun(toolCall.name, toolCall.arguments, {\n            cooldown: this.isMCPTool(toolCall, functions),\n          });\n          return {\n            result: null,\n            functionCall: {\n              name: toolCall.name,\n              arguments: toolCall.arguments,\n            },\n            cost: 0,\n          };\n        }\n        completion.content = text;\n      }\n\n      if (!completion?.content) {\n        this.providerLog(\n          \"Will assume chat completion without tool call inputs.\"\n        );\n        const textResponse = await this.#handleFunctionCallChat({\n          messages: this.cleanMsgs(messages),\n        });\n        completion.content = textResponse;\n      }\n\n      this.deduplicator.reset(\"runs\");\n      return {\n        textResponse: completion.content,\n        cost: 0,\n      };\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   * Stubbed since Ollama has no cost basis.\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = OllamaProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/openai.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst { RetryError } = require(\"../error.js\");\nconst { v4 } = require(\"uuid\");\nconst { safeJsonParse } = require(\"../../../http\");\n\n/**\n * The agent provider for the OpenAI API.\n * By default, the model is set to 'gpt-3.5-turbo'.\n */\nclass OpenAIProvider extends Provider {\n  model;\n  constructor(config = {}) {\n    const {\n      options = {\n        apiKey: process.env.OPEN_AI_KEY,\n        maxRetries: 3,\n      },\n      model = \"gpt-4o\",\n    } = config;\n\n    const client = new OpenAI(options);\n\n    super(client);\n\n    this.model = model;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * - OpenAI always supports tool calling.\n   * @returns {Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return true;\n  }\n\n  /**\n   * Format the messages to the OpenAI API Responses format.\n   * - If the message is our internal `function` type, then we need to map it to a function call + output format\n   * - Otherwise, map it to the input text format for user, system, and assistant messages\n   * - Handles attachments (images) for multimodal support\n   *\n   * @param {any[]} messages - The messages to format.\n   * @returns {OpenAI.OpenAI.Responses.ResponseInput[]} The formatted messages.\n   */\n  #formatToResponsesInput(messages) {\n    let formattedMessages = [];\n    messages.forEach((message) => {\n      if (message.role === \"function\") {\n        // If the message does not have an originalFunctionCall we cannot\n        // map it to a function call id and OpenAI will throw an error.\n        // so if this does not carry over - log and skip\n        if (!message.hasOwnProperty(\"originalFunctionCall\")) {\n          this.providerLog(\n            \"[OpenAI.#formatToResponsesInput]: message did not pass back the originalFunctionCall. We need this to map the function call to the correct id.\",\n            { message: JSON.stringify(message, null, 2) }\n          );\n          return;\n        }\n\n        formattedMessages.push(\n          {\n            type: \"function_call\",\n            name: message.originalFunctionCall.name,\n            call_id: message.originalFunctionCall.id,\n            arguments: JSON.stringify(message.originalFunctionCall.arguments),\n          },\n          {\n            type: \"function_call_output\",\n            call_id: message.originalFunctionCall.id,\n            output: message.content,\n          }\n        );\n        return;\n      }\n\n      // Build content array with text and optional image attachments\n      const content = [\n        {\n          type: message.role === \"assistant\" ? \"output_text\" : \"input_text\",\n          text: message.content,\n        },\n      ];\n\n      // Add image attachments if present (for multimodal/vision support)\n      if (message.attachments && message.attachments.length > 0) {\n        for (const attachment of message.attachments) {\n          content.push({\n            type: \"input_image\",\n            image_url: attachment.contentString,\n          });\n        }\n      }\n\n      formattedMessages.push({\n        role: message.role,\n        content,\n      });\n    });\n\n    return formattedMessages;\n  }\n\n  /**\n   * Format the functions to the OpenAI API Responses format.\n   *\n   * @param {any[]} functions - The functions to format.\n   * @returns {{\n   *   type: \"function\",\n   *   name: string,\n   *   description: string,\n   *   parameters: object,\n   *   strict: boolean,\n   * }[]} The formatted functions.\n   */\n  #formatFunctions(functions) {\n    return functions.map((func) => ({\n      type: \"function\",\n      name: func.name,\n      description: func.description,\n      parameters: func.parameters,\n      strict: false,\n    }));\n  }\n\n  /**\n   * Stream a chat completion from the LLM with tool calling\n   * Note: This using the OpenAI API Responses SDK and its implementation is specific to OpenAI models.\n   * Do not re-use this code for providers that do not EXACTLY implement the OpenAI API Responses SDK.\n   *\n   * @param {any[]} messages - The messages to send to the LLM.\n   * @param {any[]} functions - The functions to use in the LLM.\n   * @param {function} eventHandler - The event handler to use to report stream events.\n   * @returns {Promise<{ functionCall: any, textResponse: string }>} - The result of the chat completion.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    this.providerLog(\"OpenAI.stream - will process this chat completion.\");\n    this.resetUsage();\n\n    try {\n      const msgUUID = v4();\n\n      /** @type {OpenAI.OpenAI.Responses.Response} */\n      const response = await this.client.responses.create({\n        model: this.model,\n        input: this.#formatToResponsesInput(messages),\n        stream: true,\n        store: false,\n        parallel_tool_calls: false,\n        ...(Array.isArray(functions) && functions?.length > 0\n          ? { tools: this.#formatFunctions(functions) }\n          : {}),\n      });\n\n      const completion = {\n        content: \"\",\n        /** @type {null|{name: string, call_id: string, arguments: string|object}} */\n        functionCall: null,\n      };\n\n      for await (const streamEvent of response) {\n        /** @type {OpenAI.OpenAI.Responses.ResponseStreamEvent} */\n        const chunk = streamEvent;\n\n        if (chunk.type === \"response.output_text.delta\") {\n          completion.content += chunk.delta;\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"textResponseChunk\",\n            uuid: msgUUID,\n            content: chunk.delta,\n          });\n          continue;\n        }\n\n        if (\n          chunk.type === \"response.output_item.added\" &&\n          chunk.item.type === \"function_call\"\n        ) {\n          completion.functionCall = {\n            name: chunk.item.name,\n            call_id: chunk.item.call_id,\n            arguments: chunk.item.arguments,\n          };\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"toolCallInvocation\",\n            uuid: `${msgUUID}:tool_call_invocation`,\n            content: `Assembling Tool Call: ${completion.functionCall.name}(${completion.functionCall.arguments})`,\n          });\n          continue;\n        }\n\n        if (chunk.type === \"response.function_call_arguments.delta\") {\n          completion.functionCall.arguments += chunk.delta;\n          eventHandler?.(\"reportStreamEvent\", {\n            type: \"toolCallInvocation\",\n            uuid: `${msgUUID}:tool_call_invocation`,\n            content: `Assembling Tool Call: ${completion.functionCall.name}(${completion.functionCall.arguments})`,\n          });\n          continue;\n        }\n\n        if (chunk.type === \"response.completed\") {\n          const completedResponse = chunk.response;\n          if (!completedResponse?.usage) continue;\n          this.recordUsage(completedResponse.usage);\n          continue;\n        }\n      }\n\n      if (completion.functionCall) {\n        completion.functionCall.arguments = safeJsonParse(\n          completion.functionCall.arguments,\n          {}\n        );\n        return {\n          textResponse: completion.content,\n          functionCall: {\n            id: completion.functionCall.call_id,\n            name: completion.functionCall.name,\n            arguments: completion.functionCall.arguments,\n          },\n          cost: this.getCost(),\n          uuid: msgUUID,\n        };\n      }\n\n      return {\n        textResponse: completion.content,\n        functionCall: null,\n        cost: this.getCost(),\n        uuid: msgUUID,\n      };\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n\n      throw error;\n    }\n  }\n\n  /**\n   * Create a completion based on the received messages.\n   *\n   * @param messages A list of messages to send to the OpenAI API.\n   * @param functions\n   * @returns The completion.\n   */\n  async complete(messages, functions = []) {\n    this.providerLog(\"OpenAI.complete - will process this chat completion.\");\n    this.resetUsage();\n\n    try {\n      const completion = {\n        content: \"\",\n        functionCall: null,\n      };\n\n      /** @type {OpenAI.OpenAI.Responses.Response} */\n      const response = await this.client.responses.create({\n        model: this.model,\n        stream: false,\n        store: false,\n        parallel_tool_calls: false,\n        input: this.#formatToResponsesInput(messages),\n        ...(Array.isArray(functions) && functions?.length > 0\n          ? { tools: this.#formatFunctions(functions) }\n          : {}),\n      });\n\n      if (response.usage) this.recordUsage(response.usage);\n      for (const outputBlock of response.output) {\n        if (outputBlock.type === \"message\") {\n          if (outputBlock.content[0]?.type === \"output_text\") {\n            completion.content = outputBlock.content[0].text;\n          }\n        }\n\n        if (outputBlock.type === \"function_call\") {\n          completion.functionCall = {\n            name: outputBlock.name,\n            call_id: outputBlock.call_id,\n            arguments: outputBlock.arguments,\n          };\n        }\n      }\n\n      if (completion.functionCall) {\n        completion.functionCall.arguments = safeJsonParse(\n          completion.functionCall.arguments,\n          {}\n        );\n        return {\n          textResponse: completion.content,\n          functionCall: {\n            id: completion.functionCall.call_id,\n            name: completion.functionCall.name,\n            arguments: completion.functionCall.arguments,\n          },\n          cost: this.getCost(),\n          usage: this.getUsage(),\n        };\n      }\n\n      return {\n        textResponse: completion.content,\n        functionCall: null,\n        cost: this.getCost(),\n        usage: this.getUsage(),\n      };\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   * @returns {number} The cost of the completion (currently returns 0).\n   */\n  getCost() {\n    return 0;\n  }\n}\n\nmodule.exports = OpenAIProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/openrouter.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { tooledStream, tooledComplete } = require(\"./helpers/tooled.js\");\nconst { RetryError } = require(\"../error.js\");\n\n/**\n * The agent provider for the OpenRouter provider.\n * Supports true OpenAI-compatible tool calling when enabled via ENV,\n * falling back to the UnTooled prompt-based approach otherwise.\n * @extends {Provider}\n * @extends {UnTooled}\n */\nclass OpenRouterProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = \"openrouter/auto\" } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: \"https://openrouter.ai/api/v1\",\n      apiKey: process.env.OPENROUTER_API_KEY,\n      maxRetries: 3,\n      defaultHeaders: {\n        \"HTTP-Referer\": \"https://anythingllm.com\",\n        \"X-Title\": \"AnythingLLM\",\n      },\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n    this._supportsToolCalling = null;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * - Since OpenRouter models vary in tool calling support, we check the ENV.\n   * - If the ENV is not set, we default to false.\n   * @returns {boolean}\n   */\n  supportsNativeToolCalling() {\n    if (this._supportsToolCalling !== null) return this._supportsToolCalling;\n    const supportsToolCalling =\n      this.supportsNativeToolCallingViaEnv(\"openrouter\");\n    if (supportsToolCalling)\n      this.providerLog(\n        \"OpenRouter supports native tool calling is ENABLED via ENV.\"\n      );\n    else\n      this.providerLog(\n        \"OpenRouter supports native tool calling is DISABLED via ENV. Will use UnTooled instead.\"\n      );\n    this._supportsToolCalling = supportsToolCalling;\n    return supportsToolCalling;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n        user: this.executingUserId,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"OpenRouter chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"OpenRouter chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n      user: this.executingUserId,\n    });\n  }\n\n  /**\n   * Stream a chat completion with tool calling support.\n   * Uses native tool calling when enabled via ENV, otherwise falls back to UnTooled.\n   */\n  async stream(messages, functions = [], eventHandler = null) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.stream.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallStream.bind(this),\n        eventHandler\n      );\n    }\n\n    this.providerLog(\n      \"Provider.stream (tooled) - will process this chat completion.\"\n    );\n\n    try {\n      return await tooledStream(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        eventHandler,\n        { provider: this }\n      );\n    } catch (error) {\n      console.error(error.message, error);\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Create a non-streaming completion with tool calling support.\n   * Uses native tool calling when enabled via ENV, otherwise falls back to UnTooled.\n   */\n  async complete(messages, functions = []) {\n    const useNative =\n      functions.length > 0 && (await this.supportsNativeToolCalling());\n\n    if (!useNative) {\n      return await UnTooled.prototype.complete.call(\n        this,\n        messages,\n        functions,\n        this.#handleFunctionCallChat.bind(this)\n      );\n    }\n\n    try {\n      const result = await tooledComplete(\n        this.client,\n        this.model,\n        messages,\n        functions,\n        this.getCost.bind(this),\n        { provider: this }\n      );\n\n      if (result.retryWithError) {\n        return this.complete([...messages, result.retryWithError], functions);\n      }\n\n      return result;\n    } catch (error) {\n      if (error instanceof OpenAI.AuthenticationError) throw error;\n      if (\n        error instanceof OpenAI.RateLimitError ||\n        error instanceof OpenAI.InternalServerError ||\n        error instanceof OpenAI.APIError\n      ) {\n        throw new RetryError(error.message);\n      }\n      throw error;\n    }\n  }\n\n  /**\n   * Get the cost of the completion.\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = OpenRouterProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/perplexity.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\n/**\n * The agent provider for the Perplexity provider.\n */\nclass PerplexityProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    super();\n    const { model = \"sonar-small-online\" } = config;\n    const client = new OpenAI({\n      baseURL: \"https://api.perplexity.ai\",\n      apiKey: process.env.PERPLEXITY_API_KEY ?? null,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"Perplexity chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"Perplexity chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = PerplexityProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/ppio.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\n/**\n * The agent provider for the PPIO AI provider.\n */\nclass PPIOProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = \"qwen/qwen2.5-32b-instruct\" } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: \"https://api.ppinfra.com/v3/openai\",\n      apiKey: process.env.PPIO_API_KEY,\n      maxRetries: 3,\n      defaultHeaders: {\n        \"HTTP-Referer\": \"https://anythingllm.com\",\n        \"X-API-Source\": \"anythingllm\",\n      },\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return false;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!Object.prototype.hasOwnProperty.call(result, \"choices\"))\n          throw new Error(\"PPIO chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"PPIO chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   * Stubbed since PPIO has no cost basis.\n   */\n  getCost() {\n    return 0;\n  }\n}\n\nmodule.exports = PPIOProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/privatemode.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\nconst { PrivatemodeLLM } = require(\"../../../AiProviders/privatemode/index.js\");\n\n/**\n * The agent provider for the Privatemodel provider.\n * @extends {Provider}\n * @extends {UnTooled}\n */\nclass PrivatemodelProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = process.env.PRIVATEMODE_LLM_MODEL_PREF } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: PrivatemodeLLM.parseBasePath(\n        process.env.PRIVATEMODE_LLM_BASE_PATH\n      ),\n      apiKey: null,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n        user: this.executingUserId,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"Privatemodel chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"Privatemodel chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n      user: this.executingUserId,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   * Stubbed since Privatemodel has no cost basis.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = PrivatemodelProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/sambanova.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\nclass SambaNovaProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = \"Meta-Llama-3.1-8B-Instruct\" } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: \"https://api.sambanova.ai/v1\",\n      apiKey: process.env.SAMBANOVA_LLM_API_KEY,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  /**\n   * Create a completion based on the received messages.\n   *\n   * @param messages A list of messages to send to the API.\n   * @param functions\n   * @returns The completion.\n   */\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"SambaNova chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"SambaNova chat: No results length!\");\n        return result.choices[0].message.content;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = SambaNovaProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/textgenwebui.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\n/**\n * The agent provider for the Oobabooga provider.\n */\nclass TextWebGenUiProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(_config = {}) {\n    super();\n    const client = new OpenAI({\n      baseURL: process.env.TEXT_GEN_WEB_UI_BASE_PATH,\n      apiKey: process.env.TEXT_GEN_WEB_UI_API_KEY ?? null,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = \"text-generation-webui\"; // text-web-gen-ui does not have a model pref, but we need a placeholder\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"Oobabooga chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"Oobabooga chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   * Stubbed since KoboldCPP has no cost basis.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = TextWebGenUiProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/togetherai.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\n/**\n * The agent provider for the TogetherAI provider.\n */\nclass TogetherAIProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = \"mistralai/Mistral-7B-Instruct-v0.1\" } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: \"https://api.together.xyz/v1\",\n      apiKey: process.env.TOGETHER_AI_API_KEY,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"LMStudio chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"LMStudio chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   * Stubbed since LMStudio has no cost basis.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = TogetherAIProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/xai.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\n/**\n * The agent provider for the xAI provider.\n */\nclass XAIProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = \"grok-beta\" } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: \"https://api.x.ai/v1\",\n      apiKey: process.env.XAI_LLM_API_KEY,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"xAI chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"xAI chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  /**\n   * Get the cost of the completion.\n   *\n   * @param _usage The completion to get the cost for.\n   * @returns The cost of the completion.\n   */\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = XAIProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/providers/zai.js",
    "content": "const OpenAI = require(\"openai\");\nconst Provider = require(\"./ai-provider.js\");\nconst InheritMultiple = require(\"./helpers/classes.js\");\nconst UnTooled = require(\"./helpers/untooled.js\");\n\nclass ZAIProvider extends InheritMultiple([Provider, UnTooled]) {\n  model;\n\n  constructor(config = {}) {\n    const { model = \"glm-4.5\" } = config;\n    super();\n    const client = new OpenAI({\n      baseURL: \"https://api.z.ai/api/paas/v4\",\n      apiKey: process.env.ZAI_API_KEY,\n      maxRetries: 3,\n    });\n\n    this._client = client;\n    this.model = model;\n    this.verbose = true;\n  }\n\n  /**\n   * Create a completion based on the received messages.\n   *\n   * @param messages A list of messages to send to the API.\n   * @param functions\n   * @returns The completion.\n   */\n  get client() {\n    return this._client;\n  }\n\n  get supportsAgentStreaming() {\n    return true;\n  }\n\n  /**\n   * Whether this provider supports native OpenAI-compatible tool calling.\n   * Override in subclass and return true to use native tool calling instead of UnTooled.\n   * @returns {boolean|Promise<boolean>}\n   */\n  supportsNativeToolCalling() {\n    return false;\n  }\n\n  async #handleFunctionCallChat({ messages = [] }) {\n    return await this.client.chat.completions\n      .create({\n        model: this.model,\n        messages,\n      })\n      .then((result) => {\n        if (!result.hasOwnProperty(\"choices\"))\n          throw new Error(\"Z.AI chat: No results!\");\n        if (result.choices.length === 0)\n          throw new Error(\"Z.AI chat: No results length!\");\n        return result.choices[0].message.content;\n      })\n      .catch((_) => {\n        return null;\n      });\n  }\n\n  async #handleFunctionCallStream({ messages = [] }) {\n    return await this.client.chat.completions.create({\n      model: this.model,\n      stream: true,\n      messages,\n    });\n  }\n\n  async stream(messages, functions = [], eventHandler = null) {\n    return await UnTooled.prototype.stream.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallStream.bind(this),\n      eventHandler\n    );\n  }\n\n  async complete(messages, functions = []) {\n    return await UnTooled.prototype.complete.call(\n      this,\n      messages,\n      functions,\n      this.#handleFunctionCallChat.bind(this)\n    );\n  }\n\n  getCost(_usage) {\n    return 0;\n  }\n}\n\nmodule.exports = ZAIProvider;\n"
  },
  {
    "path": "server/utils/agents/aibitat/utils/dedupe.js",
    "content": "// Some models may attempt to call an expensive or annoying function many times and in that case we will want\n// to implement some stateful tracking during that agent session. GPT4 and other more powerful models are smart\n// enough to realize this, but models like 3.5 lack this. Open source models suffer greatly from this issue.\n// eg: \"save something to file...\"\n//  agent -> saves\n//  agent -> saves\n//  agent -> saves\n//  agent -> saves\n// ... do random # of times.\n// We want to block all the reruns of a plugin, so we can add this to prevent that behavior from\n// spamming the user (or other costly function) that have the exact same signatures.\n\n// Track Run/isDuplicate prevents _exact_ data re-runs based on the SHA of their inputs\n// StartCooldown/isOnCooldown does prevention of _near-duplicate_ runs based on only the function name that is running.\n// isMarkedUnique/markUnique/removeUniqueConstraint prevents one-time functions from re-running. EG: charting.\nconst crypto = require(\"crypto\");\nconst DEFAULT_COOLDOWN_MS = 30 * 1000;\n\nclass Deduplicator {\n  #hashes = {};\n  #cooldowns = {};\n  #uniques = {};\n  constructor() {}\n\n  log(message, ...args) {\n    console.log(`\\x1b[36m[Deduplicator]\\x1b[0m ${message}`, ...args);\n  }\n\n  trackRun(\n    key,\n    params = {},\n    options = {\n      cooldown: false,\n      cooldownInMs: DEFAULT_COOLDOWN_MS,\n      markUnique: false,\n    }\n  ) {\n    const hash = crypto\n      .createHash(\"sha256\")\n      .update(JSON.stringify({ key, params }))\n      .digest(\"hex\");\n    this.#hashes[hash] = Number(new Date());\n    if (options.cooldown)\n      this.startCooldown(key, { cooldownInMs: options.cooldownInMs });\n    if (options.markUnique) this.markUnique(key);\n  }\n\n  /**\n   * Checks if a key and params are:\n   * - exactly the same as a previous run.\n   * - on cooldown.\n   * - marked as unique.\n   * @param {string} key - The key to check.\n   * @param {Object} params - The parameters to check.\n   * @returns {{isDuplicate: boolean, reason: string}} - The result of the check.\n   */\n  isDuplicate(key, params = {}) {\n    const newSig = crypto\n      .createHash(\"sha256\")\n      .update(JSON.stringify({ key, params }))\n      .digest(\"hex\");\n    if (this.#hashes.hasOwnProperty(newSig))\n      return {\n        isDuplicate: true,\n        reason: `an exact duplicate of previous run of ${key}`,\n      };\n    if (this.isOnCooldown(key))\n      return {\n        isDuplicate: true,\n        reason: `the function is on cooldown for ${key}.`,\n      };\n    if (this.isMarkedUnique(key))\n      return {\n        isDuplicate: true,\n        reason: `the function is marked as unique for ${key}. Can only be called once per agent session.`,\n      };\n    return { isDuplicate: false, reason: \"\" };\n  }\n\n  /**\n   * Resets the object property for this instance of the Deduplicator class\n   * @param {('runs'|'cooldowns'|'uniques')} type - The type of prop to reset\n   */\n  reset(type = \"runs\") {\n    switch (type) {\n      case \"runs\":\n        this.#hashes = {};\n        break;\n      case \"cooldowns\":\n        this.#cooldowns = {};\n        break;\n      case \"uniques\":\n        this.#uniques = {};\n        break;\n    }\n    return;\n  }\n\n  /**\n   * Starts a cooldown for a key.\n   * @param {string} key - The key to start the cooldown for (string key of the function name).\n   * @param {Object} parameters - The parameters for the cooldown.\n   * @param {number} parameters.cooldownInMs - The cooldown in milliseconds.\n   */\n  startCooldown(\n    key,\n    parameters = {\n      cooldownInMs: DEFAULT_COOLDOWN_MS,\n    }\n  ) {\n    const cooldownDelay = parameters.cooldownInMs || DEFAULT_COOLDOWN_MS;\n    this.log(`Starting cooldown for ${key} for ${cooldownDelay}ms`);\n    this.#cooldowns[key] = Number(new Date()) + Number(cooldownDelay);\n  }\n\n  /**\n   * Checks if a key is on cooldown.\n   * @param {string} key - The key to check.\n   * @returns {boolean} - True if the key is on cooldown, false otherwise.\n   */\n  isOnCooldown(key) {\n    if (!this.#cooldowns.hasOwnProperty(key)) return false;\n    return Number(new Date()) <= this.#cooldowns[key];\n  }\n\n  /**\n   * Checks if a key is marked as unique and currently tracked by the deduplicator.\n   * @param {string} key - The key to check.\n   * @returns {boolean} - True if the key is marked as unique, false otherwise.\n   */\n  isMarkedUnique(key) {\n    return this.#uniques.hasOwnProperty(key);\n  }\n\n  /**\n   * Removes the unique constraint for a key.\n   * @param {string} key - The key to remove the unique constraint for.\n   */\n  removeUniqueConstraint(key) {\n    delete this.#uniques[key];\n  }\n\n  /**\n   * Marks a key as unique and currently tracked by the deduplicator.\n   * @param {string} key - The key to mark as unique.\n   */\n  markUnique(key) {\n    this.#uniques[key] = Number(new Date());\n  }\n}\n\nmodule.exports.Deduplicator = Deduplicator;\n"
  },
  {
    "path": "server/utils/agents/aibitat/utils/summarize.js",
    "content": "const { loadSummarizationChain } = require(\"langchain/chains\");\nconst { PromptTemplate } = require(\"@langchain/core/prompts\");\nconst { RecursiveCharacterTextSplitter } = require(\"@langchain/textsplitters\");\nconst Provider = require(\"../providers/ai-provider\");\n/**\n * @typedef {Object} LCSummarizationConfig\n * @property {string} provider The LLM to use for summarization (inherited)\n * @property {string} model The LLM Model to use for summarization (inherited)\n * @property {AbortController['signal']} controllerSignal Abort controller to stop recursive summarization\n * @property {string} content The text content of the text to summarize\n */\n\n/**\n * Summarize content using LLM LC-Chain call\n * @param {LCSummarizationConfig} The LLM to use for summarization (inherited)\n * @returns {Promise<string>} The summarized content.\n */\nasync function summarizeContent({\n  provider = \"openai\",\n  model = null,\n  controllerSignal,\n  content,\n}) {\n  const llm = Provider.LangChainChatModel(provider, {\n    temperature: 0,\n    model: model,\n  });\n\n  const textSplitter = new RecursiveCharacterTextSplitter({\n    separators: [\"\\n\\n\", \"\\n\"],\n    chunkSize: 10000,\n    chunkOverlap: 500,\n  });\n  const docs = await textSplitter.createDocuments([content]);\n\n  const mapPrompt = `\n      Write a detailed summary of the following text for a research purpose:\n      \"{text}\"\n      SUMMARY:\n      `;\n\n  const mapPromptTemplate = new PromptTemplate({\n    template: mapPrompt,\n    inputVariables: [\"text\"],\n  });\n\n  // This convenience function creates a document chain prompted to summarize a set of documents.\n  const chain = loadSummarizationChain(llm, {\n    type: \"map_reduce\",\n    combinePrompt: mapPromptTemplate,\n    combineMapPrompt: mapPromptTemplate,\n    verbose: process.env.NODE_ENV === \"development\",\n  });\n\n  const res = await chain.call({\n    ...(controllerSignal ? { signal: controllerSignal } : {}),\n    input_documents: docs,\n  });\n\n  return res.text;\n}\n\nmodule.exports = { summarizeContent };\n"
  },
  {
    "path": "server/utils/agents/aibitat/utils/toolReranker.js",
    "content": "const {\n  NativeEmbeddingReranker,\n} = require(\"../../../EmbeddingRerankers/native\");\nconst { TokenManager } = require(\"../../../helpers/tiktoken\");\n\nclass ToolReranker {\n  /**\n   * The default number of top tools to keep after reranking\n   * @type {number}\n   */\n  static defaultTopN = 15;\n\n  static instance = null;\n  static #reranker = null;\n\n  constructor() {\n    if (ToolReranker.instance) return ToolReranker.instance;\n    ToolReranker.instance = this;\n    this.tokenManager = new TokenManager();\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[33m[IntelligentSkillSelector]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Check if tool reranking is enabled via environment variable\n   * @returns {boolean}\n   */\n  static isEnabled() {\n    if (!(\"AGENT_SKILL_RERANKER_ENABLED\" in process.env)) return false;\n    if (process.env.AGENT_SKILL_RERANKER_ENABLED === \"false\") return false;\n    return true;\n  }\n\n  /**\n   * Get the configured topN value from environment or use default\n   * @returns {number}\n   */\n  static getTopN() {\n    const envTopN = parseInt(process.env.AGENT_SKILL_RERANKER_TOP_N, 10);\n    return !isNaN(envTopN) && envTopN > 0 ? envTopN : ToolReranker.defaultTopN;\n  }\n\n  /**\n   * Get or create the reranker singleton\n   * @returns {NativeEmbeddingReranker}\n   */\n  async #getReranker() {\n    if (!ToolReranker.#reranker) {\n      ToolReranker.#reranker = new NativeEmbeddingReranker();\n      await ToolReranker.#reranker.initClient();\n    }\n    return ToolReranker.#reranker;\n  }\n\n  /**\n   * Convert a tool/function definition to a text representation for reranking.\n   * Format follows the best practices benchmark we have: name, description, param descriptions, example prompts\n   * @param {Object} tool - The tool definition object\n   * @returns {{text: string, toolName: string, tool: Object, tokens: number}}\n   */\n  #toolToDocument(tool) {\n    const parts = [];\n    if (!tool || !tool.name)\n      return { text: null, toolName: null, tool: null, tokens: 0 };\n\n    parts.push(tool.name);\n    if (tool.description) parts.push(tool.description);\n\n    if (tool.parameters?.properties) {\n      const paramNames = Object.keys(tool.parameters.properties);\n      if (paramNames.length > 0) {\n        const paramDescriptions = paramNames.map((name) => {\n          const prop = tool.parameters.properties[name];\n          return prop.description ? `${name}: ${prop.description}` : name;\n        });\n        parts.push(paramDescriptions.join(\", \"));\n      }\n    }\n\n    // Include example prompts if available (common in aibitat built-in tools)\n    if (\n      tool.examples &&\n      Array.isArray(tool.examples) &&\n      tool.examples.length > 0\n    ) {\n      const examplePrompts = tool.examples\n        .map((ex) => ex.prompt)\n        .filter(Boolean);\n      if (examplePrompts.length > 0) {\n        parts.push(examplePrompts.join(\"; \"));\n      }\n    }\n\n    const textContent = parts.join(\"\\n\");\n    return {\n      text: textContent,\n      toolName: tool.name,\n      tool,\n      tokens: this.tokenManager.countFromString(textContent),\n    };\n  }\n\n  /**\n   * Rerank tools based on the user prompt and return the top N most relevant tools\n   * @param {string} userPrompt - The user's query/prompt\n   * @param {Object[]} tools - Array of tool/function definitions from aibitat.functions\n   * @param {Object} options - Options for reranking\n   * @param {number} options.topN - (optional) Number of top tools to return (default: ToolReranker.getTopN())\n   * @returns {Promise<Object[]>} - Array of reranked tools (top N)\n   */\n  async rerank(userPrompt, tools = [], options = {}) {\n    if (!ToolReranker.isEnabled()) return tools;\n    if (!tools || tools.length === 0) return tools;\n    const { topN = ToolReranker.getTopN() } = options;\n\n    if (tools.length <= topN) {\n      this.log(`${tools.length} tools <= ${topN}, skipping reranking`);\n      return tools;\n    }\n\n    try {\n      this.log(`Starting tool reranking for ${tools.length} tools...`);\n      const documents = tools.map((tool) => this.#toolToDocument(tool));\n      const originalTokenCount = documents.reduce(\n        (acc, doc) => acc + doc.tokens,\n        0\n      );\n\n      const startTime = Date.now();\n      const reranker = await this.#getReranker();\n      const rerankedDocs = await reranker.rerank(userPrompt, documents, {\n        topK: topN,\n      });\n      const elapsedMs = Date.now() - startTime;\n\n      const rerankedTools = rerankedDocs.map((doc) => doc.tool);\n      const newTokenCount = rerankedDocs.reduce(\n        (acc, doc) => acc + doc.tokens,\n        0\n      );\n      const percentSaved = Math.round(\n        ((originalTokenCount - newTokenCount) / originalTokenCount) * 100\n      );\n      this.log(`\nIdentified top ${rerankedTools.length} of ${tools.length} tools in ${elapsedMs}ms\n${originalTokenCount.toLocaleString()} -> ${newTokenCount.toLocaleString()} tokens \\x1b[0;93m(${percentSaved}% reduction)\\x1b[0m`);\n\n      let logText = \"Selected tools:\\n\";\n      rerankedDocs.forEach((doc, index) => {\n        logText += `  ${index + 1}. ${doc.toolName}\\n`;\n      });\n      this.log(logText);\n      return rerankedTools;\n    } catch (error) {\n      this.log(`Error during tool reranking: ${error.message}`);\n      this.log(\"Falling back to original tool set\");\n      return tools;\n    }\n  }\n}\n\nmodule.exports = { ToolReranker };\n"
  },
  {
    "path": "server/utils/agents/defaults.js",
    "content": "const AgentPlugins = require(\"./aibitat/plugins\");\nconst { SystemSettings } = require(\"../../models/systemSettings\");\nconst { safeJsonParse } = require(\"../http\");\nconst Provider = require(\"./aibitat/providers/ai-provider\");\nconst ImportedPlugin = require(\"./imported\");\nconst { AgentFlows } = require(\"../agentFlows\");\nconst MCPCompatibilityLayer = require(\"../MCP\");\n\n// This is a list of skills that are built-in and default enabled.\nconst DEFAULT_SKILLS = [\n  AgentPlugins.memory.name,\n  AgentPlugins.docSummarizer.name,\n  AgentPlugins.webScraping.name,\n];\n\nconst USER_AGENT = {\n  name: \"USER\",\n  getDefinition: () => {\n    return {\n      interrupt: \"ALWAYS\",\n      role: \"I am the human monitor and oversee this chat. Any questions on action or decision making should be directed to me.\",\n    };\n  },\n};\n\nconst WORKSPACE_AGENT = {\n  name: \"@agent\",\n  /**\n   * Get the definition for the workspace agent with its role (prompt) and functions in Aibitat format\n   * @param {string} provider\n   * @param {import(\"@prisma/client\").workspaces | null} workspace\n   * @param {import(\"@prisma/client\").users | null} user\n   * @returns {Promise<{ role: string, functions: object[] }>}\n   */\n  getDefinition: async (provider = null, workspace = null, user = null) => {\n    const basePrompt = await Provider.systemPrompt({\n      provider,\n      workspace,\n      user,\n    });\n\n    return {\n      role: basePrompt,\n      functions: [\n        ...(await agentSkillsFromSystemSettings()),\n        ...ImportedPlugin.activeImportedPlugins(),\n        ...AgentFlows.activeFlowPlugins(),\n        ...(await new MCPCompatibilityLayer().activeMCPServers()),\n      ],\n    };\n  },\n};\n\n/**\n * Fetches and preloads the names/identifiers for plugins that will be dynamically\n * loaded later\n * @returns {Promise<string[]>}\n */\nasync function agentSkillsFromSystemSettings() {\n  const systemFunctions = [];\n\n  // Load non-imported built-in skills that are configurable, but are default enabled.\n  const _disabledDefaultSkills = safeJsonParse(\n    await SystemSettings.getValueOrFallback(\n      { label: \"disabled_agent_skills\" },\n      \"[]\"\n    ),\n    []\n  );\n  DEFAULT_SKILLS.forEach((skill) => {\n    if (!_disabledDefaultSkills.includes(skill))\n      systemFunctions.push(AgentPlugins[skill].name);\n  });\n\n  // Load non-imported built-in skills that are configurable.\n  const _setting = safeJsonParse(\n    await SystemSettings.getValueOrFallback(\n      { label: \"default_agent_skills\" },\n      \"[]\"\n    ),\n    []\n  );\n  _setting.forEach((skillName) => {\n    if (!AgentPlugins.hasOwnProperty(skillName)) return;\n\n    // This is a plugin module with many sub-children plugins who\n    // need to be named via `${parent}#${child}` naming convention\n    if (Array.isArray(AgentPlugins[skillName].plugin)) {\n      for (const subPlugin of AgentPlugins[skillName].plugin) {\n        systemFunctions.push(\n          `${AgentPlugins[skillName].name}#${subPlugin.name}`\n        );\n      }\n      return;\n    }\n\n    // This is normal single-stage plugin\n    systemFunctions.push(AgentPlugins[skillName].name);\n  });\n  return systemFunctions;\n}\n\nmodule.exports = {\n  USER_AGENT,\n  WORKSPACE_AGENT,\n  agentSkillsFromSystemSettings,\n};\n"
  },
  {
    "path": "server/utils/agents/ephemeral.js",
    "content": "const AIbitat = require(\"./aibitat\");\nconst AgentPlugins = require(\"./aibitat/plugins\");\nconst ImportedPlugin = require(\"./imported\");\nconst MCPCompatibilityLayer = require(\"../MCP\");\nconst { AgentFlows } = require(\"../agentFlows\");\nconst { httpSocket } = require(\"./aibitat/plugins/http-socket.js\");\nconst { User } = require(\"../../models/user\");\nconst { Workspace } = require(\"../../models/workspace\");\nconst { WorkspaceChats } = require(\"../../models/workspaceChats\");\nconst { WorkspaceParsedFiles } = require(\"../../models/workspaceParsedFiles\");\nconst { DocumentManager } = require(\"../DocumentManager\");\nconst { safeJsonParse } = require(\"../http\");\nconst {\n  USER_AGENT,\n  WORKSPACE_AGENT,\n  agentSkillsFromSystemSettings,\n} = require(\"./defaults\");\nconst { AgentHandler } = require(\".\");\nconst {\n  WorkspaceAgentInvocation,\n} = require(\"../../models/workspaceAgentInvocation\");\n\n/**\n * This is an instance and functional Agent handler, but it does not utilize\n * sessions or websocket's and is instead a singular one-off agent run that does\n * not persist between invocations\n */\nclass EphemeralAgentHandler extends AgentHandler {\n  /** @type {string|null} the unique identifier for the agent invocation */\n  #invocationUUID = null;\n  /** @type {import(\"@prisma/client\").workspaces|null} the workspace to use for the agent */\n  #workspace = null;\n  /** @type {import(\"@prisma/client\").users[\"id\"]|null} the user id to use for the agent */\n  #userId = null;\n  /** @type {import(\"@prisma/client\").workspace_threads|null} the workspace thread id to use for the agent */\n  #threadId = null;\n  /** @type {string|null} the session id to use for the agent */\n  #sessionId = null;\n  /** @type {string|null} the prompt to use for the agent */\n  #prompt = null;\n  /** @type {string[]} the functions to load into the agent (Aibitat plugins) */\n  #funcsToLoad = [];\n  /** @type {Array<{name: string, mime: string, contentString: string}>} attachments for multimodal support */\n  #attachments = [];\n\n  /** @type {AIbitat|null} */\n  aibitat = null;\n  /** @type {string|null} */\n  channel = null;\n  /** @type {string|null} */\n  provider = null;\n  /** @type {string|null} the model to use for the agent */\n  model = null;\n\n  /**\n   * @param {{\n   * uuid: string,\n   * workspace: import(\"@prisma/client\").workspaces,\n   * prompt: string,\n   * userId: import(\"@prisma/client\").users[\"id\"]|null,\n   * threadId: import(\"@prisma/client\").workspace_threads[\"id\"]|null,\n   * sessionId: string|null,\n   * attachments: Array<{name: string, mime: string, contentString: string}>\n   * }} parameters\n   */\n  constructor({\n    uuid,\n    workspace,\n    prompt,\n    userId = null,\n    threadId = null,\n    sessionId = null,\n    attachments = [],\n  }) {\n    super({ uuid });\n    this.#invocationUUID = uuid;\n    this.#workspace = workspace;\n    this.#prompt = prompt;\n\n    // Note: userId for ephemeral agent is only available\n    // via the workspace-thread chat endpoints for the API\n    // since workspaces can belong to multiple users.\n    this.#userId = userId;\n    this.#threadId = threadId;\n    this.#sessionId = sessionId;\n    this.#attachments = attachments;\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[EphemeralAgentHandler]\\x1b[0m ${text}`, ...args);\n  }\n\n  closeAlert() {\n    this.log(`End ${this.#invocationUUID}::${this.provider}:${this.model}`);\n  }\n\n  async #chatHistory(limit = 10) {\n    try {\n      const rawHistory = (\n        await WorkspaceChats.where(\n          {\n            workspaceId: this.#workspace.id,\n            user_id: this.#userId || null,\n            thread_id: this.#threadId || null,\n            api_session_id: this.#sessionId,\n            include: true,\n          },\n          limit,\n          { id: \"desc\" }\n        )\n      ).reverse();\n\n      const agentHistory = [];\n      rawHistory.forEach((chatLog) => {\n        agentHistory.push(\n          {\n            from: USER_AGENT.name,\n            to: WORKSPACE_AGENT.name,\n            content: chatLog.prompt,\n            state: \"success\",\n          },\n          {\n            from: WORKSPACE_AGENT.name,\n            to: USER_AGENT.name,\n            content: safeJsonParse(chatLog.response)?.text || \"\",\n            state: \"success\",\n          }\n        );\n      });\n      return agentHistory;\n    } catch (e) {\n      this.log(\"Error loading chat history\", e.message);\n      return [];\n    }\n  }\n\n  /**\n   * Attempts to find a fallback provider and model to use if the workspace\n   * does not have an explicit `agentProvider` and `agentModel` set.\n   * 1. Fallback to the workspace `chatProvider` and `chatModel` if they exist.\n   * 2. Fallback to the system `LLM_PROVIDER` and try to load the associated default model via ENV params or a base available model.\n   * 3. Otherwise, return null - will likely throw an error the user can act on.\n   * @returns {object|null} - An object with provider and model keys.\n   */\n  #getFallbackProvider() {\n    // First, fallback to the workspace chat provider and model if they exist\n    if (this.#workspace.chatProvider && this.#workspace.chatModel) {\n      return {\n        provider: this.#workspace.chatProvider,\n        model: this.#workspace.chatModel,\n      };\n    }\n\n    // If workspace does not have chat provider and model fallback\n    // to system provider and try to load provider default model\n    const systemProvider = process.env.LLM_PROVIDER;\n    const systemModel = this.providerDefault(systemProvider);\n    if (systemProvider && systemModel) {\n      return {\n        provider: systemProvider,\n        model: systemModel,\n      };\n    }\n\n    return null;\n  }\n\n  /**\n   * Finds or assumes the model preference value to use for API calls.\n   * If multi-model loading is supported, we use their agent model selection of the workspace\n   * If not supported, we attempt to fallback to the system provider value for the LLM preference\n   * and if that fails - we assume a reasonable base model to exist.\n   * @returns {string|null} the model preference value to use in API calls\n   */\n  #fetchModel() {\n    // Provider was not explicitly set for workspace, so we are going to run our fallback logic\n    // that will set a provider and model for us to use.\n    if (!this.provider) {\n      const fallback = this.#getFallbackProvider();\n      if (!fallback) throw new Error(\"No valid provider found for the agent.\");\n      this.provider = fallback.provider; // re-set the provider to the fallback provider so it is not null.\n      return fallback.model; // set its defined model based on fallback logic.\n    }\n\n    // The provider was explicitly set, so check if the workspace has an agent model set.\n    if (this.#workspace.agentModel) return this.#workspace.agentModel;\n\n    // Otherwise, we have no model to use - so guess a default model to use via the provider\n    // and it's system ENV params and if that fails - we return either a base model or null.\n    return this.providerDefault();\n  }\n\n  #providerSetupAndCheck() {\n    this.provider = this.#workspace.agentProvider ?? null;\n    this.model = this.#fetchModel();\n\n    if (!this.provider)\n      throw new Error(\"No valid provider found for the agent.\");\n    this.log(`Start ${this.#invocationUUID}::${this.provider}:${this.model}`);\n    this.checkSetup();\n  }\n\n  async #attachPlugins(args) {\n    for (const name of this.#funcsToLoad) {\n      // Load child plugin\n      if (name.includes(\"#\")) {\n        const [parent, childPluginName] = name.split(\"#\");\n        if (!AgentPlugins.hasOwnProperty(parent)) {\n          this.log(\n            `${parent} is not a valid plugin. Skipping inclusion to agent cluster.`\n          );\n          continue;\n        }\n\n        const childPlugin = AgentPlugins[parent].plugin.find(\n          (child) => child.name === childPluginName\n        );\n        if (!childPlugin) {\n          this.log(\n            `${parent} does not have child plugin named ${childPluginName}. Skipping inclusion to agent cluster.`\n          );\n          continue;\n        }\n\n        const callOpts = this.parseCallOptions(\n          args,\n          childPlugin?.startupConfig?.params,\n          name\n        );\n        this.aibitat.use(childPlugin.plugin(callOpts));\n        this.log(\n          `Attached ${parent}:${childPluginName} plugin to Agent cluster`\n        );\n        continue;\n      }\n\n      // Load flow plugin. This is marked by `@@flow_` in the array of functions to load.\n      // Replace the @@flow_ placeholder in the agent's function list with the actual\n      // tool name so the function lookup in reply() can find it.\n      if (name.startsWith(\"@@flow_\")) {\n        const uuid = name.replace(\"@@flow_\", \"\");\n        const plugin = AgentFlows.loadFlowPlugin(uuid, this.aibitat);\n        if (!plugin) {\n          this.log(\n            `Flow ${uuid} not found in flows directory. Skipping inclusion to agent cluster.`\n          );\n          continue;\n        }\n\n        this.aibitat.agents.get(\"@agent\").functions = this.aibitat.agents\n          .get(\"@agent\")\n          .functions.filter((f) => f !== name);\n        this.aibitat.agents.get(\"@agent\").functions.push(plugin.name);\n\n        this.aibitat.use(plugin.plugin());\n        this.log(\n          `Attached flow ${plugin.name} (${plugin.flowName}) plugin to Agent cluster`\n        );\n        continue;\n      }\n\n      // Load MCP plugin. This is marked by `@@mcp_` in the array of functions to load.\n      // All sub-tools are loaded here and are denoted by `pluginName:toolName` as their identifier.\n      // This will replace the parent MCP server plugin with the sub-tools as child plugins so they\n      // can be called directly by the agent when invoked.\n      // Since to get to this point, the `activeMCPServers` method has already been called, we can\n      // safely assume that the MCP server is running and the tools are available/loaded.\n      if (name.startsWith(\"@@mcp_\")) {\n        const mcpPluginName = name.replace(\"@@mcp_\", \"\");\n        const plugins =\n          await new MCPCompatibilityLayer().convertServerToolsToPlugins(\n            mcpPluginName,\n            this.aibitat\n          );\n        if (!plugins) {\n          this.log(\n            `MCP ${mcpPluginName} not found in MCP server config. Skipping inclusion to agent cluster.`\n          );\n          continue;\n        }\n\n        // Remove the old function from the agent functions directly\n        // and push the new ones onto the end of the array so that they are loaded properly.\n        this.aibitat.agents.get(\"@agent\").functions = this.aibitat.agents\n          .get(\"@agent\")\n          .functions.filter((f) => f.name !== name);\n        for (const plugin of plugins)\n          this.aibitat.agents.get(\"@agent\").functions.push(plugin.name);\n\n        plugins.forEach((plugin) => {\n          this.aibitat.use(plugin.plugin());\n          this.log(\n            `Attached MCP::${plugin.toolName} MCP tool to Agent cluster`\n          );\n        });\n        continue;\n      }\n\n      // Load imported plugin. This is marked by `@@` in the array of functions to load.\n      // and is the @@hubID of the plugin.\n      if (name.startsWith(\"@@\")) {\n        const hubId = name.replace(\"@@\", \"\");\n        const valid = ImportedPlugin.validateImportedPluginHandler(hubId);\n        if (!valid) {\n          this.log(\n            `Imported plugin by hubId ${hubId} not found in plugin directory. Skipping inclusion to agent cluster.`\n          );\n          continue;\n        }\n\n        const plugin = ImportedPlugin.loadPluginByHubId(hubId);\n        const callOpts = plugin.parseCallOptions();\n        this.aibitat.use(plugin.plugin(callOpts));\n        this.log(\n          `Attached ${plugin.name} (${hubId}) imported plugin to Agent cluster`\n        );\n        continue;\n      }\n\n      // Load single-stage plugin.\n      if (!AgentPlugins.hasOwnProperty(name)) {\n        this.log(\n          `${name} is not a valid plugin. Skipping inclusion to agent cluster.`\n        );\n        continue;\n      }\n\n      const callOpts = this.parseCallOptions(\n        args,\n        AgentPlugins[name].startupConfig.params\n      );\n      const AIbitatPlugin = AgentPlugins[name];\n      this.aibitat.use(AIbitatPlugin.plugin(callOpts));\n      this.log(`Attached ${name} plugin to Agent cluster`);\n    }\n  }\n\n  async #loadAgents() {\n    // Default User agent and workspace agent\n    this.log(`Attaching user and default agent to Agent cluster.`);\n    this.aibitat.agent(USER_AGENT.name, USER_AGENT.getDefinition());\n    const user = this.#userId\n      ? await User.get({ id: Number(this.#userId) })\n      : null;\n\n    this.aibitat.agent(\n      WORKSPACE_AGENT.name,\n      await WORKSPACE_AGENT.getDefinition(this.provider, this.#workspace, user)\n    );\n\n    this.#funcsToLoad = [\n      ...(await agentSkillsFromSystemSettings()),\n      ...ImportedPlugin.activeImportedPlugins(),\n      ...AgentFlows.activeFlowPlugins(),\n      ...(await new MCPCompatibilityLayer().activeMCPServers()),\n    ];\n  }\n\n  async init() {\n    this.#providerSetupAndCheck();\n    return this;\n  }\n\n  /**\n   * Fetch fresh parsed files and pinned documents, format them for injection into user messages.\n   * Called on every chat turn to ensure context is always up-to-date.\n   * @returns {Promise<string>} Formatted context string to append to user message\n   */\n  async #fetchParsedFileContext() {\n    const user = this.#userId ? { id: this.#userId } : null;\n    const thread = this.#threadId ? { id: this.#threadId } : null;\n    const documentManager = new DocumentManager({\n      workspace: this.#workspace,\n    });\n\n    return Promise.all([\n      WorkspaceParsedFiles.getContextFiles(this.#workspace, thread, user),\n      documentManager.pinnedDocs(),\n    ])\n      .then(([parsedFiles, pinnedDocs]) => {\n        const allDocuments = [\n          ...(parsedFiles || []).map((doc) => ({\n            name: doc.title || \"Uploaded Document\",\n            content: doc.pageContent,\n          })),\n          ...(pinnedDocs || []).map((doc) => ({\n            name: doc.title || doc.metadata?.title || \"Pinned Document\",\n            content: doc.pageContent,\n          })),\n        ];\n\n        if (allDocuments.length === 0) return \"\";\n\n        if (parsedFiles?.length > 0)\n          this.log(\n            `Injecting ${parsedFiles.length} parsed file(s) into user message`\n          );\n        if (pinnedDocs?.length > 0)\n          this.log(\n            `Injecting ${pinnedDocs.length} pinned document(s) into user message`\n          );\n\n        return (\n          \"\\n\\n<attached_documents>\\n\" +\n          allDocuments\n            .map((doc, i) => {\n              const filename = doc.name || `Document ${i + 1}`;\n              return `<document name=\"${filename}\">\\n${doc.content}\\n</document>`;\n            })\n            .join(\"\\n\") +\n          \"\\n</attached_documents>\"\n        );\n      })\n      .catch((e) => {\n        this.log(\"Error fetching parsed file context\", e.message);\n        return \"\";\n      });\n  }\n\n  /**\n   * Strip the @agent command from the message if it exists.\n   * Prevents hallucination by the agent when the @agent command is used from the model thinking\n   * it is an agent or something itself.\n   * If the user sent nothing after the @agent command - assume its a greeting.\n   * @param {string} message - The message to strip the @agent command from.\n   * @returns {string} The message with the @agent command stripped.\n   */\n  #stripAgentCommand(message = \"\") {\n    const stripped = String(message)\n      .replace(/^@agent\\s*/, \"\")\n      .trim();\n    if (!stripped) return \"Hello!\";\n    return stripped;\n  }\n\n  async createAIbitat(\n    args = {\n      handler: null,\n    }\n  ) {\n    this.aibitat = new AIbitat({\n      provider: this.provider ?? \"openai\",\n      model: this.model ?? \"gpt-4o\",\n      chats: await this.#chatHistory(20),\n      handlerProps: {\n        invocation: {\n          workspace: this.#workspace,\n          workspace_id: this.#workspace.id,\n        },\n        log: this.log,\n      },\n    });\n\n    // Register callback to fetch fresh parsed file context on each chat turn\n    // This injects parsed files into user messages instead of system prompt\n    this.aibitat.fetchParsedFileContext = () => this.#fetchParsedFileContext();\n\n    // Attach HTTP response object if defined for chunk streaming.\n    this.log(`Attached ${httpSocket.name} plugin to Agent cluster`);\n    this.aibitat.use(\n      httpSocket.plugin({\n        handler: args.handler,\n        muteUserReply: true,\n        introspection: true,\n      })\n    );\n\n    // Load required agents (Default + custom)\n    await this.#loadAgents();\n\n    // Attach all required plugins for functions to operate.\n    await this.#attachPlugins(args);\n  }\n\n  startAgentCluster() {\n    return this.aibitat.start({\n      from: USER_AGENT.name,\n      to: this.channel ?? WORKSPACE_AGENT.name,\n      content: this.#stripAgentCommand(this.#prompt),\n      attachments: this.#attachments,\n    });\n  }\n\n  /**\n   * Determine if the message should invoke the agent handler.\n   * This is true when the user explicitly invokes an agent (via @agent prefix)\n   * or when the workspace is in automatic mode **and** the provider supports native tool calling.\n   * @param {{message: string, workspace?: object, chatMode?: string}} parameters\n   * @returns {Promise<boolean>}\n   */\n  static async isAgentInvocation({\n    message,\n    workspace = null,\n    chatMode = null,\n  }) {\n    if (this.#isAgentCommandInvocation({ message })) return true;\n    if (chatMode === \"automatic\") {\n      if (!workspace) return false;\n      if (await Workspace.supportsNativeToolCalling(workspace)) return true;\n      return false;\n    }\n    return false;\n  }\n\n  /**\n   * Determine if the message provided is an agent invocation.\n   * @param {{message:string}} parameters\n   * @returns {boolean}\n   */\n  static #isAgentCommandInvocation({ message }) {\n    const agentHandles = WorkspaceAgentInvocation.parseAgents(message);\n    if (agentHandles.length > 0) return true;\n    return false;\n  }\n}\n\nconst EventEmitter = require(\"node:events\");\nconst { writeResponseChunk } = require(\"../helpers/chat/responses\");\n\n/**\n * This is a special EventEmitter specifically used in the Aibitat agent handler\n * that enables us to use HTTP to relay all .introspect and .send events back to an\n * http handler instead of websockets, like we do on the frontend. This interface is meant to\n * mock a websocket interface for the methods used and bind them to an HTTP method so that the developer\n * API can invoke agent calls.\n */\nclass EphemeralEventListener extends EventEmitter {\n  messages = [];\n  constructor() {\n    super();\n  }\n\n  send(jsonData) {\n    const data = JSON.parse(jsonData);\n    this.messages.push(data);\n    this.emit(\"chunk\", data);\n  }\n\n  close() {\n    this.emit(\"closed\");\n  }\n\n  /**\n   * Compacts all messages in class and returns them in a condensed format.\n   * @returns {{thoughts: string[], textResponse: string}}\n   */\n  packMessages() {\n    const thoughts = [];\n    let textResponse = null;\n    for (let msg of this.messages) {\n      if (msg.type !== \"statusResponse\") {\n        textResponse = msg.content;\n      } else {\n        thoughts.push(msg.content);\n      }\n    }\n    return { thoughts, textResponse };\n  }\n\n  /**\n   * Waits on the HTTP plugin to emit the 'closed' event from the agentHandler\n   * so that we can compact and return all the messages in the current queue.\n   * @returns {Promise<{thoughts: string[], textResponse: string}>}\n   */\n  async waitForClose() {\n    return new Promise((resolve) => {\n      this.once(\"closed\", () => resolve(this.packMessages()));\n    });\n  }\n\n  /**\n   * Streams the events with `writeResponseChunk` over HTTP chunked encoding\n   * and returns on the close event emission.\n   * ----------\n   * DevNote: Agents do not stream so in here we are simply\n   * emitting the thoughts and text response as soon as we get them.\n   * @param {import(\"express\").Response} response\n   * @param {string} uuid - Unique identifier that is the same across chunks.\n   * @returns {Promise<{thoughts: string[], textResponse: string}>}\n   */\n  async streamAgentEvents(response, uuid) {\n    const onChunkHandler = (data) => {\n      if (data.type === \"statusResponse\") {\n        return writeResponseChunk(response, {\n          id: uuid,\n          type: \"agentThought\",\n          thought: data.content,\n          sources: [],\n          attachments: [],\n          close: false,\n          error: null,\n          animate: true,\n        });\n      }\n\n      return writeResponseChunk(response, {\n        id: uuid,\n        type: \"textResponse\",\n        textResponse: data.content,\n        sources: [],\n        attachments: [],\n        close: true,\n        error: null,\n        animate: false,\n      });\n    };\n    this.on(\"chunk\", onChunkHandler);\n\n    // Wait for close and after remove chunk listener\n    return this.waitForClose().then((closedResponse) => {\n      this.removeListener(\"chunk\", onChunkHandler);\n      return closedResponse;\n    });\n  }\n}\n\nmodule.exports = { EphemeralAgentHandler, EphemeralEventListener };\n"
  },
  {
    "path": "server/utils/agents/imported-manifest.schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"AnythingLLM Agent Skill Plugin Manifest Schema\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"active\": {\n      \"type\": \"boolean\",\n      \"description\": \"Determines if the custom agent skill is active.\"\n    },\n    \"hubId\": {\n      \"type\": \"string\",\n      \"description\": \"Used to identify the custom agent skill. Must be the same as the parent folder name.\"\n    },\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"The human-readable name of the skill displayed in the AnythingLLM UI.\"\n    },\n    \"schema\": {\n      \"type\": \"string\",\n      \"enum\": [\"skill-1.0.0\"],\n      \"description\": \"Must be 'skill-1.0.0'. May be updated on manifest spec changes.\"\n    },\n    \"version\": {\n      \"type\": \"string\",\n      \"description\": \"Version of the custom agent skill, defined by the user.\"\n    },\n    \"description\": {\n      \"type\": \"string\",\n      \"description\": \"Short description of the custom agent skill.\"\n    },\n    \"author\": {\n      \"type\": \"string\",\n      \"description\": \"Author tag of the custom agent skill.\"\n    },\n    \"author_url\": {\n      \"type\": \"string\",\n      \"format\": \"uri\",\n      \"description\": \"URL of the author of the custom agent skill.\"\n    },\n    \"license\": {\n      \"type\": \"string\",\n      \"description\": \"License of the custom agent skill.\"\n    },\n    \"setup_args\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"type\": {\n            \"type\": \"string\",\n            \"description\": \"Type of value expected.\"\n          },\n          \"required\": {\n            \"type\": \"boolean\",\n            \"description\": \"Indicates if the argument is required.\"\n          },\n          \"input\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"type\": {\n                \"type\": \"string\",\n                \"description\": \"Type of input to be rendered.\"\n              },\n              \"default\": {\n                \"type\": \"string\",\n                \"description\": \"Default value of the input.\"\n              },\n              \"placeholder\": {\n                \"type\": \"string\",\n                \"description\": \"Placeholder text for the input.\"\n              },\n              \"hint\": {\n                \"type\": \"string\",\n                \"description\": \"Hint text for the input.\"\n              }\n            },\n            \"required\": [\"type\"],\n            \"additionalProperties\": false\n          },\n          \"value\": {\n            \"type\": \"string\",\n            \"description\": \"Preset value of the argument.\"\n          }\n        },\n        \"required\": [\"type\"],\n        \"additionalProperties\": false\n      },\n      \"description\": \"Setup arguments used to configure the custom agent skill from the UI and make runtime arguments accessible in the handler.js file when the skill is called.\"\n    },\n    \"examples\": {\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"prompt\": {\n            \"type\": \"string\",\n            \"description\": \"Example prompt for the custom agent skill.\"\n          },\n          \"call\": {\n            \"type\": \"string\",\n            \"description\": \"Expected invocation format matching the input format of the custom agent skill.\"\n          }\n        },\n        \"required\": [\"prompt\", \"call\"],\n        \"additionalProperties\": false\n      },\n      \"description\": \"Array of examples used to pre-inject examples into the custom agent skill.\"\n    },\n    \"entrypoint\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"file\": {\n          \"type\": \"string\",\n          \"description\": \"Location of the file to be executed relative to the plugin.json file.\"\n        },\n        \"params\": {\n          \"type\": \"object\",\n          \"additionalProperties\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"description\": {\n                \"type\": \"string\",\n                \"description\": \"Short description of the parameter's purpose.\"\n              },\n              \"type\": {\n                \"type\": \"string\",\n                \"enum\": [\"string\", \"number\", \"boolean\"],\n                \"description\": \"Type of the parameter.\"\n              }\n            },\n            \"required\": [\"description\", \"type\"],\n            \"additionalProperties\": false\n          },\n          \"description\": \"Parameters expected by the custom agent skill.\"\n        }\n      },\n      \"required\": [\"file\", \"params\"],\n      \"additionalProperties\": false,\n      \"description\": \"Defines the entrypoint of the custom agent skill and the expected inputs.\"\n    },\n    \"imported\": {\n      \"type\": \"boolean\",\n      \"enum\": [true],\n      \"description\": \"Must be set to true.\"\n    }\n  },\n  \"required\": [\n    \"active\",\n    \"hubId\",\n    \"name\",\n    \"schema\",\n    \"version\",\n    \"description\",\n    \"entrypoint\",\n    \"imported\"\n  ],\n  \"additionalProperties\": true\n}\n"
  },
  {
    "path": "server/utils/agents/imported.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { safeJsonParse } = require(\"../http\");\nconst { isWithin, normalizePath } = require(\"../files\");\nconst { CollectorApi } = require(\"../collectorApi\");\nconst pluginsPath =\n  process.env.NODE_ENV === \"development\"\n    ? path.resolve(__dirname, \"../../storage/plugins/agent-skills\")\n    : path.resolve(process.env.STORAGE_DIR, \"plugins\", \"agent-skills\");\nconst sharedWebScraper = new CollectorApi();\n\nclass ImportedPlugin {\n  constructor(config) {\n    this.config = config;\n    this.handlerLocation = path.resolve(\n      pluginsPath,\n      this.config.hubId,\n      \"handler.js\"\n    );\n    delete require.cache[require.resolve(this.handlerLocation)];\n    this.handler = require(this.handlerLocation);\n    this.name = config.hubId;\n    this.startupConfig = {\n      params: {},\n    };\n  }\n\n  /**\n   * Gets the imported plugin handler.\n   * @param {string} hubId - The hub ID of the plugin.\n   * @returns {ImportedPlugin} - The plugin handler.\n   */\n  static loadPluginByHubId(hubId) {\n    const configLocation = path.resolve(\n      pluginsPath,\n      normalizePath(hubId),\n      \"plugin.json\"\n    );\n    if (!this.isValidLocation(configLocation)) return;\n    const config = safeJsonParse(fs.readFileSync(configLocation, \"utf8\"));\n    return new ImportedPlugin(config);\n  }\n\n  static isValidLocation(pathToValidate) {\n    if (!isWithin(pluginsPath, pathToValidate)) return false;\n    if (!fs.existsSync(pathToValidate)) return false;\n    return true;\n  }\n\n  /**\n   * Checks if the plugin folder exists and if it does not, creates the folder.\n   */\n  static checkPluginFolderExists() {\n    const dir = path.resolve(pluginsPath);\n    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n    return;\n  }\n\n  /**\n   * Loads plugins from `plugins` folder in storage that are custom loaded and defined.\n   * only loads plugins that are active: true.\n   * @returns {string[]} - array of plugin names to be loaded later.\n   */\n  static activeImportedPlugins() {\n    const plugins = [];\n    this.checkPluginFolderExists();\n    const folders = fs.readdirSync(path.resolve(pluginsPath));\n    for (const folder of folders) {\n      const configLocation = path.resolve(\n        pluginsPath,\n        normalizePath(folder),\n        \"plugin.json\"\n      );\n      if (!this.isValidLocation(configLocation)) continue;\n      const config = safeJsonParse(fs.readFileSync(configLocation, \"utf8\"));\n      if (config.active) plugins.push(`@@${config.hubId}`);\n    }\n    return plugins;\n  }\n\n  /**\n   * Lists all imported plugins.\n   * @returns {Array} - array of plugin configurations (JSON).\n   */\n  static listImportedPlugins() {\n    const plugins = [];\n    this.checkPluginFolderExists();\n    if (!fs.existsSync(pluginsPath)) return plugins;\n\n    const folders = fs.readdirSync(path.resolve(pluginsPath));\n    for (const folder of folders) {\n      const configLocation = path.resolve(\n        pluginsPath,\n        normalizePath(folder),\n        \"plugin.json\"\n      );\n      if (!this.isValidLocation(configLocation)) continue;\n      const config = safeJsonParse(fs.readFileSync(configLocation, \"utf8\"));\n      plugins.push(config);\n    }\n    return plugins;\n  }\n\n  /**\n   * Updates a plugin configuration.\n   * @param {string} hubId - The hub ID of the plugin.\n   * @param {object} config - The configuration to update.\n   * @returns {object} - The updated configuration.\n   */\n  static updateImportedPlugin(hubId, config) {\n    const configLocation = path.resolve(\n      pluginsPath,\n      normalizePath(hubId),\n      \"plugin.json\"\n    );\n    if (!this.isValidLocation(configLocation)) return;\n\n    const currentConfig = safeJsonParse(\n      fs.readFileSync(configLocation, \"utf8\"),\n      null\n    );\n    if (!currentConfig) return;\n\n    const updatedConfig = { ...currentConfig, ...config };\n    fs.writeFileSync(configLocation, JSON.stringify(updatedConfig, null, 2));\n    return updatedConfig;\n  }\n\n  /**\n   * Deletes a plugin. Removes the entire folder of the object.\n   * @param {string} hubId - The hub ID of the plugin.\n   * @returns {boolean} - True if the plugin was deleted, false otherwise.\n   */\n  static deletePlugin(hubId) {\n    if (!hubId) throw new Error(\"No plugin hubID passed.\");\n    const pluginFolder = path.resolve(pluginsPath, normalizePath(hubId));\n    if (!this.isValidLocation(pluginFolder)) return;\n    fs.rmSync(pluginFolder, { recursive: true });\n    return true;\n  }\n\n  /**\n  /**\n   * Validates if the handler.js file exists for the given plugin.\n   * @param {string} hubId - The hub ID of the plugin.\n   * @returns {boolean} - True if the handler.js file exists, false otherwise.\n   */\n  static validateImportedPluginHandler(hubId) {\n    const handlerLocation = path.resolve(\n      pluginsPath,\n      normalizePath(hubId),\n      \"handler.js\"\n    );\n    return this.isValidLocation(handlerLocation);\n  }\n\n  parseCallOptions() {\n    const callOpts = {};\n    if (!this.config.setup_args || typeof this.config.setup_args !== \"object\") {\n      return callOpts;\n    }\n    for (const [param, definition] of Object.entries(this.config.setup_args)) {\n      if (definition.required && !definition?.value) {\n        console.log(\n          `'${param}' required value for '${this.name}' plugin is missing. Plugin may not function or crash agent.`\n        );\n        continue;\n      }\n      callOpts[param] = definition.value || definition.default || null;\n    }\n    return callOpts;\n  }\n\n  plugin(runtimeArgs = {}) {\n    const customFunctions = this.handler.runtime;\n    return {\n      runtimeArgs,\n      name: this.name,\n      config: this.config,\n      setup(aibitat) {\n        aibitat.function({\n          super: aibitat,\n          name: this.name,\n          config: this.config,\n          runtimeArgs: this.runtimeArgs,\n          description: this.config.description,\n          logger: aibitat?.handlerProps?.log || console.log, // Allows plugin to log to the console.\n          introspect: aibitat?.introspect || console.log, // Allows plugin to display a \"thought\" the chat window UI.\n          runtime: \"docker\",\n          webScraper: sharedWebScraper,\n          examples: this.config.examples ?? [],\n          parameters: {\n            $schema: \"http://json-schema.org/draft-07/schema#\",\n            type: \"object\",\n            properties: this.config.entrypoint.params ?? {},\n            additionalProperties: false,\n          },\n          ...customFunctions,\n        });\n      },\n    };\n  }\n\n  /**\n   * Imports a community item from a URL.\n   * The community item is a zip file that contains a plugin.json file and handler.js file.\n   * This function will unzip the file and import the plugin into the agent-skills folder\n   * based on the hubId found in the plugin.json file.\n   * The zip file will be downloaded to the pluginsPath folder and then unzipped and finally deleted.\n   * @param {string} url - The signed URL of the community item zip file.\n   * @param {object} item - The community item.\n   * @returns {Promise<object>} - The result of the import.\n   */\n  static async importCommunityItemFromUrl(url, item) {\n    this.checkPluginFolderExists();\n    const hubId = item.id;\n    if (!hubId) return { success: false, error: \"No hubId passed to import.\" };\n\n    const zipFilePath = path.resolve(pluginsPath, `${item.id}.zip`);\n    const pluginFile = item.manifest.files.find(\n      (file) => file.name === \"plugin.json\"\n    );\n    if (!pluginFile)\n      return {\n        success: false,\n        error: \"No plugin.json file found in manifest.\",\n      };\n\n    const pluginFolder = path.resolve(pluginsPath, normalizePath(hubId));\n    if (fs.existsSync(pluginFolder))\n      console.log(\n        \"ImportedPlugin.importCommunityItemFromUrl - plugin folder already exists - will overwrite\"\n      );\n\n    try {\n      const protocol = new URL(url).protocol.replace(\":\", \"\");\n      const httpLib = protocol === \"https\" ? require(\"https\") : require(\"http\");\n\n      const downloadZipFile = new Promise(async (resolve) => {\n        try {\n          console.log(\n            \"ImportedPlugin.importCommunityItemFromUrl - downloading asset from \",\n            new URL(url).origin\n          );\n          const zipFile = fs.createWriteStream(zipFilePath);\n          const request = httpLib.get(url, function (response) {\n            response.pipe(zipFile);\n            zipFile.on(\"finish\", () => {\n              console.log(\n                \"ImportedPlugin.importCommunityItemFromUrl - downloaded zip file\"\n              );\n              resolve(true);\n            });\n          });\n\n          request.on(\"error\", (error) => {\n            console.error(\n              \"ImportedPlugin.importCommunityItemFromUrl - error downloading zip file: \",\n              error\n            );\n            resolve(false);\n          });\n        } catch (error) {\n          console.error(\n            \"ImportedPlugin.importCommunityItemFromUrl - error downloading zip file: \",\n            error\n          );\n          resolve(false);\n        }\n      });\n\n      const success = await downloadZipFile;\n      if (!success)\n        return { success: false, error: \"Failed to download zip file.\" };\n\n      // Unzip the file to the plugin folder\n      // Note: https://github.com/cthackers/adm-zip?tab=readme-ov-file#electron-original-fs\n      const AdmZip = require(\"adm-zip\");\n      const zip = new AdmZip(zipFilePath);\n\n      // Validate all zip entries to prevent Zip Slip path traversal attacks (CWE-22)\n      for (const entry of zip.getEntries()) {\n        const entryPath = path.resolve(pluginFolder, entry.entryName);\n        if (!isWithin(pluginFolder, entryPath) && pluginFolder !== entryPath) {\n          throw new Error(\n            `[ImportedPlugin.importCommunityItemFromUrl]: Entry \"${entry.entryName}\" would extract outside plugin folder - not allowed.`\n          );\n        }\n      }\n\n      zip.extractAllTo(pluginFolder);\n\n      // We want to make sure specific keys are set to the proper values for\n      // plugin.json so we read and overwrite the file with the proper values.\n      const pluginJsonPath = path.resolve(pluginFolder, \"plugin.json\");\n      const pluginJson = safeJsonParse(fs.readFileSync(pluginJsonPath, \"utf8\"));\n      pluginJson.active = false;\n      pluginJson.hubId = hubId;\n      fs.writeFileSync(pluginJsonPath, JSON.stringify(pluginJson, null, 2));\n\n      console.log(\n        `ImportedPlugin.importCommunityItemFromUrl - successfully imported plugin to agent-skills/${hubId}`\n      );\n      return { success: true, error: null };\n    } catch (error) {\n      console.error(\n        \"ImportedPlugin.importCommunityItemFromUrl - error: \",\n        error\n      );\n      return { success: false, error: error.message };\n    } finally {\n      if (fs.existsSync(zipFilePath)) fs.unlinkSync(zipFilePath);\n    }\n  }\n}\n\nmodule.exports = ImportedPlugin;\n"
  },
  {
    "path": "server/utils/agents/index.js",
    "content": "const AIbitat = require(\"./aibitat\");\nconst AgentPlugins = require(\"./aibitat/plugins\");\nconst {\n  WorkspaceAgentInvocation,\n} = require(\"../../models/workspaceAgentInvocation\");\nconst { WorkspaceParsedFiles } = require(\"../../models/workspaceParsedFiles\");\nconst { User } = require(\"../../models/user\");\nconst { WorkspaceChats } = require(\"../../models/workspaceChats\");\nconst { safeJsonParse } = require(\"../http\");\nconst { USER_AGENT, WORKSPACE_AGENT } = require(\"./defaults\");\nconst ImportedPlugin = require(\"./imported\");\nconst { AgentFlows } = require(\"../agentFlows\");\nconst MCPCompatibilityLayer = require(\"../MCP\");\nconst { getAndClearInvocationAttachments } = require(\"../chats/agents\");\nconst { DocumentManager } = require(\"../DocumentManager\");\n\nclass AgentHandler {\n  #invocationUUID;\n  #funcsToLoad = [];\n  invocation = null;\n  aibitat = null;\n  channel = null;\n  provider = null;\n  model = null;\n  attachments = [];\n\n  constructor({ uuid }) {\n    this.#invocationUUID = uuid;\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[AgentHandler]\\x1b[0m ${text}`, ...args);\n  }\n\n  closeAlert() {\n    this.log(`End ${this.#invocationUUID}::${this.provider}:${this.model}`);\n  }\n\n  async #chatHistory(limit = 10) {\n    try {\n      const rawHistory = (\n        await WorkspaceChats.where(\n          {\n            workspaceId: this.invocation.workspace_id,\n            user_id: this.invocation.user_id || null,\n            thread_id: this.invocation.thread_id || null,\n            api_session_id: null,\n            include: true,\n          },\n          limit,\n          { id: \"desc\" }\n        )\n      ).reverse();\n\n      const agentHistory = [];\n      rawHistory.forEach((chatLog) => {\n        agentHistory.push(\n          {\n            from: USER_AGENT.name,\n            to: WORKSPACE_AGENT.name,\n            content: chatLog.prompt,\n            state: \"success\",\n          },\n          {\n            from: WORKSPACE_AGENT.name,\n            to: USER_AGENT.name,\n            content: safeJsonParse(chatLog.response)?.text || \"\",\n            state: \"success\",\n          }\n        );\n      });\n      return agentHistory;\n    } catch (e) {\n      this.log(\"Error loading chat history\", e.message);\n      return [];\n    }\n  }\n\n  checkSetup() {\n    switch (this.provider) {\n      case \"openai\":\n        if (!process.env.OPEN_AI_KEY)\n          throw new Error(\"OpenAI API key must be provided to use agents.\");\n        break;\n      case \"anthropic\":\n        if (!process.env.ANTHROPIC_API_KEY)\n          throw new Error(\"Anthropic API key must be provided to use agents.\");\n        break;\n      case \"lmstudio\":\n        if (!process.env.LMSTUDIO_BASE_PATH)\n          throw new Error(\"LMStudio base path must be provided to use agents.\");\n        break;\n      case \"ollama\":\n        if (!process.env.OLLAMA_BASE_PATH)\n          throw new Error(\"Ollama base path must be provided to use agents.\");\n        break;\n      case \"groq\":\n        if (!process.env.GROQ_API_KEY)\n          throw new Error(\"Groq API key must be provided to use agents.\");\n        break;\n      case \"togetherai\":\n        if (!process.env.TOGETHER_AI_API_KEY)\n          throw new Error(\"TogetherAI API key must be provided to use agents.\");\n        break;\n      case \"azure\":\n        if (!process.env.AZURE_OPENAI_ENDPOINT || !process.env.AZURE_OPENAI_KEY)\n          throw new Error(\n            \"Azure OpenAI API endpoint and key must be provided to use agents.\"\n          );\n        break;\n      case \"koboldcpp\":\n        if (!process.env.KOBOLD_CPP_BASE_PATH)\n          throw new Error(\n            \"KoboldCPP must have a valid base path to use for the api.\"\n          );\n        break;\n      case \"localai\":\n        if (!process.env.LOCAL_AI_BASE_PATH)\n          throw new Error(\n            \"LocalAI must have a valid base path to use for the api.\"\n          );\n        break;\n      case \"openrouter\":\n        if (!process.env.OPENROUTER_API_KEY)\n          throw new Error(\"OpenRouter API key must be provided to use agents.\");\n        break;\n      case \"mistral\":\n        if (!process.env.MISTRAL_API_KEY)\n          throw new Error(\"Mistral API key must be provided to use agents.\");\n        break;\n      case \"generic-openai\":\n        if (!process.env.GENERIC_OPEN_AI_BASE_PATH)\n          throw new Error(\"API base path must be provided to use agents.\");\n        break;\n      case \"perplexity\":\n        if (!process.env.PERPLEXITY_API_KEY)\n          throw new Error(\"Perplexity API key must be provided to use agents.\");\n        break;\n      case \"textgenwebui\":\n        if (!process.env.TEXT_GEN_WEB_UI_BASE_PATH)\n          throw new Error(\n            \"TextWebGenUI API base path must be provided to use agents.\"\n          );\n        break;\n      case \"bedrock\":\n        // No validations since there are many possible authentication methods\n        break;\n      case \"fireworksai\":\n        if (!process.env.FIREWORKS_AI_LLM_API_KEY)\n          throw new Error(\n            \"FireworksAI API Key must be provided to use agents.\"\n          );\n        break;\n      case \"deepseek\":\n        if (!process.env.DEEPSEEK_API_KEY)\n          throw new Error(\"DeepSeek API Key must be provided to use agents.\");\n        break;\n      case \"litellm\":\n        if (!process.env.LITE_LLM_BASE_PATH)\n          throw new Error(\n            \"LiteLLM API base path and key must be provided to use agents.\"\n          );\n        break;\n      case \"apipie\":\n        if (!process.env.APIPIE_LLM_API_KEY)\n          throw new Error(\"ApiPie API Key must be provided to use agents.\");\n        break;\n      case \"xai\":\n        if (!process.env.XAI_LLM_API_KEY)\n          throw new Error(\"xAI API Key must be provided to use agents.\");\n        break;\n      case \"zai\":\n        if (!process.env.ZAI_API_KEY)\n          throw new Error(\"Z.AI API Key must be provided to use agents.\");\n        break;\n      case \"novita\":\n        if (!process.env.NOVITA_LLM_API_KEY)\n          throw new Error(\"Novita API Key must be provided to use agents.\");\n        break;\n      case \"nvidia-nim\":\n        if (!process.env.NVIDIA_NIM_LLM_BASE_PATH)\n          throw new Error(\n            \"NVIDIA NIM base path must be provided to use agents.\"\n          );\n        break;\n      case \"ppio\":\n        if (!process.env.PPIO_API_KEY)\n          throw new Error(\"PPIO API Key must be provided to use agents.\");\n        break;\n      case \"gemini\":\n        if (!process.env.GEMINI_API_KEY)\n          throw new Error(\"Gemini API key must be provided to use agents.\");\n        break;\n      case \"dpais\":\n        if (!process.env.DPAIS_LLM_BASE_PATH)\n          throw new Error(\n            \"Dell Pro AI Studio base path must be provided to use agents.\"\n          );\n        if (!process.env.DPAIS_LLM_MODEL_PREF)\n          throw new Error(\n            \"Dell Pro AI Studio model must be set to use agents.\"\n          );\n        break;\n      case \"moonshotai\":\n        if (!process.env.MOONSHOT_AI_MODEL_PREF)\n          throw new Error(\"Moonshot AI model must be set to use agents.\");\n        break;\n      case \"cometapi\":\n        if (!process.env.COMETAPI_LLM_API_KEY)\n          throw new Error(\"CometAPI API Key must be provided to use agents.\");\n        break;\n      case \"foundry\":\n        if (!process.env.FOUNDRY_BASE_PATH)\n          throw new Error(\"Foundry base path must be provided to use agents.\");\n        break;\n      case \"giteeai\":\n        if (!process.env.GITEE_AI_API_KEY)\n          throw new Error(\"GiteeAI API Key must be provided to use agents.\");\n        break;\n      case \"cohere\":\n        if (!process.env.COHERE_API_KEY)\n          throw new Error(\"Cohere API key must be provided to use agents.\");\n        break;\n      case \"docker-model-runner\":\n        if (!process.env.DOCKER_MODEL_RUNNER_BASE_PATH)\n          throw new Error(\n            \"Docker Model Runner base path must be provided to use agents.\"\n          );\n        break;\n      case \"privatemode\":\n        if (!process.env.PRIVATEMODE_LLM_BASE_PATH)\n          throw new Error(\n            \"Privatemode base path must be provided to use agents.\"\n          );\n        break;\n      case \"sambanova\":\n        if (!process.env.SAMBANOVA_LLM_API_KEY)\n          throw new Error(\"SambaNova API key must be provided to use agents.\");\n        break;\n      case \"lemonade\":\n        if (!process.env.LEMONADE_LLM_BASE_PATH)\n          throw new Error(\"Lemonade base path must be provided to use agents.\");\n        break;\n      default:\n        throw new Error(\n          \"No workspace agent provider set. Please set your agent provider in the workspace's settings\"\n        );\n    }\n  }\n\n  /**\n   * Finds the default model for a given provider. If no default model is set for it's associated ENV then\n   * it will return a reasonable base model for the provider if one exists.\n   * @param {string} provider - The provider to find the default model for.\n   * @returns {string|null} The default model for the provider.\n   */\n  providerDefault(provider = this.provider) {\n    switch (provider) {\n      case \"openai\":\n        return process.env.OPEN_MODEL_PREF ?? \"gpt-4o\";\n      case \"anthropic\":\n        return process.env.ANTHROPIC_MODEL_PREF ?? \"claude-3-sonnet-20240229\";\n      case \"lmstudio\":\n        return process.env.LMSTUDIO_MODEL_PREF ?? null;\n      case \"ollama\":\n        return process.env.OLLAMA_MODEL_PREF ?? \"llama3:latest\";\n      case \"groq\":\n        return process.env.GROQ_MODEL_PREF ?? \"llama3-70b-8192\";\n      case \"togetherai\":\n        return (\n          process.env.TOGETHER_AI_MODEL_PREF ??\n          \"mistralai/Mixtral-8x7B-Instruct-v0.1\"\n        );\n      case \"azure\":\n        return (\n          process.env.AZURE_OPENAI_MODEL_PREF || process.env.OPEN_MODEL_PREF\n        );\n      case \"koboldcpp\":\n        return process.env.KOBOLD_CPP_MODEL_PREF ?? null;\n      case \"localai\":\n        return process.env.LOCAL_AI_MODEL_PREF ?? null;\n      case \"openrouter\":\n        return process.env.OPENROUTER_MODEL_PREF ?? \"openrouter/auto\";\n      case \"mistral\":\n        return process.env.MISTRAL_MODEL_PREF ?? \"mistral-medium\";\n      case \"generic-openai\":\n        return process.env.GENERIC_OPEN_AI_MODEL_PREF ?? null;\n      case \"perplexity\":\n        return process.env.PERPLEXITY_MODEL_PREF ?? \"sonar-small-online\";\n      case \"textgenwebui\":\n        return \"text-generation-webui\";\n      case \"bedrock\":\n        return process.env.AWS_BEDROCK_LLM_MODEL_PREFERENCE ?? null;\n      case \"fireworksai\":\n        return process.env.FIREWORKS_AI_LLM_MODEL_PREF ?? null;\n      case \"deepseek\":\n        return process.env.DEEPSEEK_MODEL_PREF ?? \"deepseek-chat\";\n      case \"litellm\":\n        return process.env.LITE_LLM_MODEL_PREF ?? null;\n      case \"moonshotai\":\n        return process.env.MOONSHOT_AI_MODEL_PREF ?? \"moonshot-v1-32k\";\n      case \"apipie\":\n        return process.env.APIPIE_LLM_MODEL_PREF ?? null;\n      case \"xai\":\n        return process.env.XAI_LLM_MODEL_PREF ?? \"grok-beta\";\n      case \"zai\":\n        return process.env.ZAI_MODEL_PREF ?? \"glm-4.5\";\n      case \"novita\":\n        return process.env.NOVITA_LLM_MODEL_PREF ?? \"deepseek/deepseek-r1\";\n      case \"nvidia-nim\":\n        return process.env.NVIDIA_NIM_LLM_MODEL_PREF ?? null;\n      case \"ppio\":\n        return process.env.PPIO_MODEL_PREF ?? \"qwen/qwen2.5-32b-instruct\";\n      case \"gemini\":\n        return process.env.GEMINI_LLM_MODEL_PREF ?? \"gemini-2.0-flash-lite\";\n      case \"dpais\":\n        return process.env.DPAIS_LLM_MODEL_PREF;\n      case \"cometapi\":\n        return process.env.COMETAPI_LLM_MODEL_PREF ?? \"gpt-5-mini\";\n      case \"foundry\":\n        return process.env.FOUNDRY_MODEL_PREF ?? null;\n      case \"giteeai\":\n        return process.env.GITEE_AI_MODEL_PREF ?? null;\n      case \"cohere\":\n        return process.env.COHERE_MODEL_PREF ?? \"command-r-08-2024\";\n      case \"docker-model-runner\":\n        return process.env.DOCKER_MODEL_RUNNER_LLM_MODEL_PREF ?? null;\n      case \"privatemode\":\n        return process.env.PRIVATEMODE_LLM_MODEL_PREF ?? null;\n      case \"sambanova\":\n        return process.env.SAMBANOVA_LLM_MODEL_PREF ?? null;\n      case \"lemonade\":\n        return process.env.LEMONADE_LLM_MODEL_PREF ?? null;\n      default:\n        return null;\n    }\n  }\n\n  /**\n   * Attempts to find a fallback provider and model to use if the workspace\n   * does not have an explicit `agentProvider` and `agentModel` set.\n   * 1. Fallback to the workspace `chatProvider` and `chatModel` if they exist.\n   * 2. Fallback to the system `LLM_PROVIDER` and try to load the associated default model via ENV params or a base available model.\n   * 3. Otherwise, return null - will likely throw an error the user can act on.\n   * @returns {object|null} - An object with provider and model keys.\n   */\n  #getFallbackProvider() {\n    // First, fallback to the workspace chat provider and model if they exist\n    if (\n      this.invocation.workspace.chatProvider &&\n      this.invocation.workspace.chatModel\n    ) {\n      return {\n        provider: this.invocation.workspace.chatProvider,\n        model: this.invocation.workspace.chatModel,\n      };\n    }\n\n    // If workspace does not have chat provider and model fallback\n    // to system provider and try to load provider default model\n    const systemProvider = process.env.LLM_PROVIDER;\n    const systemModel = this.providerDefault(systemProvider);\n    if (systemProvider && systemModel) {\n      return {\n        provider: systemProvider,\n        model: systemModel,\n      };\n    }\n\n    return null;\n  }\n\n  /**\n   * Finds or assumes the model preference value to use for API calls.\n   * If multi-model loading is supported, we use their agent model selection of the workspace\n   * If not supported, we attempt to fallback to the system provider value for the LLM preference\n   * and if that fails - we assume a reasonable base model to exist.\n   * @returns {string|null} the model preference value to use in API calls\n   */\n  #fetchModel() {\n    // Provider was not explicitly set for workspace, so we are going to run our fallback logic\n    // that will set a provider and model for us to use.\n    if (!this.provider) {\n      const fallback = this.#getFallbackProvider();\n      if (!fallback) throw new Error(\"No valid provider found for the agent.\");\n      this.provider = fallback.provider; // re-set the provider to the fallback provider so it is not null.\n      return fallback.model; // set its defined model based on fallback logic.\n    }\n\n    // The provider was explicitly set, so check if the workspace has an agent model set.\n    if (this.invocation.workspace.agentModel)\n      return this.invocation.workspace.agentModel;\n\n    // Otherwise, we have no model to use - so guess a default model to use via the provider\n    // and it's system ENV params and if that fails - we return either a base model or null.\n    return this.providerDefault();\n  }\n\n  #providerSetupAndCheck() {\n    this.provider = this.invocation.workspace.agentProvider ?? null; // set provider to workspace agent provider if it exists\n    this.model = this.#fetchModel();\n\n    if (!this.provider)\n      throw new Error(\"No valid provider found for the agent.\");\n    this.log(`Start ${this.#invocationUUID}::${this.provider}:${this.model}`);\n    this.checkSetup();\n  }\n\n  async #validInvocation() {\n    const invocation = await WorkspaceAgentInvocation.getWithWorkspace({\n      uuid: String(this.#invocationUUID),\n    });\n    if (invocation?.closed)\n      throw new Error(\"This agent invocation is already closed\");\n    this.invocation = invocation ?? null;\n  }\n\n  parseCallOptions(args, config = {}, pluginName) {\n    const callOpts = {};\n    for (const [param, definition] of Object.entries(config)) {\n      if (\n        definition.required &&\n        (!Object.prototype.hasOwnProperty.call(args, param) ||\n          args[param] === null)\n      ) {\n        this.log(\n          `'${param}' required parameter for '${pluginName}' plugin is missing. Plugin may not function or crash agent.`\n        );\n        continue;\n      }\n      callOpts[param] = Object.prototype.hasOwnProperty.call(args, param)\n        ? args[param]\n        : definition.default || null;\n    }\n    return callOpts;\n  }\n\n  async #attachPlugins(args) {\n    for (const name of this.#funcsToLoad) {\n      // Load child plugin\n      if (name.includes(\"#\")) {\n        const [parent, childPluginName] = name.split(\"#\");\n        if (!Object.prototype.hasOwnProperty.call(AgentPlugins, parent)) {\n          this.log(\n            `${parent} is not a valid plugin. Skipping inclusion to agent cluster.`\n          );\n          continue;\n        }\n\n        const childPlugin = AgentPlugins[parent].plugin.find(\n          (child) => child.name === childPluginName\n        );\n        if (!childPlugin) {\n          this.log(\n            `${parent} does not have child plugin named ${childPluginName}. Skipping inclusion to agent cluster.`\n          );\n          continue;\n        }\n\n        const callOpts = this.parseCallOptions(\n          args,\n          childPlugin?.startupConfig?.params,\n          name\n        );\n        this.aibitat.use(childPlugin.plugin(callOpts));\n        this.log(\n          `Attached ${parent}:${childPluginName} plugin to Agent cluster`\n        );\n        continue;\n      }\n\n      // Load flow plugin. This is marked by `@@flow_` in the array of functions to load.\n      // Replace the @@flow_ placeholder in the agent's function list with the actual\n      // tool name so the function lookup in reply() can find it.\n      if (name.startsWith(\"@@flow_\")) {\n        const uuid = name.replace(\"@@flow_\", \"\");\n        const plugin = AgentFlows.loadFlowPlugin(uuid, this.aibitat);\n        if (!plugin) {\n          this.log(\n            `Flow ${uuid} not found in flows directory. Skipping inclusion to agent cluster.`\n          );\n          continue;\n        }\n\n        this.aibitat.agents.get(\"@agent\").functions = this.aibitat.agents\n          .get(\"@agent\")\n          .functions.filter((f) => f !== name);\n        this.aibitat.agents.get(\"@agent\").functions.push(plugin.name);\n\n        this.aibitat.use(plugin.plugin());\n        this.log(\n          `Attached flow ${plugin.name} (${plugin.flowName}) plugin to Agent cluster`\n        );\n        continue;\n      }\n\n      // Load MCP plugin. This is marked by `@@mcp_` in the array of functions to load.\n      // All sub-tools are loaded here and are denoted by `pluginName:toolName` as their identifier.\n      // This will replace the parent MCP server plugin with the sub-tools as child plugins so they\n      // can be called directly by the agent when invoked.\n      // Since to get to this point, the `activeMCPServers` method has already been called, we can\n      // safely assume that the MCP server is running and the tools are available/loaded.\n      if (name.startsWith(\"@@mcp_\")) {\n        const mcpPluginName = name.replace(\"@@mcp_\", \"\");\n        const plugins =\n          await new MCPCompatibilityLayer().convertServerToolsToPlugins(\n            mcpPluginName,\n            this.aibitat\n          );\n        if (!plugins) {\n          this.log(\n            `MCP ${mcpPluginName} not found in MCP server config. Skipping inclusion to agent cluster.`\n          );\n          continue;\n        }\n\n        // Remove the old function from the agent functions directly\n        // and push the new ones onto the end of the array so that they are loaded properly.\n        this.aibitat.agents.get(\"@agent\").functions = this.aibitat.agents\n          .get(\"@agent\")\n          .functions.filter((f) => f.name !== name);\n        for (const plugin of plugins)\n          this.aibitat.agents.get(\"@agent\").functions.push(plugin.name);\n\n        plugins.forEach((plugin) => {\n          this.aibitat.use(plugin.plugin());\n          this.log(\n            `Attached MCP::${plugin.toolName} MCP tool to Agent cluster`\n          );\n        });\n        continue;\n      }\n\n      // Load imported plugin. This is marked by `@@` in the array of functions to load.\n      // and is the @@hubID of the plugin.\n      if (name.startsWith(\"@@\")) {\n        const hubId = name.replace(\"@@\", \"\");\n        const valid = ImportedPlugin.validateImportedPluginHandler(hubId);\n        if (!valid) {\n          this.log(\n            `Imported plugin by hubId ${hubId} not found in plugin directory. Skipping inclusion to agent cluster.`\n          );\n          continue;\n        }\n\n        const plugin = ImportedPlugin.loadPluginByHubId(hubId);\n        const callOpts = plugin.parseCallOptions();\n        this.aibitat.use(plugin.plugin(callOpts));\n        this.log(\n          `Attached ${plugin.name} (${hubId}) imported plugin to Agent cluster`\n        );\n        continue;\n      }\n\n      // Load single-stage plugin.\n      if (!Object.prototype.hasOwnProperty.call(AgentPlugins, name)) {\n        this.log(\n          `${name} is not a valid plugin. Skipping inclusion to agent cluster.`\n        );\n        continue;\n      }\n\n      const callOpts = this.parseCallOptions(\n        args,\n        AgentPlugins[name].startupConfig.params\n      );\n      const AIbitatPlugin = AgentPlugins[name];\n      this.aibitat.use(AIbitatPlugin.plugin(callOpts));\n      this.log(`Attached ${name} plugin to Agent cluster`);\n    }\n  }\n\n  async #loadAgents() {\n    // Default User agent and workspace agent\n    this.log(`Attaching user and default agent to Agent cluster.`);\n    const user = this.invocation.user_id\n      ? await User.get({ id: Number(this.invocation.user_id) })\n      : null;\n    const userAgentDef = await USER_AGENT.getDefinition();\n    const workspaceAgentDef = await WORKSPACE_AGENT.getDefinition(\n      this.provider,\n      this.invocation.workspace,\n      user\n    );\n\n    this.aibitat.agent(USER_AGENT.name, userAgentDef);\n    this.aibitat.agent(WORKSPACE_AGENT.name, workspaceAgentDef);\n    this.#funcsToLoad = [\n      ...(userAgentDef?.functions || []),\n      ...(workspaceAgentDef?.functions || []),\n    ];\n  }\n\n  async init() {\n    await this.#validInvocation();\n    this.#providerSetupAndCheck();\n\n    // Retrieve cached attachments (images, etc.) from the HTTP request\n    this.attachments = getAndClearInvocationAttachments(this.#invocationUUID);\n\n    return this;\n  }\n\n  /**\n   * Fetch fresh parsed files and pinned documents, format them for injection into user messages.\n   * Called on every chat turn to ensure context is always up-to-date.\n   * @returns {Promise<string>} Formatted context string to append to user message\n   */\n  async #fetchParsedFileContext() {\n    const user = this.invocation.user_id\n      ? { id: this.invocation.user_id }\n      : null;\n    const thread = this.invocation.thread_id\n      ? { id: this.invocation.thread_id }\n      : null;\n    const documentManager = new DocumentManager({\n      workspace: this.invocation.workspace,\n    });\n\n    return Promise.all([\n      WorkspaceParsedFiles.getContextFiles(\n        this.invocation.workspace,\n        thread,\n        user\n      ),\n      documentManager.pinnedDocs(),\n    ])\n      .then(([parsedFiles, pinnedDocs]) => {\n        const allDocuments = [\n          ...(parsedFiles || []).map((doc) => ({\n            name: doc.title || \"Uploaded Document\",\n            content: doc.pageContent,\n          })),\n          ...(pinnedDocs || []).map((doc) => ({\n            name: doc.title || doc.metadata?.title || \"Pinned Document\",\n            content: doc.pageContent,\n          })),\n        ];\n\n        if (allDocuments.length === 0) return \"\";\n        if (parsedFiles?.length > 0)\n          this.log(\n            `Injecting ${parsedFiles.length} parsed file(s) into user message`\n          );\n        if (pinnedDocs?.length > 0)\n          this.log(\n            `Injecting ${pinnedDocs.length} pinned document(s) into user message`\n          );\n\n        return (\n          \"\\n\\n<attached_documents>\\n\" +\n          allDocuments\n            .map((doc, i) => {\n              const filename = doc.name || `Document ${i + 1}`;\n              return `<document name=\"${filename}\">\\n${doc.content}\\n</document>`;\n            })\n            .join(\"\\n\") +\n          \"\\n</attached_documents>\"\n        );\n      })\n      .catch((e) => {\n        this.log(\"Error fetching parsed file context\", e.message);\n        return \"\";\n      });\n  }\n\n  async createAIbitat(\n    args = {\n      socket: null,\n    }\n  ) {\n    this.aibitat = new AIbitat({\n      provider: this.provider ?? \"openai\",\n      model: this.model ?? \"gpt-4o\",\n      chats: await this.#chatHistory(20),\n      handlerProps: {\n        invocation: this.invocation,\n        log: this.log,\n      },\n    });\n\n    // Register callback to fetch fresh parsed file context on each chat turn\n    // This injects parsed files into user messages instead of system prompt\n    this.aibitat.fetchParsedFileContext = () => this.#fetchParsedFileContext();\n\n    // Attach standard websocket plugin for frontend communication.\n    this.log(`Attached ${AgentPlugins.websocket.name} plugin to Agent cluster`);\n    this.aibitat.use(\n      AgentPlugins.websocket.plugin({\n        socket: args.socket,\n        muteUserReply: true,\n        introspection: true,\n      })\n    );\n\n    // Attach standard chat-history plugin for message storage.\n    this.log(\n      `Attached ${AgentPlugins.chatHistory.name} plugin to Agent cluster`\n    );\n    this.aibitat.use(AgentPlugins.chatHistory.plugin());\n\n    // Load required agents (Default + custom)\n    await this.#loadAgents();\n\n    // Attach all required plugins for functions to operate.\n    await this.#attachPlugins(args);\n  }\n\n  /**\n   * Strip the @agent command from the message if it exists.\n   * Prevents hallucination by the agent when the @agent command is used from the model thinking\n   * it is an agent or something itself.\n   * If the user sent nothing after the @agent command - assume its a greeting.\n   * @param {string} message - The message to strip the @agent command from.\n   * @returns {string} The message with the @agent command stripped.\n   */\n  #stripAgentCommand(message = \"\") {\n    const stripped = String(message)\n      .replace(/^@agent\\s*/, \"\")\n      .trim();\n    if (!stripped) return \"Hello!\";\n    return stripped;\n  }\n\n  startAgentCluster() {\n    return this.aibitat.start({\n      from: USER_AGENT.name,\n      to: this.channel ?? WORKSPACE_AGENT.name,\n      content: this.#stripAgentCommand(this.invocation.prompt),\n      attachments: this.attachments,\n    });\n  }\n}\n\nmodule.exports.AgentHandler = AgentHandler;\n"
  },
  {
    "path": "server/utils/boot/MetaGenerator.js",
    "content": "/**\n * @typedef MetaTagDefinition\n * @property {('link'|'meta')} tag - the type of meta tag element\n * @property {{string:string}|null} props - the inner key/values of a meta tag\n * @property {string|null} content - Text content to be injected between tags. If null self-closing.\n */\n\n/**\n * This class serves the default index.html page that is not present when built in production.\n * and therefore this class should not be called when in development mode since it is unused.\n * All this class does is basically emulate SSR for the meta-tag generation of the root index page.\n * Since we are an SPA, we can just render the primary page and the known entrypoints for the index.{js,css}\n * we can always start at the right place and dynamically load in lazy-loaded as we typically normally would\n * and we dont have any of the overhead that would normally come with having the rewrite the whole app in next or something.\n * Lastly, this class is singleton, so once instantiate the same reference is shared for as long as the server is alive.\n * the main function is `.generate()` which will return the index HTML. These settings are stored in the #customConfig\n * static property and will not be reloaded until the page is loaded AND #customConfig is explicitly null. So anytime a setting\n * for meta-props is updated you should get this singleton class and call `.clearConfig` so the next page load will show the new props.\n */\nclass MetaGenerator {\n  name = \"MetaGenerator\";\n\n  /** @type {MetaGenerator|null} */\n  static _instance = null;\n\n  /** @type {MetaTagDefinition[]|null} */\n  #customConfig = null;\n\n  #defaultManifest = {\n    name: \"AnythingLLM\",\n    short_name: \"AnythingLLM\",\n    display: \"standalone\",\n    orientation: \"portrait\",\n    start_url: \"/\",\n    icons: [\n      {\n        src: \"/favicon.png\",\n        sizes: \"any\",\n      },\n    ],\n  };\n\n  constructor() {\n    if (MetaGenerator._instance) return MetaGenerator._instance;\n    MetaGenerator._instance = this;\n  }\n\n  #log(text, ...args) {\n    console.log(`\\x1b[36m[${this.name}]\\x1b[0m ${text}`, ...args);\n  }\n\n  #defaultMeta() {\n    return [\n      {\n        tag: \"link\",\n        props: { type: \"image/svg+xml\", href: \"/favicon.png\" },\n        content: null,\n      },\n      {\n        tag: \"title\",\n        props: null,\n        content: \"AnythingLLM | Your personal LLM trained on anything\",\n      },\n\n      {\n        tag: \"meta\",\n        props: {\n          name: \"title\",\n          content: \"AnythingLLM | Your personal LLM trained on anything\",\n        },\n      },\n      {\n        tag: \"meta\",\n        props: {\n          description: \"title\",\n          content: \"AnythingLLM | Your personal LLM trained on anything\",\n        },\n      },\n\n      // <!-- Facebook -->\n      { tag: \"meta\", props: { property: \"og:type\", content: \"website\" } },\n      {\n        tag: \"meta\",\n        props: { property: \"og:url\", content: \"https://anythingllm.com\" },\n      },\n      {\n        tag: \"meta\",\n        props: {\n          property: \"og:title\",\n          content: \"AnythingLLM | Your personal LLM trained on anything\",\n        },\n      },\n      {\n        tag: \"meta\",\n        props: {\n          property: \"og:description\",\n          content: \"AnythingLLM | Your personal LLM trained on anything\",\n        },\n      },\n      {\n        tag: \"meta\",\n        props: {\n          property: \"og:image\",\n          content:\n            \"https://raw.githubusercontent.com/Mintplex-Labs/anything-llm/master/images/promo.png\",\n        },\n      },\n\n      // <!-- Twitter -->\n      {\n        tag: \"meta\",\n        props: { property: \"twitter:card\", content: \"summary_large_image\" },\n      },\n      {\n        tag: \"meta\",\n        props: { property: \"twitter:url\", content: \"https://anythingllm.com\" },\n      },\n      {\n        tag: \"meta\",\n        props: {\n          property: \"twitter:title\",\n          content: \"AnythingLLM | Your personal LLM trained on anything\",\n        },\n      },\n      {\n        tag: \"meta\",\n        props: {\n          property: \"twitter:description\",\n          content: \"AnythingLLM | Your personal LLM trained on anything\",\n        },\n      },\n      {\n        tag: \"meta\",\n        props: {\n          property: \"twitter:image\",\n          content:\n            \"https://raw.githubusercontent.com/Mintplex-Labs/anything-llm/master/images/promo.png\",\n        },\n      },\n\n      { tag: \"link\", props: { rel: \"icon\", href: \"/favicon.png\" } },\n      { tag: \"link\", props: { rel: \"apple-touch-icon\", href: \"/favicon.png\" } },\n\n      // PWA specific tags\n      {\n        tag: \"meta\",\n        props: { name: \"mobile-web-app-capable\", content: \"yes\" },\n      },\n      {\n        tag: \"meta\",\n        props: { name: \"apple-mobile-web-app-capable\", content: \"yes\" },\n      },\n      {\n        tag: \"meta\",\n        props: {\n          name: \"apple-mobile-web-app-status-bar-style\",\n          content: \"black-translucent\",\n        },\n      },\n      { tag: \"link\", props: { rel: \"manifest\", href: \"/manifest.json\" } },\n    ];\n  }\n\n  /**\n   * Assembles Meta tags as one large string\n   * @param {MetaTagDefinition[]} tagArray\n   * @returns {string}\n   */\n  #assembleMeta() {\n    const output = [];\n    for (const tag of this.#customConfig) {\n      let htmlString;\n      htmlString = `<${tag.tag} `;\n\n      if (tag.props !== null) {\n        for (const [key, value] of Object.entries(tag.props))\n          htmlString += `${key}=\"${value}\" `;\n      }\n\n      if (tag.content) {\n        htmlString += `>${tag.content}</${tag.tag}>`;\n      } else {\n        htmlString += `>`;\n      }\n      output.push(htmlString);\n    }\n    return output.join(\"\\n\");\n  }\n\n  #validUrl(faviconUrl = null) {\n    if (faviconUrl === null) return \"/favicon.png\";\n    try {\n      const url = new URL(faviconUrl);\n      return url.toString();\n    } catch {\n      return \"/favicon.png\";\n    }\n  }\n\n  async #fetchConfg() {\n    this.#log(`fetching custom meta tag settings...`);\n    const { SystemSettings } = require(\"../../models/systemSettings\");\n    const customTitle = await SystemSettings.getValueOrFallback(\n      { label: \"meta_page_title\" },\n      null\n    );\n    const faviconURL = await SystemSettings.getValueOrFallback(\n      { label: \"meta_page_favicon\" },\n      null\n    );\n\n    // If nothing defined - assume defaults.\n    if (customTitle === null && faviconURL === null) {\n      this.#customConfig = this.#defaultMeta();\n    } else {\n      // When custom settings exist, include all default meta tags but override specific ones\n      this.#customConfig = this.#defaultMeta().map((tag) => {\n        // Override favicon link\n        if (tag.tag === \"link\" && tag.props?.rel === \"icon\") {\n          return {\n            tag: \"link\",\n            props: { rel: \"icon\", href: this.#validUrl(faviconURL) },\n          };\n        }\n        // Override page title\n        if (tag.tag === \"title\") {\n          return {\n            tag: \"title\",\n            props: null,\n            content:\n              customTitle ??\n              \"AnythingLLM | Your personal LLM trained on anything\",\n          };\n        }\n        // Override meta title\n        if (tag.tag === \"meta\" && tag.props?.name === \"title\") {\n          return {\n            tag: \"meta\",\n            props: {\n              name: \"title\",\n              content:\n                customTitle ??\n                \"AnythingLLM | Your personal LLM trained on anything\",\n            },\n          };\n        }\n        // Override og:title\n        if (tag.tag === \"meta\" && tag.props?.property === \"og:title\") {\n          return {\n            tag: \"meta\",\n            props: {\n              property: \"og:title\",\n              content:\n                customTitle ??\n                \"AnythingLLM | Your personal LLM trained on anything\",\n            },\n          };\n        }\n        // Override twitter:title\n        if (tag.tag === \"meta\" && tag.props?.property === \"twitter:title\") {\n          return {\n            tag: \"meta\",\n            props: {\n              property: \"twitter:title\",\n              content:\n                customTitle ??\n                \"AnythingLLM | Your personal LLM trained on anything\",\n            },\n          };\n        }\n        // Override apple-touch-icon if custom favicon is set\n        if (\n          tag.tag === \"link\" &&\n          tag.props?.rel === \"apple-touch-icon\" &&\n          faviconURL\n        ) {\n          return {\n            tag: \"link\",\n            props: {\n              rel: \"apple-touch-icon\",\n              href: this.#validUrl(faviconURL),\n            },\n          };\n        }\n        // Return original tag for everything else (including PWA tags)\n        return tag;\n      });\n    }\n\n    return this.#customConfig;\n  }\n\n  /**\n   * Clears the current config so it can be refetched on the server for next render.\n   */\n  clearConfig() {\n    this.#customConfig = null;\n  }\n\n  /**\n   *\n   * @param {import('express').Response} response\n   * @param {number} code\n   */\n  async generate(response, code = 200) {\n    if (this.#customConfig === null) await this.#fetchConfg();\n    response.status(code).send(`\n       <!DOCTYPE html>\n        <html lang=\"en\">\n          <head>\n            <meta charset=\"UTF-8\" />\n            <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n            ${this.#assembleMeta()}\n            <script type=\"module\" crossorigin src=\"/index.js\"></script>\n            <link rel=\"stylesheet\" href=\"/index.css\">\n          </head>\n          <body>\n            <div id=\"root\" class=\"h-screen\"></div>\n          </body>\n        </html>`);\n  }\n\n  /**\n   * Generates the manifest.json file for the PWA application on the fly.\n   * @param {import('express').Response} response\n   * @param {number} code\n   */\n  async generateManifest(response) {\n    try {\n      const { SystemSettings } = require(\"../../models/systemSettings\");\n      const manifestName = await SystemSettings.getValueOrFallback(\n        { label: \"meta_page_title\" },\n        \"AnythingLLM\"\n      );\n      const faviconURL = await SystemSettings.getValueOrFallback(\n        { label: \"meta_page_favicon\" },\n        null\n      );\n\n      let iconUrl = \"/favicon.png\";\n      if (faviconURL) {\n        try {\n          new URL(faviconURL);\n          iconUrl = faviconURL;\n        } catch {\n          iconUrl = \"/favicon.png\";\n        }\n      }\n\n      const manifest = {\n        name: manifestName,\n        short_name: manifestName,\n        display: \"standalone\",\n        orientation: \"portrait\",\n        start_url: \"/\",\n        icons: [\n          {\n            src: iconUrl,\n            sizes: \"any\",\n          },\n        ],\n      };\n\n      response.type(\"application/json\").status(200).send(manifest).end();\n    } catch (error) {\n      this.#log(`error generating manifest: ${error.message}`, error);\n      response\n        .type(\"application/json\")\n        .status(200)\n        .send(this.#defaultManifest)\n        .end();\n    }\n  }\n}\n\nmodule.exports.MetaGenerator = MetaGenerator;\n"
  },
  {
    "path": "server/utils/boot/eagerLoadContextWindows.js",
    "content": "/**\n * Eagerly load the context windows for the current provider.\n * This is done to ensure that the context windows are pre-cached when the server boots.\n *\n * This prevents us from having misreporting of the context window before a chat is ever sent.\n * eg: when viewing the attachments in the workspace - the context window would be misreported if a chat\n * has not been sent yet.\n */\nasync function eagerLoadContextWindows() {\n  const currentProvider = process.env.LLM_PROVIDER;\n\n  const log = (provider) => {\n    console.log(`⚡\\x1b[32mPre-cached context windows for ${provider}\\x1b[0m`);\n  };\n\n  switch (currentProvider) {\n    case \"lmstudio\":\n      const { LMStudioLLM } = require(\"../AiProviders/lmStudio\");\n      await LMStudioLLM.cacheContextWindows(true);\n      log(\"LMStudio\");\n      break;\n    case \"ollama\":\n      const { OllamaAILLM } = require(\"../AiProviders/ollama\");\n      await OllamaAILLM.cacheContextWindows(true);\n      log(\"Ollama\");\n      break;\n    case \"foundry\":\n      const { FoundryLLM } = require(\"../AiProviders/foundry\");\n      await FoundryLLM.cacheContextWindows(true);\n      log(\"Foundry\");\n      break;\n  }\n}\n\nmodule.exports = eagerLoadContextWindows;\n"
  },
  {
    "path": "server/utils/boot/index.js",
    "content": "const { Telemetry } = require(\"../../models/telemetry\");\nconst { BackgroundService } = require(\"../BackgroundWorkers\");\nconst { EncryptionManager } = require(\"../EncryptionManager\");\nconst { CommunicationKey } = require(\"../comKey\");\nconst setupTelemetry = require(\"../telemetry\");\nconst eagerLoadContextWindows = require(\"./eagerLoadContextWindows\");\nconst markOnboarded = require(\"./markOnboarded\");\nconst { PushNotifications } = require(\"../PushNotifications\");\n\n// Testing SSL? You can make a self signed certificate and point the ENVs to that location\n// make a directory in server called 'sslcert' - cd into it\n// - openssl genrsa -aes256 -passout pass:gsahdg -out server.pass.key 4096\n// - openssl rsa -passin pass:gsahdg -in server.pass.key -out server.key\n// - rm server.pass.key\n// - openssl req -new -key server.key -out server.csr\n// Update .env keys with the correct values and boot. These are temporary and not real SSL certs - only use for local.\n// Test with https://localhost:3001/api/ping\n// build and copy frontend to server/public with correct API_BASE and start server in prod model and all should be ok\nfunction bootSSL(app, port = 3001) {\n  try {\n    console.log(\n      `\\x1b[33m[SSL BOOT ENABLED]\\x1b[0m Loading the certificate and key for HTTPS mode...`\n    );\n    const fs = require(\"fs\");\n    const https = require(\"https\");\n    const privateKey = fs.readFileSync(process.env.HTTPS_KEY_PATH);\n    const certificate = fs.readFileSync(process.env.HTTPS_CERT_PATH);\n    const credentials = { key: privateKey, cert: certificate };\n    const server = https.createServer(credentials, app);\n\n    server\n      .listen(port, async () => {\n        await markOnboarded();\n        await setupTelemetry();\n        new CommunicationKey(true);\n        new EncryptionManager();\n        new BackgroundService().boot();\n        await eagerLoadContextWindows();\n        await PushNotifications.setupPushNotificationService();\n        console.log(`Primary server in HTTPS mode listening on port ${port}`);\n      })\n      .on(\"error\", catchSigTerms);\n\n    require(\"@mintplex-labs/express-ws\").default(app, server);\n    return { app, server };\n  } catch (e) {\n    console.error(\n      `\\x1b[31m[SSL BOOT FAILED]\\x1b[0m ${e.message} - falling back to HTTP boot.`,\n      {\n        ENABLE_HTTPS: process.env.ENABLE_HTTPS,\n        HTTPS_KEY_PATH: process.env.HTTPS_KEY_PATH,\n        HTTPS_CERT_PATH: process.env.HTTPS_CERT_PATH,\n        stacktrace: e.stack,\n      }\n    );\n    return bootHTTP(app, port);\n  }\n}\n\nfunction bootHTTP(app, port = 3001) {\n  if (!app) throw new Error('No \"app\" defined - crashing!');\n\n  app\n    .listen(port, async () => {\n      await markOnboarded();\n      await setupTelemetry();\n      new CommunicationKey(true);\n      new EncryptionManager();\n      new BackgroundService().boot();\n      await eagerLoadContextWindows();\n      await PushNotifications.setupPushNotificationService();\n      console.log(`Primary server in HTTP mode listening on port ${port}`);\n    })\n    .on(\"error\", catchSigTerms);\n\n  return { app, server: null };\n}\n\nfunction catchSigTerms() {\n  process.once(\"SIGUSR2\", function () {\n    Telemetry.flush();\n    process.kill(process.pid, \"SIGUSR2\");\n  });\n  process.on(\"SIGINT\", function () {\n    Telemetry.flush();\n    process.kill(process.pid, \"SIGINT\");\n  });\n}\n\nmodule.exports = {\n  bootHTTP,\n  bootSSL,\n};\n"
  },
  {
    "path": "server/utils/boot/markOnboarded.js",
    "content": "const { SystemSettings } = require(\"../../models/systemSettings\");\n\n/**\n * Mark the onboarding as completed for legacy users prior to this change where onboarding is now a flag in the DB.\n * This is a legacy patch to ensure that existing users are not redirected to the onboarding page who have been using the app for a while.\n */\nasync function markOnboarded() {\n  try {\n    const onboardingStatus = await SystemSettings.isOnboardingComplete();\n    if (onboardingStatus === true) return;\n\n    // Check if the server is already onboarded by the old way of checking if the server in any way has been setup.\n    // If it is, then we can mark the onboarding as complete in the DB to persist this\n    const alreadyOnboarded = await isLegacyOnboarded();\n    if (alreadyOnboarded === true) {\n      console.log(\n        \"\\x1b[33m[ONBOARDING PATCH]\\x1b[0m Legacy instance is already onboarded, marking onboarding as complete. You will not see this message again.\"\n      );\n      await SystemSettings.markOnboardingComplete();\n      return true;\n    }\n    return false;\n  } catch (e) {\n    console.error(\n      \"\\x1b[31m[ONBOARDING PATCH]\\x1b[0m Error marking onboarding as complete\",\n      e.message,\n      e\n    );\n    return false;\n  }\n}\n\n/**\n * Check if the server is already onboarded by the old way of checking if the server in any way has been setup.\n * @returns {Promise<boolean>}\n */\nasync function isLegacyOnboarded() {\n  // LLM Provider is set, so we can assume onboarding is complete since this is default null in SystemSettings.js\n  if (!!process.env.LLM_PROVIDER) return true;\n\n  // Vector DB is set, so we can assume onboarding is complete since this is default null in SystemSettings.js (default is lancedb in frontend)\n  if (!!process.env.VECTOR_DB) return true;\n\n  // Check if the AUTH_TOKEN/JWT_SECRET is set, so we can assume onboarding is complete since this is default null in SystemSettings.js\n  if (!!process.env.AUTH_TOKEN || !!process.env.JWT_SECRET) return true;\n\n  // Check multi-user mode is enabled, if it is, then they are already using the app.\n  if ((await SystemSettings.isMultiUserMode()) === true) return true;\n  return false;\n}\n\nmodule.exports = markOnboarded;\n"
  },
  {
    "path": "server/utils/chats/agents.js",
    "content": "const pluralize = require(\"pluralize\");\nconst {\n  WorkspaceAgentInvocation,\n} = require(\"../../models/workspaceAgentInvocation\");\nconst { writeResponseChunk } = require(\"../helpers/chat/responses\");\nconst { Workspace } = require(\"../../models/workspace\");\n\n/**\n * In-memory cache for attachments associated with agent invocations.\n * Attachments are stored here when grepAgents creates an invocation,\n * then retrieved by AgentHandler when the websocket connects.\n * @type {Map<string, Array>}\n */\nconst invocationAttachmentsCache = new Map();\n\n/**\n * Store attachments for an invocation UUID\n * @param {string} uuid - The invocation UUID\n * @param {Array} attachments - The attachments array\n */\nfunction cacheInvocationAttachments(uuid, attachments = []) {\n  if (attachments.length > 0) {\n    invocationAttachmentsCache.set(uuid, attachments);\n  }\n}\n\n/**\n * Retrieve and remove attachments for an invocation UUID\n * @param {string} uuid - The invocation UUID\n * @returns {Array} The attachments array (empty if none cached)\n */\nfunction getAndClearInvocationAttachments(uuid) {\n  const attachments = invocationAttachmentsCache.get(uuid) || [];\n  invocationAttachmentsCache.delete(uuid);\n  return attachments;\n}\n\nasync function grepAgents({\n  uuid,\n  response,\n  message,\n  workspace,\n  user = null,\n  thread = null,\n  attachments = [],\n}) {\n  let nativeToolingEnabled = false;\n\n  // If the workspace is in automatic mode, check if the workspace supports native tooling\n  // to determine if the agent flow should be used or not.\n  if (workspace?.chatMode === \"automatic\")\n    nativeToolingEnabled = await Workspace.supportsNativeToolCalling(workspace);\n\n  const agentHandles = WorkspaceAgentInvocation.parseAgents(message);\n  if (agentHandles.length > 0 || nativeToolingEnabled) {\n    const { invocation: newInvocation } = await WorkspaceAgentInvocation.new({\n      prompt: message,\n      workspace: workspace,\n      user: user,\n      thread: thread,\n    });\n\n    if (!newInvocation) {\n      writeResponseChunk(response, {\n        id: uuid,\n        type: \"statusResponse\",\n        textResponse: `${pluralize(\n          \"Agent\",\n          agentHandles.length\n        )} ${agentHandles.join(\n          \", \"\n        )} could not be called. Chat will be handled as default chat.`,\n        sources: [],\n        close: true,\n        animate: false,\n        error: null,\n      });\n      return;\n    }\n\n    // Cache attachments for the websocket handler to retrieve later\n    cacheInvocationAttachments(newInvocation.uuid, attachments);\n\n    writeResponseChunk(response, {\n      id: uuid,\n      type: \"agentInitWebsocketConnection\",\n      textResponse: null,\n      sources: [],\n      close: false,\n      error: null,\n      websocketUUID: newInvocation.uuid,\n    });\n\n    // Close HTTP stream-able chunk response method because we will swap to agents now.\n    writeResponseChunk(response, {\n      id: uuid,\n      type: \"statusResponse\",\n      textResponse: `${pluralize(\n        \"Agent\",\n        agentHandles.length\n      )} ${agentHandles.join(\n        \", \"\n      )} invoked.\\nSwapping over to agent chat. Type /exit to exit agent execution loop early.`,\n      sources: [],\n      close: true,\n      error: null,\n      animate: true,\n    });\n    return true;\n  }\n\n  return false;\n}\n\nmodule.exports = { grepAgents, getAndClearInvocationAttachments };\n"
  },
  {
    "path": "server/utils/chats/apiChatHandler.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst { DocumentManager } = require(\"../DocumentManager\");\nconst { WorkspaceChats } = require(\"../../models/workspaceChats\");\nconst { getVectorDbClass, getLLMProvider } = require(\"../helpers\");\nconst { writeResponseChunk } = require(\"../helpers/chat/responses\");\nconst {\n  chatPrompt,\n  sourceIdentifier,\n  recentChatHistory,\n  grepAllSlashCommands,\n} = require(\"./index\");\nconst {\n  EphemeralAgentHandler,\n  EphemeralEventListener,\n} = require(\"../agents/ephemeral\");\nconst { Telemetry } = require(\"../../models/telemetry\");\nconst { CollectorApi } = require(\"../collectorApi\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { hotdirPath, normalizePath, isWithin } = require(\"../files\");\n/**\n * @typedef ResponseObject\n * @property {string} id - uuid of response\n * @property {string} type - Type of response\n * @property {string|null} textResponse - full text response\n * @property {object[]} sources\n * @property {boolean} close\n * @property {string|null} error\n * @property {object} metrics\n */\n\n/**\n * Users can pass in documents as attachments to the chat API.\n * The name of the document is the name of the attachment and must include the file extension.\n * the mime type for documents is `application/anythingllm-document` - anything else is assumed to be an image.\n * @param {{name: string, mime: string, contentString: string}[]} attachments\n * @returns {Promise<{parsedDocuments: Object[], imageAttachments: {name: string; mime: string; contentString: string}[]}>}\n */\nasync function processDocumentAttachments(attachments = []) {\n  if (!Array.isArray(attachments) || attachments.length === 0)\n    return { parsedDocuments: [], imageAttachments: [] };\n  const documentAttachments = [];\n  const imageAttachments = [];\n  for (const attachment of attachments) {\n    if (\n      attachment &&\n      attachment.contentString &&\n      attachment.mime &&\n      attachment.mime.toLowerCase() === \"application/anythingllm-document\"\n    )\n      documentAttachments.push(attachment);\n    else imageAttachments.push(attachment);\n  }\n\n  if (documentAttachments.length === 0)\n    return { parsedDocuments: [], imageAttachments };\n  const Collector = new CollectorApi();\n  const processingOnline = await Collector.online();\n  if (!processingOnline) {\n    console.warn(\n      \"Collector API is not online, skipping document attachment processing\"\n    );\n    return { parsedDocuments: [], imageAttachments };\n  }\n  if (!fs.existsSync(hotdirPath)) fs.mkdirSync(hotdirPath, { recursive: true });\n\n  const parsedDocuments = [];\n  for (const attachment of documentAttachments) {\n    try {\n      let base64Data = attachment.contentString;\n      const dataUriMatch = base64Data.match(/^data:[^;]+;base64,(.+)$/);\n      if (dataUriMatch) base64Data = dataUriMatch[1];\n\n      const buffer = Buffer.from(base64Data, \"base64\");\n      const filename = normalizePath(\n        attachment.name || `attachment-${uuidv4()}`\n      );\n      const filePath = normalizePath(path.join(hotdirPath, filename));\n      if (!isWithin(hotdirPath, filePath))\n        throw new Error(`Invalid file path for attachment ${filename}`);\n      fs.writeFileSync(filePath, buffer);\n\n      const { success, reason, documents } =\n        await Collector.parseDocument(filename);\n      if (success && documents?.length > 0) parsedDocuments.push(...documents);\n      else console.warn(`Failed to parse attachment ${filename}:`, reason);\n    } catch (error) {\n      console.error(\n        `Error processing attachment ${attachment.name}:`,\n        error.message\n      );\n    }\n  }\n\n  return { parsedDocuments, imageAttachments };\n}\n\n/**\n * Handle synchronous chats with your workspace via the developer API endpoint\n * @param {{\n *  workspace: import(\"@prisma/client\").workspaces,\n *  message:string,\n *  mode: \"automatic\"|\"chat\"|\"query\",\n *  user: import(\"@prisma/client\").users|null,\n *  thread: import(\"@prisma/client\").workspace_threads|null,\n *  sessionId: string|null,\n *  attachments: { name: string; mime: string; contentString: string }[],\n *  reset: boolean,\n * }} parameters\n * @returns {Promise<ResponseObject>}\n */\nasync function chatSync({\n  workspace,\n  message = null,\n  mode = \"chat\",\n  user = null,\n  thread = null,\n  sessionId = null,\n  attachments = [],\n  reset = false,\n}) {\n  const uuid = uuidv4();\n  const chatMode = mode ?? \"chat\";\n\n  // If the user wants to reset the chat history we do so pre-flight\n  // and continue execution. If no message is provided then the user intended\n  // to reset the chat history only and we can exit early with a confirmation.\n  if (reset) {\n    await WorkspaceChats.markThreadHistoryInvalidV2({\n      workspaceId: workspace.id,\n      user_id: user?.id,\n      thread_id: thread?.id,\n      api_session_id: sessionId,\n    });\n    if (!message?.length) {\n      return {\n        id: uuid,\n        type: \"textResponse\",\n        textResponse: \"Chat history was reset!\",\n        sources: [],\n        close: true,\n        error: null,\n        metrics: {},\n      };\n    }\n  }\n\n  // Process slash commands\n  // Since preset commands are not supported in API calls, we can just process the message here\n  const processedMessage = await grepAllSlashCommands(message);\n  message = processedMessage;\n\n  if (\n    await EphemeralAgentHandler.isAgentInvocation({\n      message,\n      workspace,\n      chatMode,\n    })\n  ) {\n    await Telemetry.sendTelemetry(\"agent_chat_started\");\n\n    // Initialize the EphemeralAgentHandler to handle non-continuous\n    // conversations with agents since this is over REST.\n    const agentHandler = new EphemeralAgentHandler({\n      uuid,\n      workspace,\n      prompt: message,\n      userId: user?.id || null,\n      threadId: thread?.id || null,\n      sessionId,\n      attachments,\n    });\n\n    // Establish event listener that emulates websocket calls\n    // in Aibitat so that we can keep the same interface in Aibitat\n    // but use HTTP.\n    const eventListener = new EphemeralEventListener();\n    await agentHandler.init();\n    await agentHandler.createAIbitat({ handler: eventListener });\n    agentHandler.startAgentCluster();\n\n    // The cluster has started and now we wait for close event since\n    // this is a synchronous call for an agent, so we return everything at once.\n    // After this, we conclude the call as we normally do.\n    return await eventListener\n      .waitForClose()\n      .then(async ({ thoughts, textResponse }) => {\n        await WorkspaceChats.new({\n          workspaceId: workspace.id,\n          prompt: String(message),\n          response: {\n            text: textResponse,\n            sources: [],\n            attachments,\n            type: chatMode,\n            thoughts,\n          },\n          include: false,\n          apiSessionId: sessionId,\n        });\n        return {\n          id: uuid,\n          type: \"textResponse\",\n          sources: [],\n          close: true,\n          error: null,\n          textResponse,\n          thoughts,\n        };\n      });\n  }\n\n  const LLMConnector = getLLMProvider({\n    provider: workspace?.chatProvider,\n    model: workspace?.chatModel,\n  });\n  const VectorDb = getVectorDbClass();\n  const messageLimit = workspace?.openAiHistory || 20;\n  const hasVectorizedSpace = await VectorDb.hasNamespace(workspace.slug);\n  const embeddingsCount = await VectorDb.namespaceCount(workspace.slug);\n\n  // User is trying to query-mode chat a workspace that has no data in it - so\n  // we should exit early as no information can be found under these conditions.\n  if ((!hasVectorizedSpace || embeddingsCount === 0) && chatMode === \"query\") {\n    const textResponse =\n      workspace?.queryRefusalResponse ??\n      \"There is no relevant information in this workspace to answer your query.\";\n\n    await WorkspaceChats.new({\n      workspaceId: workspace.id,\n      prompt: String(message),\n      response: {\n        text: textResponse,\n        sources: [],\n        attachments: attachments,\n        type: chatMode,\n        metrics: {},\n      },\n      include: false,\n      apiSessionId: sessionId,\n    });\n\n    return {\n      id: uuid,\n      type: \"textResponse\",\n      sources: [],\n      close: true,\n      error: null,\n      textResponse,\n      metrics: {},\n    };\n  }\n\n  // If we are here we know that we are in a workspace that is:\n  // 1. Chatting in \"chat\" mode and may or may _not_ have embeddings\n  // 2. Chatting in \"query\" mode and has at least 1 embedding\n  let contextTexts = [];\n  let sources = [];\n  let pinnedDocIdentifiers = [];\n  const { rawHistory, chatHistory } = await recentChatHistory({\n    user,\n    workspace,\n    thread,\n    messageLimit,\n    apiSessionId: sessionId,\n  });\n\n  await new DocumentManager({\n    workspace,\n    maxTokens: LLMConnector.promptWindowLimit(),\n  })\n    .pinnedDocs()\n    .then((pinnedDocs) => {\n      pinnedDocs.forEach((doc) => {\n        const { pageContent, ...metadata } = doc;\n        pinnedDocIdentifiers.push(sourceIdentifier(doc));\n        contextTexts.push(doc.pageContent);\n        sources.push({\n          text:\n            pageContent.slice(0, 1_000) +\n            \"...continued on in source document...\",\n          ...metadata,\n        });\n      });\n    });\n\n  const processedAttachments = await processDocumentAttachments(attachments);\n  const parsedAttachments = processedAttachments.parsedDocuments;\n  attachments = processedAttachments.imageAttachments;\n  parsedAttachments.forEach((doc) => {\n    if (doc.pageContent) {\n      contextTexts.push(doc.pageContent);\n      const { pageContent, ...metadata } = doc;\n      sources.push({\n        text:\n          pageContent.slice(0, 1_000) + \"...continued on in source document...\",\n        ...metadata,\n      });\n    }\n  });\n\n  const vectorSearchResults =\n    embeddingsCount !== 0\n      ? await VectorDb.performSimilaritySearch({\n          namespace: workspace.slug,\n          input: message,\n          LLMConnector,\n          similarityThreshold: workspace?.similarityThreshold,\n          topN: workspace?.topN,\n          filterIdentifiers: pinnedDocIdentifiers,\n          rerank: workspace?.vectorSearchMode === \"rerank\",\n        })\n      : {\n          contextTexts: [],\n          sources: [],\n          message: null,\n        };\n\n  // Failed similarity search if it was run at all and failed.\n  if (!!vectorSearchResults.message) {\n    return {\n      id: uuid,\n      type: \"abort\",\n      textResponse: null,\n      sources: [],\n      close: true,\n      error: vectorSearchResults.message,\n      metrics: {},\n    };\n  }\n\n  const { fillSourceWindow } = require(\"../helpers/chat\");\n  const filledSources = fillSourceWindow({\n    nDocs: workspace?.topN || 4,\n    searchResults: vectorSearchResults.sources,\n    history: rawHistory,\n    filterIdentifiers: pinnedDocIdentifiers,\n  });\n\n  // Why does contextTexts get all the info, but sources only get current search?\n  // This is to give the ability of the LLM to \"comprehend\" a contextual response without\n  // populating the Citations under a response with documents the user \"thinks\" are irrelevant\n  // due to how we manage backfilling of the context to keep chats with the LLM more correct in responses.\n  // If a past citation was used to answer the question - that is visible in the history so it logically makes sense\n  // and does not appear to the user that a new response used information that is otherwise irrelevant for a given prompt.\n  // TLDR; reduces GitHub issues for \"LLM citing document that has no answer in it\" while keep answers highly accurate.\n  contextTexts = [...contextTexts, ...filledSources.contextTexts];\n  sources = [...sources, ...vectorSearchResults.sources];\n\n  // If in query mode and no context chunks are found from search, backfill, or pins -  do not\n  // let the LLM try to hallucinate a response or use general knowledge and exit early\n  if (chatMode === \"query\" && contextTexts.length === 0) {\n    const textResponse =\n      workspace?.queryRefusalResponse ??\n      \"There is no relevant information in this workspace to answer your query.\";\n\n    await WorkspaceChats.new({\n      workspaceId: workspace.id,\n      prompt: message,\n      response: {\n        text: textResponse,\n        sources: [],\n        attachments: attachments,\n        type: chatMode,\n        metrics: {},\n      },\n      threadId: thread?.id || null,\n      include: false,\n      apiSessionId: sessionId,\n      user,\n    });\n\n    return {\n      id: uuid,\n      type: \"textResponse\",\n      sources: [],\n      close: true,\n      error: null,\n      textResponse,\n      metrics: {},\n    };\n  }\n\n  // Compress & Assemble message to ensure prompt passes token limit with room for response\n  // and build system messages based on inputs and history.\n  const messages = await LLMConnector.compressMessages(\n    {\n      systemPrompt: await chatPrompt(workspace, user),\n      userPrompt: message,\n      contextTexts,\n      chatHistory,\n      attachments,\n    },\n    rawHistory\n  );\n\n  // Send the text completion.\n  const { textResponse, metrics: performanceMetrics } =\n    await LLMConnector.getChatCompletion(messages, {\n      temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp,\n      user: user,\n    });\n\n  if (!textResponse) {\n    return {\n      id: uuid,\n      type: \"abort\",\n      textResponse: null,\n      sources: [],\n      close: true,\n      error: \"No text completion could be completed with this input.\",\n      metrics: performanceMetrics,\n    };\n  }\n\n  const { chat } = await WorkspaceChats.new({\n    workspaceId: workspace.id,\n    prompt: message,\n    response: {\n      text: textResponse,\n      sources,\n      attachments,\n      type: chatMode,\n      metrics: performanceMetrics,\n    },\n    threadId: thread?.id || null,\n    apiSessionId: sessionId,\n    user,\n  });\n\n  return {\n    id: uuid,\n    type: \"textResponse\",\n    close: true,\n    error: null,\n    chatId: chat.id,\n    textResponse,\n    sources,\n    metrics: performanceMetrics,\n  };\n}\n\n/**\n * Handle streamable HTTP chunks for chats with your workspace via the developer API endpoint\n * @param {{\n * response: import(\"express\").Response,\n *  workspace: import(\"@prisma/client\").workspaces,\n *  message:string,\n *  mode: \"automatic\"|\"chat\"|\"query\",\n *  user: import(\"@prisma/client\").users|null,\n *  thread: import(\"@prisma/client\").workspace_threads|null,\n *  sessionId: string|null,\n *  attachments: { name: string; mime: string; contentString: string }[],\n *  reset: boolean,\n * }} parameters\n * @returns {Promise<VoidFunction>}\n */\nasync function streamChat({\n  response,\n  workspace,\n  message = null,\n  mode = \"chat\",\n  user = null,\n  thread = null,\n  sessionId = null,\n  attachments = [],\n  reset = false,\n}) {\n  const uuid = uuidv4();\n  const chatMode = mode ?? \"chat\";\n\n  // If the user wants to reset the chat history we do so pre-flight\n  // and continue execution. If no message is provided then the user intended\n  // to reset the chat history only and we can exit early with a confirmation.\n  if (reset) {\n    await WorkspaceChats.markThreadHistoryInvalidV2({\n      workspaceId: workspace.id,\n      user_id: user?.id,\n      thread_id: thread?.id,\n      api_session_id: sessionId,\n    });\n    if (!message?.length) {\n      writeResponseChunk(response, {\n        id: uuid,\n        type: \"textResponse\",\n        textResponse: \"Chat history was reset!\",\n        sources: [],\n        attachments: [],\n        close: true,\n        error: null,\n        metrics: {},\n      });\n      return;\n    }\n  }\n\n  // Check for and process slash commands\n  // Since preset commands are not supported in API calls, we can just process the message here\n  const processedMessage = await grepAllSlashCommands(message);\n  message = processedMessage;\n\n  if (\n    await EphemeralAgentHandler.isAgentInvocation({\n      message,\n      workspace,\n      chatMode,\n    })\n  ) {\n    await Telemetry.sendTelemetry(\"agent_chat_started\");\n\n    // Initialize the EphemeralAgentHandler to handle non-continuous\n    // conversations with agents since this is over REST.\n    const agentHandler = new EphemeralAgentHandler({\n      uuid,\n      workspace,\n      prompt: message,\n      userId: user?.id || null,\n      threadId: thread?.id || null,\n      sessionId,\n      attachments,\n    });\n\n    // Establish event listener that emulates websocket calls\n    // in Aibitat so that we can keep the same interface in Aibitat\n    // but use HTTP.\n    const eventListener = new EphemeralEventListener();\n    await agentHandler.init();\n    await agentHandler.createAIbitat({ handler: eventListener });\n    agentHandler.startAgentCluster();\n\n    // The cluster has started and now we wait for close event since\n    // and stream back any results we get from agents as they come in.\n    return eventListener\n      .streamAgentEvents(response, uuid)\n      .then(async ({ thoughts, textResponse }) => {\n        await WorkspaceChats.new({\n          workspaceId: workspace.id,\n          prompt: String(message),\n          response: {\n            text: textResponse,\n            sources: [],\n            attachments: attachments,\n            type: chatMode,\n            thoughts,\n          },\n          include: true,\n          threadId: thread?.id || null,\n          apiSessionId: sessionId,\n        });\n        writeResponseChunk(response, {\n          uuid,\n          type: \"finalizeResponseStream\",\n          textResponse,\n          thoughts,\n          close: true,\n          error: false,\n        });\n      });\n  }\n\n  const LLMConnector = getLLMProvider({\n    provider: workspace?.chatProvider,\n    model: workspace?.chatModel,\n  });\n\n  const VectorDb = getVectorDbClass();\n  const messageLimit = workspace?.openAiHistory || 20;\n  const hasVectorizedSpace = await VectorDb.hasNamespace(workspace.slug);\n  const embeddingsCount = await VectorDb.namespaceCount(workspace.slug);\n\n  // User is trying to query-mode chat a workspace that has no data in it - so\n  // we should exit early as no information can be found under these conditions.\n  if ((!hasVectorizedSpace || embeddingsCount === 0) && chatMode === \"query\") {\n    const textResponse =\n      workspace?.queryRefusalResponse ??\n      \"There is no relevant information in this workspace to answer your query.\";\n    writeResponseChunk(response, {\n      id: uuid,\n      type: \"textResponse\",\n      textResponse,\n      sources: [],\n      attachments: [],\n      close: true,\n      error: null,\n      metrics: {},\n    });\n    await WorkspaceChats.new({\n      workspaceId: workspace.id,\n      prompt: message,\n      response: {\n        text: textResponse,\n        sources: [],\n        attachments: attachments,\n        type: chatMode,\n        metrics: {},\n      },\n      threadId: thread?.id || null,\n      apiSessionId: sessionId,\n      include: false,\n      user,\n    });\n    return;\n  }\n\n  // If we are here we know that we are in a workspace that is:\n  // 1. Chatting in \"chat\" mode and may or may _not_ have embeddings\n  // 2. Chatting in \"query\" mode and has at least 1 embedding\n  let completeText;\n  let metrics = {};\n  let contextTexts = [];\n  let sources = [];\n  let pinnedDocIdentifiers = [];\n  const { rawHistory, chatHistory } = await recentChatHistory({\n    user,\n    workspace,\n    thread,\n    messageLimit,\n    apiSessionId: sessionId,\n  });\n\n  // Look for pinned documents and see if the user decided to use this feature. We will also do a vector search\n  // as pinning is a supplemental tool but it should be used with caution since it can easily blow up a context window.\n  // However we limit the maximum of appended context to 80% of its overall size, mostly because if it expands beyond this\n  // it will undergo prompt compression anyway to make it work. If there is so much pinned that the context here is bigger than\n  // what the model can support - it would get compressed anyway and that really is not the point of pinning. It is really best\n  // suited for high-context models.\n  await new DocumentManager({\n    workspace,\n    maxTokens: LLMConnector.promptWindowLimit(),\n  })\n    .pinnedDocs()\n    .then((pinnedDocs) => {\n      pinnedDocs.forEach((doc) => {\n        const { pageContent, ...metadata } = doc;\n        pinnedDocIdentifiers.push(sourceIdentifier(doc));\n        contextTexts.push(doc.pageContent);\n        sources.push({\n          text:\n            pageContent.slice(0, 1_000) +\n            \"...continued on in source document...\",\n          ...metadata,\n        });\n      });\n    });\n\n  const processedAttachments = await processDocumentAttachments(attachments);\n  const parsedAttachments = processedAttachments.parsedDocuments;\n  attachments = processedAttachments.imageAttachments;\n  parsedAttachments.forEach((doc) => {\n    if (doc.pageContent) {\n      contextTexts.push(doc.pageContent);\n      const { pageContent, ...metadata } = doc;\n      sources.push({\n        text:\n          pageContent.slice(0, 1_000) + \"...continued on in source document...\",\n        ...metadata,\n      });\n    }\n  });\n\n  const vectorSearchResults =\n    embeddingsCount !== 0\n      ? await VectorDb.performSimilaritySearch({\n          namespace: workspace.slug,\n          input: message,\n          LLMConnector,\n          similarityThreshold: workspace?.similarityThreshold,\n          topN: workspace?.topN,\n          filterIdentifiers: pinnedDocIdentifiers,\n          rerank: workspace?.vectorSearchMode === \"rerank\",\n        })\n      : {\n          contextTexts: [],\n          sources: [],\n          message: null,\n        };\n\n  // Failed similarity search if it was run at all and failed.\n  if (!!vectorSearchResults.message) {\n    writeResponseChunk(response, {\n      id: uuid,\n      type: \"abort\",\n      textResponse: null,\n      sources: [],\n      close: true,\n      error: vectorSearchResults.message,\n      metrics: {},\n    });\n    return;\n  }\n\n  const { fillSourceWindow } = require(\"../helpers/chat\");\n  const filledSources = fillSourceWindow({\n    nDocs: workspace?.topN || 4,\n    searchResults: vectorSearchResults.sources,\n    history: rawHistory,\n    filterIdentifiers: pinnedDocIdentifiers,\n  });\n\n  // Why does contextTexts get all the info, but sources only get current search?\n  // This is to give the ability of the LLM to \"comprehend\" a contextual response without\n  // populating the Citations under a response with documents the user \"thinks\" are irrelevant\n  // due to how we manage backfilling of the context to keep chats with the LLM more correct in responses.\n  // If a past citation was used to answer the question - that is visible in the history so it logically makes sense\n  // and does not appear to the user that a new response used information that is otherwise irrelevant for a given prompt.\n  // TLDR; reduces GitHub issues for \"LLM citing document that has no answer in it\" while keep answers highly accurate.\n  contextTexts = [...contextTexts, ...filledSources.contextTexts];\n  sources = [...sources, ...vectorSearchResults.sources];\n\n  // If in query mode and no context chunks are found from search, backfill, or pins -  do not\n  // let the LLM try to hallucinate a response or use general knowledge and exit early\n  if (chatMode === \"query\" && contextTexts.length === 0) {\n    const textResponse =\n      workspace?.queryRefusalResponse ??\n      \"There is no relevant information in this workspace to answer your query.\";\n    writeResponseChunk(response, {\n      id: uuid,\n      type: \"textResponse\",\n      textResponse,\n      sources: [],\n      close: true,\n      error: null,\n      metrics: {},\n    });\n\n    await WorkspaceChats.new({\n      workspaceId: workspace.id,\n      prompt: message,\n      response: {\n        text: textResponse,\n        sources: [],\n        attachments: attachments,\n        type: chatMode,\n        metrics: {},\n      },\n      threadId: thread?.id || null,\n      apiSessionId: sessionId,\n      include: false,\n      user,\n    });\n    return;\n  }\n\n  // Compress & Assemble message to ensure prompt passes token limit with room for response\n  // and build system messages based on inputs and history.\n  const messages = await LLMConnector.compressMessages(\n    {\n      systemPrompt: await chatPrompt(workspace, user),\n      userPrompt: message,\n      contextTexts,\n      chatHistory,\n      attachments,\n    },\n    rawHistory\n  );\n\n  // If streaming is not explicitly enabled for connector\n  // we do regular waiting of a response and send a single chunk.\n  if (LLMConnector.streamingEnabled() !== true) {\n    console.log(\n      `\\x1b[31m[STREAMING DISABLED]\\x1b[0m Streaming is not available for ${LLMConnector.constructor.name}. Will use regular chat method.`\n    );\n    const { textResponse, metrics: performanceMetrics } =\n      await LLMConnector.getChatCompletion(messages, {\n        temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp,\n        user: user,\n      });\n    completeText = textResponse;\n    metrics = performanceMetrics;\n    writeResponseChunk(response, {\n      uuid,\n      sources,\n      type: \"textResponseChunk\",\n      textResponse: completeText,\n      close: true,\n      error: false,\n      metrics,\n    });\n  } else {\n    const stream = await LLMConnector.streamGetChatCompletion(messages, {\n      temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp,\n      user: user,\n    });\n    completeText = await LLMConnector.handleStream(response, stream, { uuid });\n    metrics = stream.metrics;\n  }\n\n  if (completeText?.length > 0) {\n    const { chat } = await WorkspaceChats.new({\n      workspaceId: workspace.id,\n      prompt: message,\n      response: {\n        text: completeText,\n        sources,\n        type: chatMode,\n        metrics,\n        attachments,\n      },\n      threadId: thread?.id || null,\n      apiSessionId: sessionId,\n      user,\n    });\n\n    writeResponseChunk(response, {\n      uuid,\n      type: \"finalizeResponseStream\",\n      close: true,\n      error: false,\n      chatId: chat.id,\n      metrics,\n      sources,\n    });\n    return;\n  }\n\n  writeResponseChunk(response, {\n    uuid,\n    type: \"finalizeResponseStream\",\n    close: true,\n    error: false,\n  });\n  return;\n}\n\nmodule.exports.ApiChatHandler = {\n  chatSync,\n  streamChat,\n};\n"
  },
  {
    "path": "server/utils/chats/commands/reset.js",
    "content": "const { WorkspaceChats } = require(\"../../../models/workspaceChats\");\n\nasync function resetMemory(\n  workspace,\n  _message,\n  msgUUID,\n  user = null,\n  thread = null\n) {\n  // If thread is present we are wanting to reset this specific thread. Not the whole workspace.\n  thread\n    ? await WorkspaceChats.markThreadHistoryInvalid(\n        workspace.id,\n        user,\n        thread.id\n      )\n    : await WorkspaceChats.markHistoryInvalid(workspace.id, user);\n\n  return {\n    uuid: msgUUID,\n    type: \"textResponse\",\n    textResponse: \"Chat memory was reset!\",\n    sources: [],\n    close: true,\n    error: false,\n    action: \"reset_chat\",\n  };\n}\n\nmodule.exports = {\n  resetMemory,\n};\n"
  },
  {
    "path": "server/utils/chats/embed.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst { getVectorDbClass, getLLMProvider } = require(\"../helpers\");\nconst { chatPrompt, sourceIdentifier } = require(\"./index\");\nconst { EmbedChats } = require(\"../../models/embedChats\");\nconst {\n  convertToPromptHistory,\n  writeResponseChunk,\n} = require(\"../helpers/chat/responses\");\nconst { DocumentManager } = require(\"../DocumentManager\");\n\nasync function streamChatWithForEmbed(\n  response,\n  /** @type {import(\"@prisma/client\").embed_configs & {workspace?: import(\"@prisma/client\").workspaces}} */\n  embed,\n  /** @type {String} */\n  message,\n  /** @type {String} */\n  sessionId,\n  { promptOverride, modelOverride, temperatureOverride, username }\n) {\n  const chatMode = embed.chat_mode;\n  const chatModel = embed.allow_model_override ? modelOverride : null;\n\n  // If there are overrides in request & they are permitted, override the default workspace ref information.\n  if (embed.allow_prompt_override)\n    embed.workspace.openAiPrompt = promptOverride;\n  if (embed.allow_temperature_override)\n    embed.workspace.openAiTemp = parseFloat(temperatureOverride);\n\n  const uuid = uuidv4();\n  const LLMConnector = getLLMProvider({\n    provider: embed?.workspace?.chatProvider,\n    model: chatModel ?? embed.workspace?.chatModel,\n  });\n  const VectorDb = getVectorDbClass();\n\n  const messageLimit = embed.message_limit ?? 20;\n  const hasVectorizedSpace = await VectorDb.hasNamespace(embed.workspace.slug);\n  const embeddingsCount = await VectorDb.namespaceCount(embed.workspace.slug);\n\n  // User is trying to query-mode chat a workspace that has no data in it - so\n  // we should exit early as no information can be found under these conditions.\n  if ((!hasVectorizedSpace || embeddingsCount === 0) && chatMode === \"query\") {\n    writeResponseChunk(response, {\n      id: uuid,\n      type: \"textResponse\",\n      textResponse:\n        \"I do not have enough information to answer that. Try another question.\",\n      sources: [],\n      close: true,\n      error: null,\n    });\n    return;\n  }\n\n  let completeText;\n  let metrics = {};\n  let contextTexts = [];\n  let sources = [];\n  let pinnedDocIdentifiers = [];\n  const { rawHistory, chatHistory } = await recentEmbedChatHistory(\n    sessionId,\n    embed,\n    messageLimit\n  );\n\n  // See stream.js comment for more information on this implementation.\n  await new DocumentManager({\n    workspace: embed.workspace,\n    maxTokens: LLMConnector.promptWindowLimit(),\n  })\n    .pinnedDocs()\n    .then((pinnedDocs) => {\n      pinnedDocs.forEach((doc) => {\n        const { pageContent, ...metadata } = doc;\n        pinnedDocIdentifiers.push(sourceIdentifier(doc));\n        contextTexts.push(doc.pageContent);\n        sources.push({\n          text:\n            pageContent.slice(0, 1_000) +\n            \"...continued on in source document...\",\n          ...metadata,\n        });\n      });\n    });\n\n  const vectorSearchResults =\n    embeddingsCount !== 0\n      ? await VectorDb.performSimilaritySearch({\n          namespace: embed.workspace.slug,\n          input: message,\n          LLMConnector,\n          similarityThreshold: embed.workspace?.similarityThreshold,\n          topN: embed.workspace?.topN,\n          filterIdentifiers: pinnedDocIdentifiers,\n          rerank: embed.workspace?.vectorSearchMode === \"rerank\",\n        })\n      : {\n          contextTexts: [],\n          sources: [],\n          message: null,\n        };\n\n  // Failed similarity search if it was run at all and failed.\n  if (!!vectorSearchResults.message) {\n    writeResponseChunk(response, {\n      id: uuid,\n      type: \"abort\",\n      textResponse: null,\n      sources: [],\n      close: true,\n      error: \"Failed to connect to vector database provider.\",\n    });\n    return;\n  }\n\n  const { fillSourceWindow } = require(\"../helpers/chat\");\n  const filledSources = fillSourceWindow({\n    nDocs: embed.workspace?.topN || 4,\n    searchResults: vectorSearchResults.sources,\n    history: rawHistory,\n    filterIdentifiers: pinnedDocIdentifiers,\n  });\n\n  // Why does contextTexts get all the info, but sources only get current search?\n  // This is to give the ability of the LLM to \"comprehend\" a contextual response without\n  // populating the Citations under a response with documents the user \"thinks\" are irrelevant\n  // due to how we manage backfilling of the context to keep chats with the LLM more correct in responses.\n  // If a past citation was used to answer the question - that is visible in the history so it logically makes sense\n  // and does not appear to the user that a new response used information that is otherwise irrelevant for a given prompt.\n  // TLDR; reduces GitHub issues for \"LLM citing document that has no answer in it\" while keep answers highly accurate.\n  contextTexts = [...contextTexts, ...filledSources.contextTexts];\n  sources = [...sources, ...vectorSearchResults.sources];\n\n  // If in query mode and no sources are found in current search or backfilled from history, do not\n  // let the LLM try to hallucinate a response or use general knowledge\n  if (chatMode === \"query\" && contextTexts.length === 0) {\n    writeResponseChunk(response, {\n      id: uuid,\n      type: \"textResponse\",\n      textResponse:\n        embed.workspace?.queryRefusalResponse ??\n        \"There is no relevant information in this workspace to answer your query.\",\n      sources: [],\n      close: true,\n      error: null,\n    });\n    return;\n  }\n\n  // Compress message to ensure prompt passes token limit with room for response\n  // and build system messages based on inputs and history.\n  const messages = await LLMConnector.compressMessages(\n    {\n      systemPrompt: await chatPrompt(embed.workspace, username),\n      userPrompt: message,\n      contextTexts,\n      chatHistory,\n    },\n    rawHistory\n  );\n\n  // If streaming is not explicitly enabled for connector\n  // we do regular waiting of a response and send a single chunk.\n  if (LLMConnector.streamingEnabled() !== true) {\n    console.log(\n      `\\x1b[31m[STREAMING DISABLED]\\x1b[0m Streaming is not available for ${LLMConnector.constructor.name}. Will use regular chat method.`\n    );\n    const { textResponse, metrics: performanceMetrics } =\n      await LLMConnector.getChatCompletion(messages, {\n        temperature: embed.workspace?.openAiTemp ?? LLMConnector.defaultTemp,\n      });\n    completeText = textResponse;\n    metrics = performanceMetrics;\n    writeResponseChunk(response, {\n      uuid,\n      sources: [],\n      type: \"textResponseChunk\",\n      textResponse: completeText,\n      close: true,\n      error: false,\n    });\n  } else {\n    const stream = await LLMConnector.streamGetChatCompletion(messages, {\n      temperature: embed.workspace?.openAiTemp ?? LLMConnector.defaultTemp,\n    });\n    completeText = await LLMConnector.handleStream(response, stream, {\n      uuid,\n      sources: [],\n    });\n    metrics = stream.metrics;\n  }\n\n  await EmbedChats.new({\n    embedId: embed.id,\n    prompt: message,\n    response: { text: completeText, type: chatMode, sources, metrics },\n    connection_information: response.locals.connection\n      ? {\n          ...response.locals.connection,\n          username: !!username ? String(username) : null,\n        }\n      : { username: !!username ? String(username) : null },\n    sessionId,\n  });\n  return;\n}\n\n/**\n * @param {string} sessionId the session id of the user from embed widget\n * @param {Object} embed the embed config object\n * @param {Number} messageLimit the number of messages to return\n * @returns {Promise<{rawHistory: import(\"@prisma/client\").embed_chats[], chatHistory: {role: string, content: string, attachments?: Object[]}[]}>\n */\nasync function recentEmbedChatHistory(sessionId, embed, messageLimit = 20) {\n  const rawHistory = (\n    await EmbedChats.forEmbedByUser(embed.id, sessionId, messageLimit, {\n      id: \"desc\",\n    })\n  ).reverse();\n  return { rawHistory, chatHistory: convertToPromptHistory(rawHistory) };\n}\n\nmodule.exports = {\n  streamChatWithForEmbed,\n};\n"
  },
  {
    "path": "server/utils/chats/index.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst { WorkspaceChats } = require(\"../../models/workspaceChats\");\nconst { resetMemory } = require(\"./commands/reset\");\nconst { convertToPromptHistory } = require(\"../helpers/chat/responses\");\nconst { SlashCommandPresets } = require(\"../../models/slashCommandsPresets\");\nconst { SystemPromptVariables } = require(\"../../models/systemPromptVariables\");\n\nconst VALID_COMMANDS = {\n  \"/reset\": resetMemory,\n};\n\nasync function grepCommand(message, user = null) {\n  const userPresets = await SlashCommandPresets.getUserPresets(user?.id);\n  const availableCommands = Object.keys(VALID_COMMANDS);\n\n  // Check if the message starts with any built-in command\n  for (let i = 0; i < availableCommands.length; i++) {\n    const cmd = availableCommands[i];\n    const re = new RegExp(`^(${cmd})`, \"i\");\n    if (re.test(message)) {\n      return cmd;\n    }\n  }\n\n  // Replace all preset commands with their corresponding prompts\n  // Allows multiple commands in one message\n  let updatedMessage = message;\n  for (const preset of userPresets) {\n    const regex = new RegExp(\n      `(?:\\\\b\\\\s|^)(${preset.command})(?:\\\\b\\\\s|$)`,\n      \"g\"\n    );\n    updatedMessage = updatedMessage.replace(regex, preset.prompt);\n  }\n\n  return updatedMessage;\n}\n\n/**\n * @description This function will do recursive replacement of all slash commands with their corresponding prompts.\n * @notice This function is used for API calls and is not user-scoped. THIS FUNCTION DOES NOT SUPPORT PRESET COMMANDS.\n * @returns {Promise<string>}\n */\nasync function grepAllSlashCommands(message) {\n  const allPresets = await SlashCommandPresets.where({});\n\n  // Replace all preset commands with their corresponding prompts\n  // Allows multiple commands in one message\n  let updatedMessage = message;\n  for (const preset of allPresets) {\n    const regex = new RegExp(\n      `(?:\\\\b\\\\s|^)(${preset.command})(?:\\\\b\\\\s|$)`,\n      \"g\"\n    );\n    updatedMessage = updatedMessage.replace(regex, preset.prompt);\n  }\n\n  return updatedMessage;\n}\n\nasync function recentChatHistory({\n  user = null,\n  workspace,\n  thread = null,\n  messageLimit = 20,\n  apiSessionId = null,\n}) {\n  const rawHistory = (\n    await WorkspaceChats.where(\n      {\n        workspaceId: workspace.id,\n        user_id: user?.id || null,\n        thread_id: thread?.id || null,\n        api_session_id: apiSessionId || null,\n        include: true,\n      },\n      messageLimit,\n      { id: \"desc\" }\n    )\n  ).reverse();\n  return { rawHistory, chatHistory: convertToPromptHistory(rawHistory) };\n}\n\n/**\n * Returns the base prompt for the chat. This method will also do variable\n * substitution on the prompt if there are any defined variables in the prompt.\n * @param {Object|null} workspace - the workspace object\n * @param {Object|null} user - the user object\n * @returns {Promise<string>} - the base prompt\n */\nasync function chatPrompt(workspace, user = null) {\n  const { SystemSettings } = require(\"../../models/systemSettings\");\n  const basePrompt =\n    workspace?.openAiPrompt ?? SystemSettings.saneDefaultSystemPrompt;\n  return await SystemPromptVariables.expandSystemPromptVariables(\n    basePrompt,\n    user?.id,\n    workspace?.id\n  );\n}\n\n// We use this util function to deduplicate sources from similarity searching\n// if the document is already pinned.\n// Eg: You pin a csv, if we RAG + full-text that you will get the same data\n// points both in the full-text and possibly from RAG - result in bad results\n// even if the LLM was not even going to hallucinate.\nfunction sourceIdentifier(sourceDocument) {\n  if (!sourceDocument?.title || !sourceDocument?.published) return uuidv4();\n  return `title:${sourceDocument.title}-timestamp:${sourceDocument.published}`;\n}\n\nmodule.exports = {\n  sourceIdentifier,\n  recentChatHistory,\n  chatPrompt,\n  grepCommand,\n  grepAllSlashCommands,\n  VALID_COMMANDS,\n};\n"
  },
  {
    "path": "server/utils/chats/openaiCompatible.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst { DocumentManager } = require(\"../DocumentManager\");\nconst { WorkspaceChats } = require(\"../../models/workspaceChats\");\nconst { getVectorDbClass, getLLMProvider } = require(\"../helpers\");\nconst { writeResponseChunk } = require(\"../helpers/chat/responses\");\nconst { chatPrompt, sourceIdentifier } = require(\"./index\");\n\nconst { PassThrough } = require(\"stream\");\n\nasync function chatSync({\n  workspace,\n  systemPrompt = null,\n  history = [],\n  prompt = null,\n  attachments = [],\n  temperature = null,\n}) {\n  const uuid = uuidv4();\n  const chatMode = workspace?.chatMode ?? \"chat\";\n  const LLMConnector = getLLMProvider({\n    provider: workspace?.chatProvider,\n    model: workspace?.chatModel,\n  });\n  const VectorDb = getVectorDbClass();\n  const hasVectorizedSpace = await VectorDb.hasNamespace(workspace.slug);\n  const embeddingsCount = await VectorDb.namespaceCount(workspace.slug);\n\n  // User is trying to query-mode chat a workspace that has no data in it - so\n  // we should exit early as no information can be found under these conditions.\n  if ((!hasVectorizedSpace || embeddingsCount === 0) && chatMode === \"query\") {\n    const textResponse =\n      workspace?.queryRefusalResponse ??\n      \"There is no relevant information in this workspace to answer your query.\";\n\n    await WorkspaceChats.new({\n      workspaceId: workspace.id,\n      prompt: String(prompt),\n      response: {\n        text: textResponse,\n        sources: [],\n        type: chatMode,\n        attachments,\n      },\n      include: false,\n    });\n\n    return formatJSON(\n      {\n        id: uuid,\n        type: \"textResponse\",\n        sources: [],\n        close: true,\n        error: null,\n        textResponse,\n      },\n      { model: workspace.slug, finish_reason: \"abort\" }\n    );\n  }\n\n  // If we are here we know that we are in a workspace that is:\n  // 1. Chatting in \"chat\" mode and may or may _not_ have embeddings\n  // 2. Chatting in \"query\" mode and has at least 1 embedding\n  let contextTexts = [];\n  let sources = [];\n  let pinnedDocIdentifiers = [];\n  await new DocumentManager({\n    workspace,\n    maxTokens: LLMConnector.promptWindowLimit(),\n  })\n    .pinnedDocs()\n    .then((pinnedDocs) => {\n      pinnedDocs.forEach((doc) => {\n        const { pageContent, ...metadata } = doc;\n        pinnedDocIdentifiers.push(sourceIdentifier(doc));\n        contextTexts.push(doc.pageContent);\n        sources.push({\n          text:\n            pageContent.slice(0, 1_000) +\n            \"...continued on in source document...\",\n          ...metadata,\n        });\n      });\n    });\n\n  const vectorSearchResults =\n    embeddingsCount !== 0\n      ? await VectorDb.performSimilaritySearch({\n          namespace: workspace.slug,\n          input: String(prompt),\n          LLMConnector,\n          similarityThreshold: workspace?.similarityThreshold,\n          topN: workspace?.topN,\n          filterIdentifiers: pinnedDocIdentifiers,\n          rerank: workspace?.vectorSearchMode === \"rerank\",\n        })\n      : {\n          contextTexts: [],\n          sources: [],\n          message: null,\n        };\n\n  // Failed similarity search if it was run at all and failed.\n  if (!!vectorSearchResults.message) {\n    return formatJSON(\n      {\n        id: uuid,\n        type: \"abort\",\n        textResponse: null,\n        sources: [],\n        close: true,\n        error: vectorSearchResults.message,\n      },\n      { model: workspace.slug, finish_reason: \"abort\" }\n    );\n  }\n\n  // For OpenAI Compatible chats, we cannot do backfilling so we simply aggregate results here.\n  contextTexts = [...contextTexts, ...vectorSearchResults.contextTexts];\n  sources = [...sources, ...vectorSearchResults.sources];\n\n  // If in query mode and no context chunks are found from search, backfill, or pins -  do not\n  // let the LLM try to hallucinate a response or use general knowledge and exit early\n  if (chatMode === \"query\" && contextTexts.length === 0) {\n    const textResponse =\n      workspace?.queryRefusalResponse ??\n      \"There is no relevant information in this workspace to answer your query.\";\n\n    await WorkspaceChats.new({\n      workspaceId: workspace.id,\n      prompt: String(prompt),\n      response: {\n        text: textResponse,\n        sources: [],\n        type: chatMode,\n        attachments,\n      },\n      include: false,\n    });\n\n    return formatJSON(\n      {\n        id: uuid,\n        type: \"textResponse\",\n        sources: [],\n        close: true,\n        error: null,\n        textResponse,\n      },\n      { model: workspace.slug, finish_reason: \"no_content\" }\n    );\n  }\n\n  // Compress & Assemble message to ensure prompt passes token limit with room for response\n  // and build system messages based on inputs and history.\n  const messages = await LLMConnector.compressMessages({\n    systemPrompt: systemPrompt ?? (await chatPrompt(workspace)),\n    userPrompt: String(prompt),\n    contextTexts,\n    chatHistory: history,\n    attachments,\n  });\n\n  // Send the text completion.\n  const { textResponse, metrics } = await LLMConnector.getChatCompletion(\n    messages,\n    {\n      temperature:\n        temperature ?? workspace?.openAiTemp ?? LLMConnector.defaultTemp,\n    }\n  );\n\n  if (!textResponse) {\n    return formatJSON(\n      {\n        id: uuid,\n        type: \"textResponse\",\n        sources: [],\n        close: true,\n        error: \"No text completion could be completed with this input.\",\n        textResponse: null,\n      },\n      { model: workspace.slug, finish_reason: \"no_content\", usage: metrics }\n    );\n  }\n\n  const { chat } = await WorkspaceChats.new({\n    workspaceId: workspace.id,\n    prompt: String(prompt),\n    response: {\n      text: textResponse,\n      sources,\n      type: chatMode,\n      metrics,\n      attachments,\n    },\n  });\n\n  return formatJSON(\n    {\n      id: uuid,\n      type: \"textResponse\",\n      close: true,\n      error: null,\n      chatId: chat.id,\n      textResponse,\n      sources,\n    },\n    { model: workspace.slug, finish_reason: \"stop\", usage: metrics }\n  );\n}\n\nasync function streamChat({\n  workspace,\n  response,\n  systemPrompt = null,\n  history = [],\n  prompt = null,\n  attachments = [],\n  temperature = null,\n}) {\n  const uuid = uuidv4();\n  const chatMode = workspace?.chatMode ?? \"chat\";\n  const LLMConnector = getLLMProvider({\n    provider: workspace?.chatProvider,\n    model: workspace?.chatModel,\n  });\n  const VectorDb = getVectorDbClass();\n  const hasVectorizedSpace = await VectorDb.hasNamespace(workspace.slug);\n  const embeddingsCount = await VectorDb.namespaceCount(workspace.slug);\n\n  // We don't want to write a new method for every LLM to support openAI calls\n  // via the `handleStreamResponseV2` method handler. So here we create a passthrough\n  // that on writes to the main response, transforms the chunk to OpenAI format.\n  // The chunk is coming in the format from `writeResponseChunk` but in the AnythingLLM\n  // response chunk schema, so we here we mutate each chunk.\n  const responseInterceptor = new PassThrough({});\n  responseInterceptor.on(\"data\", (chunk) => {\n    try {\n      const originalData = JSON.parse(chunk.toString().split(\"data: \")[1]);\n      const modified = formatJSON(originalData, {\n        chunked: true,\n        model: workspace.slug,\n      }); // rewrite to OpenAI format\n      response.write(`data: ${JSON.stringify(modified)}\\n\\n`);\n    } catch (e) {\n      console.error(e);\n    }\n  });\n\n  // User is trying to query-mode chat a workspace that has no data in it - so\n  // we should exit early as no information can be found under these conditions.\n  if ((!hasVectorizedSpace || embeddingsCount === 0) && chatMode === \"query\") {\n    const textResponse =\n      workspace?.queryRefusalResponse ??\n      \"There is no relevant information in this workspace to answer your query.\";\n\n    await WorkspaceChats.new({\n      workspaceId: workspace.id,\n      prompt: String(prompt),\n      response: {\n        text: textResponse,\n        sources: [],\n        type: chatMode,\n        attachments,\n      },\n      include: false,\n    });\n\n    writeResponseChunk(\n      response,\n      formatJSON(\n        {\n          id: uuid,\n          type: \"textResponse\",\n          sources: [],\n          close: true,\n          error: null,\n          textResponse,\n        },\n        { chunked: true, model: workspace.slug, finish_reason: \"abort\" }\n      )\n    );\n    return;\n  }\n\n  // If we are here we know that we are in a workspace that is:\n  // 1. Chatting in \"chat\" mode and may or may _not_ have embeddings\n  // 2. Chatting in \"query\" mode and has at least 1 embedding\n  let contextTexts = [];\n  let sources = [];\n  let pinnedDocIdentifiers = [];\n  await new DocumentManager({\n    workspace,\n    maxTokens: LLMConnector.promptWindowLimit(),\n  })\n    .pinnedDocs()\n    .then((pinnedDocs) => {\n      pinnedDocs.forEach((doc) => {\n        const { pageContent, ...metadata } = doc;\n        pinnedDocIdentifiers.push(sourceIdentifier(doc));\n        contextTexts.push(doc.pageContent);\n        sources.push({\n          text:\n            pageContent.slice(0, 1_000) +\n            \"...continued on in source document...\",\n          ...metadata,\n        });\n      });\n    });\n\n  const vectorSearchResults =\n    embeddingsCount !== 0\n      ? await VectorDb.performSimilaritySearch({\n          namespace: workspace.slug,\n          input: String(prompt),\n          LLMConnector,\n          similarityThreshold: workspace?.similarityThreshold,\n          topN: workspace?.topN,\n          filterIdentifiers: pinnedDocIdentifiers,\n          rerank: workspace?.vectorSearchMode === \"rerank\",\n        })\n      : {\n          contextTexts: [],\n          sources: [],\n          message: null,\n        };\n\n  // Failed similarity search if it was run at all and failed.\n  if (!!vectorSearchResults.message) {\n    writeResponseChunk(\n      response,\n      formatJSON(\n        {\n          id: uuid,\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: vectorSearchResults.message,\n        },\n        { chunked: true, model: workspace.slug, finish_reason: \"abort\" }\n      )\n    );\n    return;\n  }\n\n  // For OpenAI Compatible chats, we cannot do backfilling so we simply aggregate results here.\n  contextTexts = [...contextTexts, ...vectorSearchResults.contextTexts];\n  sources = [...sources, ...vectorSearchResults.sources];\n\n  // If in query mode and no context chunks are found from search, backfill, or pins -  do not\n  // let the LLM try to hallucinate a response or use general knowledge and exit early\n  if (chatMode === \"query\" && contextTexts.length === 0) {\n    const textResponse =\n      workspace?.queryRefusalResponse ??\n      \"There is no relevant information in this workspace to answer your query.\";\n\n    await WorkspaceChats.new({\n      workspaceId: workspace.id,\n      prompt: String(prompt),\n      response: {\n        text: textResponse,\n        sources: [],\n        type: chatMode,\n        attachments,\n      },\n      include: false,\n    });\n\n    writeResponseChunk(\n      response,\n      formatJSON(\n        {\n          id: uuid,\n          type: \"textResponse\",\n          sources: [],\n          close: true,\n          error: null,\n          textResponse,\n        },\n        { chunked: true, model: workspace.slug, finish_reason: \"no_content\" }\n      )\n    );\n    return;\n  }\n\n  // Compress & Assemble message to ensure prompt passes token limit with room for response\n  // and build system messages based on inputs and history.\n  const messages = await LLMConnector.compressMessages({\n    systemPrompt: systemPrompt ?? (await chatPrompt(workspace)),\n    userPrompt: String(prompt),\n    contextTexts,\n    chatHistory: history,\n    attachments,\n  });\n\n  if (!LLMConnector.streamingEnabled()) {\n    writeResponseChunk(\n      response,\n      formatJSON(\n        {\n          id: uuid,\n          type: \"textResponse\",\n          sources: [],\n          close: true,\n          error: \"Streaming is not available for the connected LLM Provider\",\n          textResponse: null,\n        },\n        {\n          chunked: true,\n          model: workspace.slug,\n          finish_reason: \"streaming_disabled\",\n        }\n      )\n    );\n    return;\n  }\n\n  const stream = await LLMConnector.streamGetChatCompletion(messages, {\n    temperature:\n      temperature ?? workspace?.openAiTemp ?? LLMConnector.defaultTemp,\n  });\n  const completeText = await LLMConnector.handleStream(\n    responseInterceptor,\n    stream,\n    {\n      uuid,\n      sources,\n    }\n  );\n\n  if (completeText?.length > 0) {\n    const { chat } = await WorkspaceChats.new({\n      workspaceId: workspace.id,\n      prompt: String(prompt),\n      response: {\n        text: completeText,\n        sources,\n        type: chatMode,\n        metrics: stream.metrics,\n        attachments,\n      },\n    });\n\n    writeResponseChunk(\n      response,\n      formatJSON(\n        {\n          uuid,\n          type: \"finalizeResponseStream\",\n          close: true,\n          error: false,\n          chatId: chat.id,\n          textResponse: \"\",\n        },\n        {\n          chunked: true,\n          model: workspace.slug,\n          finish_reason: \"stop\",\n          usage: stream.metrics,\n        }\n      )\n    );\n    return;\n  }\n\n  writeResponseChunk(\n    response,\n    formatJSON(\n      {\n        uuid,\n        type: \"finalizeResponseStream\",\n        close: true,\n        error: false,\n        textResponse: \"\",\n      },\n      {\n        chunked: true,\n        model: workspace.slug,\n        finish_reason: \"stop\",\n        usage: stream.metrics,\n      }\n    )\n  );\n  return;\n}\n\nfunction formatJSON(\n  chat,\n  { chunked = false, model, finish_reason = null, usage = {} }\n) {\n  const data = {\n    id: chat.uuid ?? chat.id,\n    object: \"chat.completion\",\n    created: Math.floor(Number(new Date()) / 1000),\n    model: model,\n    choices: [\n      {\n        index: 0,\n        [chunked ? \"delta\" : \"message\"]: {\n          role: \"assistant\",\n          content: chat.textResponse,\n        },\n        logprobs: null,\n        finish_reason: finish_reason,\n      },\n    ],\n    usage,\n  };\n\n  return data;\n}\n\nmodule.exports.OpenAICompatibleChat = {\n  chatSync,\n  streamChat,\n};\n"
  },
  {
    "path": "server/utils/chats/stream.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst { DocumentManager } = require(\"../DocumentManager\");\nconst { WorkspaceChats } = require(\"../../models/workspaceChats\");\nconst { WorkspaceParsedFiles } = require(\"../../models/workspaceParsedFiles\");\nconst { getVectorDbClass, getLLMProvider } = require(\"../helpers\");\nconst { writeResponseChunk } = require(\"../helpers/chat/responses\");\nconst { grepAgents } = require(\"./agents\");\nconst {\n  grepCommand,\n  VALID_COMMANDS,\n  chatPrompt,\n  recentChatHistory,\n  sourceIdentifier,\n} = require(\"./index\");\n\nconst VALID_CHAT_MODE = [\"automatic\", \"chat\", \"query\"];\n\nasync function streamChatWithWorkspace(\n  response,\n  workspace,\n  message,\n  chatMode = \"chat\",\n  user = null,\n  thread = null,\n  attachments = []\n) {\n  const uuid = uuidv4();\n  const updatedMessage = await grepCommand(message, user);\n\n  if (Object.keys(VALID_COMMANDS).includes(updatedMessage)) {\n    const data = await VALID_COMMANDS[updatedMessage](\n      workspace,\n      message,\n      uuid,\n      user,\n      thread\n    );\n    writeResponseChunk(response, data);\n    return;\n  }\n\n  // If is agent enabled chat we will exit this flow early.\n  const isAgentChat = await grepAgents({\n    uuid,\n    response,\n    message: updatedMessage,\n    user,\n    workspace,\n    thread,\n    attachments,\n  });\n  if (isAgentChat) return;\n\n  const LLMConnector = getLLMProvider({\n    provider: workspace?.chatProvider,\n    model: workspace?.chatModel,\n  });\n  const VectorDb = getVectorDbClass();\n\n  const messageLimit = workspace?.openAiHistory || 20;\n  const hasVectorizedSpace = await VectorDb.hasNamespace(workspace.slug);\n  const embeddingsCount = await VectorDb.namespaceCount(workspace.slug);\n\n  // User is trying to query-mode chat a workspace that has no data in it - so\n  // we should exit early as no information can be found under these conditions.\n  if ((!hasVectorizedSpace || embeddingsCount === 0) && chatMode === \"query\") {\n    const textResponse =\n      workspace?.queryRefusalResponse ??\n      \"There is no relevant information in this workspace to answer your query.\";\n    writeResponseChunk(response, {\n      id: uuid,\n      type: \"textResponse\",\n      textResponse,\n      sources: [],\n      attachments,\n      close: true,\n      error: null,\n    });\n    await WorkspaceChats.new({\n      workspaceId: workspace.id,\n      prompt: message,\n      response: {\n        text: textResponse,\n        sources: [],\n        type: chatMode,\n        attachments,\n      },\n      threadId: thread?.id || null,\n      include: false,\n      user,\n    });\n    return;\n  }\n\n  // If we are here we know that we are in a workspace that is:\n  // 1. Chatting in \"chat\" mode and may or may _not_ have embeddings\n  // 2. Chatting in \"query\" mode and has at least 1 embedding\n  let completeText;\n  let metrics = {};\n  let contextTexts = [];\n  let sources = [];\n  let pinnedDocIdentifiers = [];\n  const { rawHistory, chatHistory } = await recentChatHistory({\n    user,\n    workspace,\n    thread,\n    messageLimit,\n  });\n\n  // Look for pinned documents and see if the user decided to use this feature. We will also do a vector search\n  // as pinning is a supplemental tool but it should be used with caution since it can easily blow up a context window.\n  // However we limit the maximum of appended context to 80% of its overall size, mostly because if it expands beyond this\n  // it will undergo prompt compression anyway to make it work. If there is so much pinned that the context here is bigger than\n  // what the model can support - it would get compressed anyway and that really is not the point of pinning. It is really best\n  // suited for high-context models.\n  await new DocumentManager({\n    workspace,\n    maxTokens: LLMConnector.promptWindowLimit(),\n  })\n    .pinnedDocs()\n    .then((pinnedDocs) => {\n      pinnedDocs.forEach((doc) => {\n        const { pageContent, ...metadata } = doc;\n        pinnedDocIdentifiers.push(sourceIdentifier(doc));\n        contextTexts.push(doc.pageContent);\n        sources.push({\n          text:\n            pageContent.slice(0, 1_000) +\n            \"...continued on in source document...\",\n          ...metadata,\n        });\n      });\n    });\n\n  // Inject any parsed files for this workspace/thread/user\n  const parsedFiles = await WorkspaceParsedFiles.getContextFiles(\n    workspace,\n    thread || null,\n    user || null\n  );\n  parsedFiles.forEach((doc) => {\n    const { pageContent, ...metadata } = doc;\n    contextTexts.push(doc.pageContent);\n    sources.push({\n      text:\n        pageContent.slice(0, 1_000) + \"...continued on in source document...\",\n      ...metadata,\n    });\n  });\n\n  const vectorSearchResults =\n    embeddingsCount !== 0\n      ? await VectorDb.performSimilaritySearch({\n          namespace: workspace.slug,\n          input: updatedMessage,\n          LLMConnector,\n          similarityThreshold: workspace?.similarityThreshold,\n          topN: workspace?.topN,\n          filterIdentifiers: pinnedDocIdentifiers,\n          rerank: workspace?.vectorSearchMode === \"rerank\",\n        })\n      : {\n          contextTexts: [],\n          sources: [],\n          message: null,\n        };\n\n  // Failed similarity search if it was run at all and failed.\n  if (!!vectorSearchResults.message) {\n    writeResponseChunk(response, {\n      id: uuid,\n      type: \"abort\",\n      textResponse: null,\n      sources: [],\n      close: true,\n      error: vectorSearchResults.message,\n    });\n    return;\n  }\n\n  const { fillSourceWindow } = require(\"../helpers/chat\");\n  const filledSources = fillSourceWindow({\n    nDocs: workspace?.topN || 4,\n    searchResults: vectorSearchResults.sources,\n    history: rawHistory,\n    filterIdentifiers: pinnedDocIdentifiers,\n  });\n\n  // Why does contextTexts get all the info, but sources only get current search?\n  // This is to give the ability of the LLM to \"comprehend\" a contextual response without\n  // populating the Citations under a response with documents the user \"thinks\" are irrelevant\n  // due to how we manage backfilling of the context to keep chats with the LLM more correct in responses.\n  // If a past citation was used to answer the question - that is visible in the history so it logically makes sense\n  // and does not appear to the user that a new response used information that is otherwise irrelevant for a given prompt.\n  // TLDR; reduces GitHub issues for \"LLM citing document that has no answer in it\" while keep answers highly accurate.\n  contextTexts = [...contextTexts, ...filledSources.contextTexts];\n  sources = [...sources, ...vectorSearchResults.sources];\n\n  // If in query mode and no context chunks are found from search, backfill, or pins -  do not\n  // let the LLM try to hallucinate a response or use general knowledge and exit early\n  if (chatMode === \"query\" && contextTexts.length === 0) {\n    const textResponse =\n      workspace?.queryRefusalResponse ??\n      \"There is no relevant information in this workspace to answer your query.\";\n    writeResponseChunk(response, {\n      id: uuid,\n      type: \"textResponse\",\n      textResponse,\n      sources: [],\n      close: true,\n      error: null,\n    });\n\n    await WorkspaceChats.new({\n      workspaceId: workspace.id,\n      prompt: message,\n      response: {\n        text: textResponse,\n        sources: [],\n        type: chatMode,\n        attachments,\n      },\n      threadId: thread?.id || null,\n      include: false,\n      user,\n    });\n    return;\n  }\n\n  // Compress & Assemble message to ensure prompt passes token limit with room for response\n  // and build system messages based on inputs and history.\n  const messages = await LLMConnector.compressMessages(\n    {\n      systemPrompt: await chatPrompt(workspace, user),\n      userPrompt: updatedMessage,\n      contextTexts,\n      chatHistory,\n      attachments,\n    },\n    rawHistory\n  );\n\n  // If streaming is not explicitly enabled for connector\n  // we do regular waiting of a response and send a single chunk.\n  if (LLMConnector.streamingEnabled() !== true) {\n    console.log(\n      `\\x1b[31m[STREAMING DISABLED]\\x1b[0m Streaming is not available for ${LLMConnector.constructor.name}. Will use regular chat method.`\n    );\n    const { textResponse, metrics: performanceMetrics } =\n      await LLMConnector.getChatCompletion(messages, {\n        temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp,\n        user: user,\n      });\n\n    completeText = textResponse;\n    metrics = performanceMetrics;\n    writeResponseChunk(response, {\n      uuid,\n      sources,\n      type: \"textResponseChunk\",\n      textResponse: completeText,\n      close: true,\n      error: false,\n      metrics,\n    });\n  } else {\n    const stream = await LLMConnector.streamGetChatCompletion(messages, {\n      temperature: workspace?.openAiTemp ?? LLMConnector.defaultTemp,\n      user: user,\n    });\n    completeText = await LLMConnector.handleStream(response, stream, {\n      uuid,\n      sources,\n    });\n    metrics = stream.metrics;\n  }\n\n  if (completeText?.length > 0) {\n    const { chat } = await WorkspaceChats.new({\n      workspaceId: workspace.id,\n      prompt: message,\n      response: {\n        text: completeText,\n        sources,\n        type: chatMode,\n        attachments,\n        metrics,\n      },\n      threadId: thread?.id || null,\n      user,\n    });\n\n    writeResponseChunk(response, {\n      uuid,\n      type: \"finalizeResponseStream\",\n      close: true,\n      error: false,\n      chatId: chat.id,\n      metrics,\n    });\n    return;\n  }\n\n  writeResponseChunk(response, {\n    uuid,\n    type: \"finalizeResponseStream\",\n    close: true,\n    error: false,\n    metrics,\n  });\n  return;\n}\n\nmodule.exports = {\n  VALID_CHAT_MODE,\n  streamChatWithWorkspace,\n};\n"
  },
  {
    "path": "server/utils/collectorApi/index.js",
    "content": "const { EncryptionManager } = require(\"../EncryptionManager\");\nconst { Agent } = require(\"undici\");\n\n/**\n * @typedef {Object} CollectorOptions\n * @property {string} whisperProvider - The provider to use for whisper, defaults to \"local\"\n * @property {string} WhisperModelPref - The model to use for whisper if set.\n * @property {string} openAiKey - The API key to use for OpenAI interfacing, mostly passed to OAI Whisper provider.\n * @property {Object} ocr - The OCR options\n * @property {{allowAnyIp: \"true\"|null|undefined}} runtimeSettings - The runtime settings that are passed to the collector. Persisted across requests.\n */\n\n// When running locally will occupy the 0.0.0.0 hostname space but when deployed inside\n// of docker this endpoint is not exposed so it is only on the Docker instances internal network\n// so no additional security is needed on the endpoint directly. Auth is done however by the express\n// middleware prior to leaving the node-side of the application so that is good enough >:)\nclass CollectorApi {\n  /** @type {number} - The maximum timeout for extension requests in milliseconds */\n  extensionRequestTimeout = 15 * 60_000; // 15 minutes\n  /** @type {Agent} - The agent for extension requests */\n  extensionRequestAgent = new Agent({\n    headersTimeout: this.extensionRequestTimeout,\n    bodyTimeout: this.extensionRequestTimeout,\n  });\n\n  constructor() {\n    const { CommunicationKey } = require(\"../comKey\");\n    this.comkey = new CommunicationKey();\n    this.endpoint = `http://0.0.0.0:${process.env.COLLECTOR_PORT || 8888}`;\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[CollectorApi]\\x1b[0m ${text}`, ...args);\n  }\n\n  /**\n   * Attach options to the request passed to the collector API\n   * @returns {CollectorOptions}\n   */\n  #attachOptions() {\n    return {\n      whisperProvider: process.env.WHISPER_PROVIDER || \"local\",\n      WhisperModelPref: process.env.WHISPER_MODEL_PREF,\n      openAiKey: process.env.OPEN_AI_KEY || null,\n      ocr: {\n        langList: process.env.TARGET_OCR_LANG || \"eng\",\n      },\n      runtimeSettings: {\n        allowAnyIp: process.env.COLLECTOR_ALLOW_ANY_IP ?? \"false\",\n        browserLaunchArgs: process.env.ANYTHINGLLM_CHROMIUM_ARGS ?? [],\n      },\n    };\n  }\n\n  async online() {\n    return await fetch(this.endpoint)\n      .then((res) => res.ok)\n      .catch(() => false);\n  }\n\n  async acceptedFileTypes() {\n    return await fetch(`${this.endpoint}/accepts`)\n      .then((res) => {\n        if (!res.ok) throw new Error(\"failed to GET /accepts\");\n        return res.json();\n      })\n      .then((res) => res)\n      .catch((e) => {\n        this.log(e.message);\n        return null;\n      });\n  }\n\n  /**\n   * Process a document\n   * - Will append the options and optional metadata to the request body\n   * @param {string} filename - The filename of the document to process\n   * @param {Object} metadata - Optional metadata key:value pairs\n   * @returns {Promise<Object>} - The response from the collector API\n   */\n  async processDocument(filename = \"\", metadata = {}) {\n    if (!filename) return false;\n\n    const data = JSON.stringify({\n      filename,\n      metadata,\n      options: this.#attachOptions(),\n    });\n\n    return await fetch(`${this.endpoint}/process`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"X-Integrity\": this.comkey.sign(data),\n        \"X-Payload-Signer\": this.comkey.encrypt(\n          new EncryptionManager().xPayload\n        ),\n      },\n      body: data,\n      dispatcher: new Agent({ headersTimeout: 600000 }),\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Response could not be completed\");\n        return res.json();\n      })\n      .then((res) => res)\n      .catch((e) => {\n        this.log(e.message);\n        return { success: false, reason: e.message, documents: [] };\n      });\n  }\n\n  /**\n   * Process a link\n   * - Will append the options to the request body\n   * @param {string} link - The link to process\n   * @param {{[key: string]: string}} scraperHeaders - Custom headers to apply to the web-scraping request URL\n   * @param {[key: string]: string} metadata - Optional metadata to attach to the document\n   * @returns {Promise<Object>} - The response from the collector API\n   */\n  async processLink(link = \"\", scraperHeaders = {}, metadata = {}) {\n    if (!link) return false;\n\n    const data = JSON.stringify({\n      link,\n      scraperHeaders,\n      options: this.#attachOptions(),\n      metadata: metadata,\n    });\n\n    return await fetch(`${this.endpoint}/process-link`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"X-Integrity\": this.comkey.sign(data),\n        \"X-Payload-Signer\": this.comkey.encrypt(\n          new EncryptionManager().xPayload\n        ),\n      },\n      body: data,\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Response could not be completed\");\n        return res.json();\n      })\n      .then((res) => res)\n      .catch((e) => {\n        this.log(e.message);\n        return { success: false, reason: e.message, documents: [] };\n      });\n  }\n\n  /**\n   * Process raw text as a document for the collector\n   * - Will append the options to the request body\n   * @param {string} textContent - The text to process\n   * @param {[key: string]: string} metadata - The metadata to process\n   * @returns {Promise<Object>} - The response from the collector API\n   */\n  async processRawText(textContent = \"\", metadata = {}) {\n    const data = JSON.stringify({\n      textContent,\n      metadata,\n      options: this.#attachOptions(),\n    });\n    return await fetch(`${this.endpoint}/process-raw-text`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"X-Integrity\": this.comkey.sign(data),\n        \"X-Payload-Signer\": this.comkey.encrypt(\n          new EncryptionManager().xPayload\n        ),\n      },\n      body: data,\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Response could not be completed\");\n        return res.json();\n      })\n      .then((res) => res)\n      .catch((e) => {\n        this.log(e.message);\n        return { success: false, reason: e.message, documents: [] };\n      });\n  }\n\n  // We will not ever expose the document processor to the frontend API so instead we relay\n  // all requests through the server. You can use this function to directly expose a specific endpoint\n  // on the document processor.\n  async forwardExtensionRequest({ endpoint, method, body }) {\n    const data = typeof body === \"string\" ? body : JSON.stringify(body);\n    return await fetch(`${this.endpoint}${endpoint}`, {\n      method,\n      body: data,\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"X-Integrity\": this.comkey.sign(data),\n        \"X-Payload-Signer\": this.comkey.encrypt(\n          new EncryptionManager().xPayload\n        ),\n      },\n      // Extensions do a lot of work, and may take a while to complete so we need to increase the timeout\n      // substantially so that they do not show a failure to the user early.\n      dispatcher: this.extensionRequestAgent,\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Response could not be completed\");\n        return res.json();\n      })\n      .then((res) => res)\n      .catch((e) => {\n        this.log(e.message);\n        return { success: false, data: {}, reason: e.message };\n      });\n  }\n\n  /**\n   * Get the content of a link only in a specific format\n   * - Will append the options to the request body\n   * @param {string} link - The link to get the content of\n   * @param {\"text\"|\"html\"} captureAs - The format to capture the content as\n   * @returns {Promise<Object>} - The response from the collector API\n   */\n  async getLinkContent(link = \"\", captureAs = \"text\") {\n    if (!link) return false;\n\n    const data = JSON.stringify({\n      link,\n      captureAs,\n      options: this.#attachOptions(),\n    });\n    return await fetch(`${this.endpoint}/util/get-link`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"X-Integrity\": this.comkey.sign(data),\n        \"X-Payload-Signer\": this.comkey.encrypt(\n          new EncryptionManager().xPayload\n        ),\n      },\n      body: data,\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Response could not be completed\");\n        return res.json();\n      })\n      .then((res) => res)\n      .catch((e) => {\n        this.log(e.message);\n        return { success: false, content: null };\n      });\n  }\n\n  /**\n   * Parse a document without processing it\n   * - Will append the options to the request body\n   * @param {string} filename - The filename of the document to parse\n   * @returns {Promise<Object>} - The response from the collector API\n   */\n  async parseDocument(filename = \"\") {\n    if (!filename) return false;\n\n    const data = JSON.stringify({\n      filename,\n      options: this.#attachOptions(),\n    });\n\n    return await fetch(`${this.endpoint}/parse`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"X-Integrity\": this.comkey.sign(data),\n        \"X-Payload-Signer\": this.comkey.encrypt(\n          new EncryptionManager().xPayload\n        ),\n      },\n      body: data,\n    })\n      .then((res) => {\n        if (!res.ok) throw new Error(\"Response could not be completed\");\n        return res.json();\n      })\n      .then((res) => res)\n      .catch((e) => {\n        this.log(e.message);\n        return { success: false, reason: e.message, documents: [] };\n      });\n  }\n}\n\nmodule.exports.CollectorApi = CollectorApi;\n"
  },
  {
    "path": "server/utils/comKey/index.js",
    "content": "const crypto = require(\"crypto\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst keyPath =\n  process.env.NODE_ENV === \"development\"\n    ? path.resolve(__dirname, `../../storage/comkey`)\n    : path.resolve(\n        process.env.STORAGE_DIR ?? path.resolve(__dirname, `../../storage`),\n        `comkey`\n      );\n\n// What does this class do?\n// This class generates a hashed version of some text (typically a JSON payload) using a rolling RSA key\n// that can then be appended as a header value to do integrity checking on a payload. Given the\n// nature of this class and that keys are rolled constantly, this protects the request\n// integrity of requests sent to the collector as only the server can sign these requests.\n// This keeps accidental misconfigurations of AnythingLLM that leaving port 8888 open from\n// being abused or SSRF'd by users scraping malicious sites who have a loopback embedded in a <script>, for example.\n// Since each request to the collector must be signed to be valid, unsigned requests directly to the collector\n// will be dropped and must go through the /server endpoint directly.\nclass CommunicationKey {\n  #privKeyName = \"ipc-priv.pem\";\n  #pubKeyName = \"ipc-pub.pem\";\n  #storageLoc = keyPath;\n\n  // Init the class and determine if keys should be rolled.\n  // This typically occurs on boot up so key is fresh each boot.\n  constructor(generate = false) {\n    if (generate) this.#generate();\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[36m[CommunicationKey]\\x1b[0m ${text}`, ...args);\n  }\n\n  #readPrivateKey() {\n    return fs.readFileSync(path.resolve(this.#storageLoc, this.#privKeyName));\n  }\n\n  #generate() {\n    const keyPair = crypto.generateKeyPairSync(\"rsa\", {\n      modulusLength: 2048,\n      publicKeyEncoding: {\n        type: \"pkcs1\",\n        format: \"pem\",\n      },\n      privateKeyEncoding: {\n        type: \"pkcs1\",\n        format: \"pem\",\n      },\n    });\n\n    if (!fs.existsSync(this.#storageLoc))\n      fs.mkdirSync(this.#storageLoc, { recursive: true });\n    fs.writeFileSync(\n      `${path.resolve(this.#storageLoc, this.#privKeyName)}`,\n      keyPair.privateKey\n    );\n    fs.writeFileSync(\n      `${path.resolve(this.#storageLoc, this.#pubKeyName)}`,\n      keyPair.publicKey\n    );\n    this.log(\n      \"RSA key pair generated for signed payloads within AnythingLLM services.\"\n    );\n  }\n\n  // This instance of ComKey on server is intended for generation of Priv/Pub key for signing and decoding.\n  // this resource is shared with /collector/ via a class of the same name in /utils which does decoding/verification only\n  // while this server class only does signing with the private key.\n  sign(textData = \"\") {\n    return crypto\n      .sign(\"RSA-SHA256\", Buffer.from(textData), this.#readPrivateKey())\n      .toString(\"hex\");\n  }\n\n  // Use the rolling priv-key to encrypt arbitrary data that is text\n  // returns the encrypted content as a base64 string.\n  encrypt(textData = \"\") {\n    return crypto\n      .privateEncrypt(this.#readPrivateKey(), Buffer.from(textData, \"utf-8\"))\n      .toString(\"base64\");\n  }\n}\n\nmodule.exports = { CommunicationKey };\n"
  },
  {
    "path": "server/utils/database/index.js",
    "content": "const { getGitVersion } = require(\"../../endpoints/utils\");\nconst { Telemetry } = require(\"../../models/telemetry\");\n\nfunction checkColumnTemplate(tablename = null, column = null) {\n  if (!tablename || !column)\n    throw new Error(`Migration Error`, { tablename, column });\n  return `SELECT COUNT(*) AS _exists FROM pragma_table_info('${tablename}') WHERE name='${column}'`;\n}\n\n// Note (tcarambat): Since there is no good way to track migrations in Node/SQLite we use this simple system\n// Each model has a `migrations` method that will return an array like...\n// { colName: 'stringColName', execCmd: `SQL Command to run when`, doif: boolean },\n// colName = name of column\n// execCmd = Command to run when doif matches the state of the DB\n// doif = condition to match that determines if execCmd will run.\n// eg: Table workspace has slug column.\n// execCmd: ALTER TABLE DROP COLUMN slug;\n// doif: true\n// => Will drop the slug column if the workspace table has a column named 'slug' otherwise nothing happens.\n// If you are adding a new table column if needs to exist in the Models `colsInit` and as a migration.\n// So both new and existing DBs will get the column when code is pulled in.\n\nasync function checkForMigrations(model, db) {\n  if (model.migrations().length === 0) return;\n  const toMigrate = [];\n  for (const { colName, execCmd, doif } of model.migrations()) {\n    const { _exists } = await db.get(\n      checkColumnTemplate(model.tablename, colName)\n    );\n    const colExists = _exists !== 0;\n    if (colExists !== doif) continue;\n\n    toMigrate.push(execCmd);\n  }\n\n  if (toMigrate.length === 0) return;\n\n  console.log(`Running ${toMigrate.length} migrations`, toMigrate);\n  await db.exec(toMigrate.join(\";\\n\"));\n  return;\n}\n\n// Note(tcarambat): When building in production via Docker the SQLite file will not exist\n// and if this function tries to run on boot the file will not exist\n// and the server will abort and the container will exit.\n// This function will run each reload on dev but on production\n// it will be stubbed until the /api/migrate endpoint is GET.\nasync function validateTablePragmas(force = false) {\n  try {\n    if (process.env.NODE_ENV !== \"development\" && force === false) {\n      console.log(\n        `\\x1b[34m[MIGRATIONS STUBBED]\\x1b[0m Please ping /migrate once server starts to run migrations`\n      );\n      return;\n    }\n    const { SystemSettings } = require(\"../../models/systemSettings\");\n    const { User } = require(\"../../models/user\");\n    const { Workspace } = require(\"../../models/workspace\");\n    const { WorkspaceUser } = require(\"../../models/workspaceUsers\");\n    const { Document } = require(\"../../models/documents\");\n    const { DocumentVectors } = require(\"../../models/vectors\");\n    const { WorkspaceChats } = require(\"../../models/workspaceChats\");\n    const { Invite } = require(\"../../models/invite\");\n    const { ApiKey } = require(\"../../models/apiKeys\");\n\n    await SystemSettings.migrateTable();\n    await User.migrateTable();\n    await Workspace.migrateTable();\n    await WorkspaceUser.migrateTable();\n    await Document.migrateTable();\n    await DocumentVectors.migrateTable();\n    await WorkspaceChats.migrateTable();\n    await Invite.migrateTable();\n    await ApiKey.migrateTable();\n  } catch (e) {\n    console.error(`validateTablePragmas: Migrations failed`, e);\n  }\n  return;\n}\n\n// Telemetry is anonymized and your data is never read. This can be disabled by setting\n// DISABLE_TELEMETRY=true in the `.env` of however you setup. Telemetry helps us determine use\n// of how AnythingLLM is used and how to improve this product!\n// You can see all Telemetry events by ctrl+f `Telemetry.sendTelemetry` calls to verify this claim.\nasync function setupTelemetry() {\n  if (process.env.DISABLE_TELEMETRY === \"true\") {\n    console.log(\n      `\\x1b[31m[TELEMETRY DISABLED]\\x1b[0m Telemetry is marked as disabled - no events will send. Telemetry helps Mintplex Labs Inc improve AnythingLLM.`\n    );\n    return true;\n  }\n\n  if (Telemetry.isDev()) {\n    console.log(\n      `\\x1b[33m[TELEMETRY STUBBED]\\x1b[0m Anonymous Telemetry stubbed in development.`\n    );\n    return;\n  }\n\n  console.log(\n    `\\x1b[32m[TELEMETRY ENABLED]\\x1b[0m Anonymous Telemetry enabled. Telemetry helps Mintplex Labs Inc improve AnythingLLM.`\n  );\n  await Telemetry.findOrCreateId();\n  await Telemetry.sendTelemetry(\"server_boot\", {\n    commit: getGitVersion(),\n  });\n  return;\n}\n\nmodule.exports = {\n  checkForMigrations,\n  validateTablePragmas,\n  setupTelemetry,\n};\n"
  },
  {
    "path": "server/utils/files/index.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { v5: uuidv5 } = require(\"uuid\");\nconst { Document } = require(\"../../models/documents\");\nconst { DocumentSyncQueue } = require(\"../../models/documentSyncQueue\");\nconst documentsPath =\n  process.env.NODE_ENV === \"development\"\n    ? path.resolve(__dirname, `../../storage/documents`)\n    : path.resolve(process.env.STORAGE_DIR, `documents`);\nconst directUploadsPath =\n  process.env.NODE_ENV === \"development\"\n    ? path.resolve(__dirname, `../../storage/direct-uploads`)\n    : path.resolve(process.env.STORAGE_DIR, `direct-uploads`);\nconst vectorCachePath =\n  process.env.NODE_ENV === \"development\"\n    ? path.resolve(__dirname, `../../storage/vector-cache`)\n    : path.resolve(process.env.STORAGE_DIR, `vector-cache`);\nconst hotdirPath =\n  process.env.NODE_ENV === \"development\"\n    ? path.resolve(__dirname, `../../../collector/hotdir`)\n    : path.resolve(process.env.STORAGE_DIR, `../../collector/hotdir`);\n\n// Should take in a folder that is a subfolder of documents\n// eg: youtube-subject/video-123.json\nasync function fileData(filePath = null) {\n  if (!filePath) throw new Error(\"No docPath provided in request\");\n  const fullFilePath = path.resolve(documentsPath, normalizePath(filePath));\n  if (!fs.existsSync(fullFilePath) || !isWithin(documentsPath, fullFilePath))\n    return null;\n\n  const data = fs.readFileSync(fullFilePath, \"utf8\");\n  return JSON.parse(data);\n}\n\nasync function viewLocalFiles() {\n  if (!fs.existsSync(documentsPath)) fs.mkdirSync(documentsPath);\n  const liveSyncAvailable = await DocumentSyncQueue.enabled();\n  const directory = {\n    name: \"documents\",\n    type: \"folder\",\n    items: [],\n  };\n\n  for (const file of fs.readdirSync(documentsPath)) {\n    if (path.extname(file) === \".md\") continue;\n    const folderPath = path.resolve(documentsPath, file);\n    const isFolder = fs.lstatSync(folderPath).isDirectory();\n    if (isFolder) {\n      const subdocs = {\n        name: file,\n        type: \"folder\",\n        items: [],\n      };\n\n      const subfiles = fs.readdirSync(folderPath);\n      const filenames = {};\n      const filePromises = [];\n\n      for (let i = 0; i < subfiles.length; i++) {\n        const subfile = subfiles[i];\n        const cachefilename = `${file}/${subfile}`;\n        if (path.extname(subfile) !== \".json\") continue;\n        filePromises.push(\n          fileToPickerData({\n            pathToFile: path.join(folderPath, subfile),\n            liveSyncAvailable,\n            cachefilename,\n          })\n        );\n        filenames[cachefilename] = subfile;\n      }\n      const results = await Promise.all(filePromises)\n        .then((results) => results.filter((i) => !!i)) // Remove null results\n        .then((results) => results.filter((i) => hasRequiredMetadata(i))); // Remove invalid file structures\n      subdocs.items.push(...results);\n\n      // Grab the pinned workspaces and watched documents for this folder's documents\n      // at the time of the query so we don't have to re-query the database for each file\n      const pinnedWorkspacesByDocument =\n        await getPinnedWorkspacesByDocument(filenames);\n      const watchedDocumentsFilenames =\n        await getWatchedDocumentFilenames(filenames);\n      for (const item of subdocs.items) {\n        item.pinnedWorkspaces = pinnedWorkspacesByDocument[item.name] || [];\n        item.watched =\n          watchedDocumentsFilenames.hasOwnProperty(item.name) || false;\n      }\n\n      directory.items.push(subdocs);\n    }\n  }\n\n  // Make sure custom-documents is always the first folder in picker\n  directory.items = [\n    directory.items.find((folder) => folder.name === \"custom-documents\"),\n    ...directory.items.filter((folder) => folder.name !== \"custom-documents\"),\n  ].filter((i) => !!i);\n\n  return directory;\n}\n\n/**\n * Gets the documents by folder name.\n * @param {string} folderName - The name of the folder to get the documents from.\n * @returns {Promise<{folder: string, documents: any[], code: number, error: string}>} - The documents by folder name.\n */\nasync function getDocumentsByFolder(folderName = \"\") {\n  if (!folderName) {\n    return {\n      folder: folderName,\n      documents: [],\n      code: 400,\n      error: \"Folder name must be provided.\",\n    };\n  }\n\n  const folderPath = path.resolve(documentsPath, normalizePath(folderName));\n  if (\n    !isWithin(documentsPath, folderPath) ||\n    !fs.existsSync(folderPath) ||\n    !fs.lstatSync(folderPath).isDirectory()\n  ) {\n    return {\n      folder: folderName,\n      documents: [],\n      code: 404,\n      error: `Folder \"${folderName}\" does not exist.`,\n    };\n  }\n\n  const documents = [];\n  const filenames = {};\n  const files = fs.readdirSync(folderPath);\n  for (const file of files) {\n    if (path.extname(file) !== \".json\") continue;\n    const filePath = path.join(folderPath, file);\n    const rawData = fs.readFileSync(filePath, \"utf8\");\n    const cachefilename = `${folderName}/${file}`;\n    const { pageContent: _pageContent, ...metadata } = JSON.parse(rawData);\n    documents.push({\n      name: file,\n      type: \"file\",\n      ...metadata,\n      cached: await cachedVectorInformation(cachefilename, true),\n    });\n    filenames[cachefilename] = file;\n  }\n\n  // Get pinned and watched information for each document in the folder\n  const pinnedWorkspacesByDocument =\n    await getPinnedWorkspacesByDocument(filenames);\n  const watchedDocumentsFilenames =\n    await getWatchedDocumentFilenames(filenames);\n  for (let doc of documents) {\n    doc.pinnedWorkspaces = pinnedWorkspacesByDocument[doc.name] || [];\n    doc.watched = Object.prototype.hasOwnProperty.call(\n      watchedDocumentsFilenames,\n      doc.name\n    );\n  }\n\n  return { folder: folderName, documents, code: 200, error: null };\n}\n\n/**\n * Searches the vector-cache folder for existing information so we dont have to re-embed a\n * document and can instead push directly to vector db.\n * @param {string} filename - the filename to check for cached vector information\n * @param {boolean} checkOnly - if true, only check if the file exists, do not return the cached data\n * @returns {Promise<{exists: boolean, chunks: any[]}>} - a promise that resolves to an object containing the existence of the file and its cached chunks\n */\nasync function cachedVectorInformation(filename = null, checkOnly = false) {\n  if (!filename) return checkOnly ? false : { exists: false, chunks: [] };\n\n  const digest = uuidv5(filename, uuidv5.URL);\n  const file = path.resolve(vectorCachePath, `${digest}.json`);\n  const exists = fs.existsSync(file);\n\n  if (checkOnly) return exists;\n  if (!exists) return { exists, chunks: [] };\n\n  console.log(\n    `Cached vectorized results of ${filename} found! Using cached data to save on embed costs.`\n  );\n  const rawData = fs.readFileSync(file, \"utf8\");\n  return { exists: true, chunks: JSON.parse(rawData) };\n}\n\n// vectorData: pre-chunked vectorized data for a given file that includes the proper metadata and chunk-size limit so it can be iterated and dumped into Pinecone, etc\n// filename is the fullpath to the doc so we can compare by filename to find cached matches.\nasync function storeVectorResult(vectorData = [], filename = null) {\n  if (!filename) return;\n  console.log(\n    `Caching vectorized results of ${filename} to prevent duplicated embedding.`\n  );\n  if (!fs.existsSync(vectorCachePath)) fs.mkdirSync(vectorCachePath);\n\n  const digest = uuidv5(filename, uuidv5.URL);\n  const writeTo = path.resolve(vectorCachePath, `${digest}.json`);\n  fs.writeFileSync(writeTo, JSON.stringify(vectorData), \"utf8\");\n  return;\n}\n\n// Purges a file from the documents/ folder.\nasync function purgeSourceDocument(filename = null) {\n  if (!filename) return;\n  const filePath = path.resolve(documentsPath, normalizePath(filename));\n\n  if (\n    !fs.existsSync(filePath) ||\n    !isWithin(documentsPath, filePath) ||\n    !fs.lstatSync(filePath).isFile()\n  )\n    return;\n\n  console.log(`Purging source document of ${filename}.`);\n  fs.rmSync(filePath);\n  return;\n}\n\n// Purges a vector-cache file from the vector-cache/ folder.\nasync function purgeVectorCache(filename = null) {\n  if (!filename) return;\n  const digest = uuidv5(filename, uuidv5.URL);\n  const filePath = path.resolve(vectorCachePath, `${digest}.json`);\n\n  if (!fs.existsSync(filePath) || !fs.lstatSync(filePath).isFile()) return;\n  console.log(`Purging vector-cache of ${filename}.`);\n  fs.rmSync(filePath);\n  return;\n}\n\n// Search for a specific document by its unique name in the entire `documents`\n// folder via iteration of all folders and checking if the expected file exists.\nasync function findDocumentInDocuments(documentName = null) {\n  if (!documentName) return null;\n  for (const folder of fs.readdirSync(documentsPath)) {\n    const isFolder = fs\n      .lstatSync(path.join(documentsPath, folder))\n      .isDirectory();\n    if (!isFolder) continue;\n\n    const targetFilename = normalizePath(documentName);\n    const targetFileLocation = path.join(documentsPath, folder, targetFilename);\n\n    if (\n      !fs.existsSync(targetFileLocation) ||\n      !isWithin(documentsPath, targetFileLocation)\n    )\n      continue;\n\n    const fileData = fs.readFileSync(targetFileLocation, \"utf8\");\n    const cachefilename = `${folder}/${targetFilename}`;\n    const { pageContent: _pageContent, ...metadata } = JSON.parse(fileData);\n    return {\n      name: targetFilename,\n      type: \"file\",\n      ...metadata,\n      cached: await cachedVectorInformation(cachefilename, true),\n    };\n  }\n\n  return null;\n}\n\n/**\n * Checks if a given path is within another path.\n * @param {string} outer - The outer path (should be resolved).\n * @param {string} inner - The inner path (should be resolved).\n * @returns {boolean} - Returns true if the inner path is within the outer path, false otherwise.\n */\nfunction isWithin(outer, inner) {\n  if (outer === inner) return false;\n  const rel = path.relative(outer, inner);\n  return !rel.startsWith(\"../\") && rel !== \"..\";\n}\n\nfunction normalizePath(filepath = \"\") {\n  const result = path\n    .normalize(filepath.trim())\n    .replace(/^(\\.\\.(\\/|\\\\|$))+/, \"\")\n    .trim();\n  if ([\"..\", \".\", \"/\"].includes(result)) throw new Error(\"Invalid path.\");\n  return result;\n}\n\n// Check if the vector-cache folder is empty or not\n// useful for it the user is changing embedders as this will\n// break the previous cache.\nfunction hasVectorCachedFiles() {\n  try {\n    return (\n      fs.readdirSync(vectorCachePath)?.filter((name) => name.endsWith(\".json\"))\n        .length !== 0\n    );\n  } catch {}\n  return false;\n}\n\n/**\n * @param {string[]} filenames - array of filenames to check for pinned workspaces\n * @returns {Promise<Record<string, string[]>>} - a record of filenames and their corresponding workspaceIds\n */\nasync function getPinnedWorkspacesByDocument(filenames = []) {\n  return (\n    await Document.where(\n      {\n        docpath: {\n          in: Object.keys(filenames),\n        },\n        pinned: true,\n      },\n      null,\n      null,\n      null,\n      {\n        workspaceId: true,\n        docpath: true,\n      }\n    )\n  ).reduce((result, { workspaceId, docpath }) => {\n    const filename = filenames[docpath];\n    if (!result[filename]) result[filename] = [];\n    if (!result[filename].includes(workspaceId))\n      result[filename].push(workspaceId);\n    return result;\n  }, {});\n}\n\n/**\n * Get a record of filenames and their corresponding workspaceIds that have watched a document\n * that will be used to determine if a document should be displayed in the watched documents sidebar\n * @param {string[]} filenames - array of filenames to check for watched workspaces\n * @returns {Promise<Record<string, string[]>>} - a record of filenames and their corresponding workspaceIds\n */\nasync function getWatchedDocumentFilenames(filenames = []) {\n  return (\n    await Document.where(\n      {\n        docpath: { in: Object.keys(filenames) },\n        watched: true,\n      },\n      null,\n      null,\n      null,\n      { workspaceId: true, docpath: true }\n    )\n  ).reduce((result, { workspaceId, docpath }) => {\n    const filename = filenames[docpath];\n    result[filename] = workspaceId;\n    return result;\n  }, {});\n}\n\n/**\n * Purges the entire vector-cache folder and recreates it.\n * @returns {void}\n */\nfunction purgeEntireVectorCache() {\n  fs.rmSync(vectorCachePath, { recursive: true, force: true });\n  fs.mkdirSync(vectorCachePath);\n  return;\n}\n\n/**\n * File size threshold for files that are too large to be read into memory (MB)\n *\n * If the file is larger than this, we will stream it and parse it in chunks\n * This is to prevent us from using too much memory when parsing large files\n * or loading the files in the file picker.\n * @TODO - When lazy loading for folders is implemented, we should increase this threshold (512MB)\n * since it will always be faster to readSync than to stream the file and parse it in chunks.\n */\nconst FILE_READ_SIZE_THRESHOLD = 150 * (1024 * 1024);\n\n/**\n * Converts a file to picker data\n * @param {string} pathToFile - The path to the file to convert\n * @param {boolean} liveSyncAvailable - Whether live sync is available\n * @returns {Promise<{name: string, type: string, [string]: any, cached: boolean, canWatch: boolean}>} - The picker data\n */\nasync function fileToPickerData({\n  pathToFile,\n  liveSyncAvailable = false,\n  cachefilename = null,\n}) {\n  let metadata = {};\n  const filename = path.basename(pathToFile);\n  const fileStats = fs.statSync(pathToFile);\n  const cachedStatus = await cachedVectorInformation(cachefilename, true);\n\n  if (fileStats.size < FILE_READ_SIZE_THRESHOLD) {\n    const rawData = fs.readFileSync(pathToFile, \"utf8\");\n    try {\n      metadata = JSON.parse(rawData);\n      // Remove the pageContent field from the metadata - it is large and not needed for the picker\n      delete metadata.pageContent;\n    } catch (err) {\n      console.error(\"Error parsing file\", err);\n      return null;\n    }\n\n    return {\n      name: filename,\n      type: \"file\",\n      ...metadata,\n      cached: cachedStatus,\n      canWatch: liveSyncAvailable\n        ? DocumentSyncQueue.canWatch(metadata)\n        : false,\n      // pinnedWorkspaces: [], // This is the list of workspaceIds that have pinned this document\n      // watched: false, // boolean to indicate if this document is watched in ANY workspace\n    };\n  }\n\n  console.log(\n    `Stream-parsing ${path.basename(pathToFile)} because it exceeds the ${FILE_READ_SIZE_THRESHOLD} byte limit.`\n  );\n  const stream = fs.createReadStream(pathToFile, { encoding: \"utf8\" });\n  try {\n    let fileContent = \"\";\n    metadata = await new Promise((resolve, reject) => {\n      stream\n        .on(\"data\", (chunk) => {\n          fileContent += chunk;\n        })\n        .on(\"end\", () => {\n          metadata = JSON.parse(fileContent);\n          // Remove the pageContent field from the metadata - it is large and not needed for the picker\n          delete metadata.pageContent;\n          resolve(metadata);\n        })\n        .on(\"error\", (err) => {\n          console.error(\"Error parsing file\", err);\n          reject(null);\n        });\n    }).catch((err) => {\n      console.error(\"Error parsing file\", err);\n    });\n  } catch (err) {\n    console.error(\"Error parsing file\", err);\n    metadata = null;\n  } finally {\n    stream.destroy();\n  }\n\n  // If the metadata is empty or something went wrong, return null\n  if (!metadata || !Object.keys(metadata)?.length) {\n    console.log(`Stream-parsing failed for ${path.basename(pathToFile)}`);\n    return null;\n  }\n\n  return {\n    name: filename,\n    type: \"file\",\n    ...metadata,\n    cached: cachedStatus,\n    canWatch: liveSyncAvailable ? DocumentSyncQueue.canWatch(metadata) : false,\n  };\n}\n\nconst REQUIRED_FILE_OBJECT_FIELDS = [\n  \"name\",\n  \"type\",\n  \"url\",\n  \"title\",\n  \"docAuthor\",\n  \"description\",\n  \"docSource\",\n  \"chunkSource\",\n  \"published\",\n  \"wordCount\",\n  \"token_count_estimate\",\n];\n\n/**\n * Checks if a given metadata object has all the required fields\n * @param {{name: string, type: string, url: string, title: string, docAuthor: string, description: string, docSource: string, chunkSource: string, published: string, wordCount: number, token_count_estimate: number}} metadata - The metadata object to check (fileToPickerData)\n * @returns {boolean} - Returns true if the metadata object has all the required fields, false otherwise\n */\nfunction hasRequiredMetadata(metadata = {}) {\n  return REQUIRED_FILE_OBJECT_FIELDS.every((field) =>\n    metadata.hasOwnProperty(field)\n  );\n}\n\nmodule.exports = {\n  findDocumentInDocuments,\n  cachedVectorInformation,\n  viewLocalFiles,\n  purgeSourceDocument,\n  purgeVectorCache,\n  storeVectorResult,\n  fileData,\n  normalizePath,\n  isWithin,\n  documentsPath,\n  directUploadsPath,\n  hasVectorCachedFiles,\n  purgeEntireVectorCache,\n  getDocumentsByFolder,\n  hotdirPath,\n};\n"
  },
  {
    "path": "server/utils/files/logo.js",
    "content": "const path = require(\"path\");\nconst fs = require(\"fs\");\nconst { getType } = require(\"mime\");\nconst { v4 } = require(\"uuid\");\nconst { SystemSettings } = require(\"../../models/systemSettings\");\nconst { normalizePath, isWithin } = require(\".\");\nconst LOGO_FILENAME = \"anything-llm.png\";\nconst LOGO_FILENAME_DARK = \"anything-llm-dark.png\";\n\n/**\n * Checks if the filename is the default logo filename for dark or light mode.\n * @param {string} filename - The filename to check.\n * @returns {boolean} Whether the filename is the default logo filename.\n */\nfunction isDefaultFilename(filename) {\n  return [LOGO_FILENAME, LOGO_FILENAME_DARK].includes(filename);\n}\n\nfunction validFilename(newFilename = \"\") {\n  return !isDefaultFilename(newFilename);\n}\n\n/**\n * Shows the logo for the current theme. In dark mode, it shows the light logo\n * and vice versa.\n * @param {boolean} darkMode - Whether the logo should be for dark mode.\n * @returns {string} The filename of the logo.\n */\nfunction getDefaultFilename(darkMode = true) {\n  return darkMode ? LOGO_FILENAME : LOGO_FILENAME_DARK;\n}\n\nasync function determineLogoFilepath(defaultFilename = LOGO_FILENAME) {\n  const currentLogoFilename = await SystemSettings.currentLogoFilename();\n  const basePath = process.env.STORAGE_DIR\n    ? path.join(process.env.STORAGE_DIR, \"assets\")\n    : path.join(__dirname, \"../../storage/assets\");\n  const defaultFilepath = path.join(basePath, defaultFilename);\n\n  if (currentLogoFilename && validFilename(currentLogoFilename)) {\n    const customLogoPath = path.join(\n      basePath,\n      normalizePath(currentLogoFilename)\n    );\n    if (!isWithin(path.resolve(basePath), path.resolve(customLogoPath)))\n      return defaultFilepath;\n    return fs.existsSync(customLogoPath) ? customLogoPath : defaultFilepath;\n  }\n\n  return defaultFilepath;\n}\n\nfunction fetchLogo(logoPath) {\n  if (!fs.existsSync(logoPath)) {\n    return {\n      found: false,\n      buffer: null,\n      size: 0,\n      mime: \"none/none\",\n    };\n  }\n\n  const mime = getType(logoPath);\n  const buffer = fs.readFileSync(logoPath);\n  return {\n    found: true,\n    buffer,\n    size: buffer.length,\n    mime,\n  };\n}\n\nasync function renameLogoFile(originalFilename = null) {\n  const extname = path.extname(originalFilename) || \".png\";\n  const newFilename = `${v4()}${extname}`;\n  const assetsDirectory = process.env.STORAGE_DIR\n    ? path.join(process.env.STORAGE_DIR, \"assets\")\n    : path.join(__dirname, `../../storage/assets`);\n  const originalFilepath = path.join(\n    assetsDirectory,\n    normalizePath(originalFilename)\n  );\n  if (!isWithin(path.resolve(assetsDirectory), path.resolve(originalFilepath)))\n    throw new Error(\"Invalid file path.\");\n\n  // The output always uses a random filename.\n  const outputFilepath = process.env.STORAGE_DIR\n    ? path.join(process.env.STORAGE_DIR, \"assets\", normalizePath(newFilename))\n    : path.join(__dirname, `../../storage/assets`, normalizePath(newFilename));\n\n  fs.renameSync(originalFilepath, outputFilepath);\n  return newFilename;\n}\n\nasync function removeCustomLogo(logoFilename = LOGO_FILENAME) {\n  if (!logoFilename || !validFilename(logoFilename)) return false;\n  const assetsDirectory = process.env.STORAGE_DIR\n    ? path.join(process.env.STORAGE_DIR, \"assets\")\n    : path.join(__dirname, `../../storage/assets`);\n\n  const logoPath = path.join(assetsDirectory, normalizePath(logoFilename));\n  if (!isWithin(path.resolve(assetsDirectory), path.resolve(logoPath)))\n    throw new Error(\"Invalid file path.\");\n  if (fs.existsSync(logoPath)) fs.unlinkSync(logoPath);\n  return true;\n}\n\nmodule.exports = {\n  fetchLogo,\n  renameLogoFile,\n  removeCustomLogo,\n  validFilename,\n  getDefaultFilename,\n  determineLogoFilepath,\n  isDefaultFilename,\n  LOGO_FILENAME,\n};\n"
  },
  {
    "path": "server/utils/files/multer.js",
    "content": "const multer = require(\"multer\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst { v4 } = require(\"uuid\");\nconst { normalizePath } = require(\".\");\n\n/**\n * Handle File uploads for auto-uploading.\n * Mostly used for internal GUI/API uploads.\n */\nconst fileUploadStorage = multer.diskStorage({\n  destination: function (_, __, cb) {\n    const uploadOutput =\n      process.env.NODE_ENV === \"development\"\n        ? path.resolve(__dirname, `../../../collector/hotdir`)\n        : path.resolve(process.env.STORAGE_DIR, `../../collector/hotdir`);\n    cb(null, uploadOutput);\n  },\n  filename: function (_, file, cb) {\n    file.originalname = normalizePath(\n      Buffer.from(file.originalname, \"latin1\").toString(\"utf8\")\n    );\n    cb(null, file.originalname);\n  },\n});\n\n/**\n * Handle API file upload as documents - this does not manipulate the filename\n * at all for encoding/charset reasons.\n */\nconst fileAPIUploadStorage = multer.diskStorage({\n  destination: function (_, __, cb) {\n    const uploadOutput =\n      process.env.NODE_ENV === \"development\"\n        ? path.resolve(__dirname, `../../../collector/hotdir`)\n        : path.resolve(process.env.STORAGE_DIR, `../../collector/hotdir`);\n    cb(null, uploadOutput);\n  },\n  filename: function (_, file, cb) {\n    file.originalname = normalizePath(\n      Buffer.from(file.originalname, \"latin1\").toString(\"utf8\")\n    );\n    cb(null, file.originalname);\n  },\n});\n\n// Asset storage for logos\nconst assetUploadStorage = multer.diskStorage({\n  destination: function (_, __, cb) {\n    const uploadOutput =\n      process.env.NODE_ENV === \"development\"\n        ? path.resolve(__dirname, `../../storage/assets`)\n        : path.resolve(process.env.STORAGE_DIR, \"assets\");\n    fs.mkdirSync(uploadOutput, { recursive: true });\n    return cb(null, uploadOutput);\n  },\n  filename: function (_, file, cb) {\n    file.originalname = normalizePath(\n      Buffer.from(file.originalname, \"latin1\").toString(\"utf8\")\n    );\n    cb(null, file.originalname);\n  },\n});\n\n/**\n * Handle PFP file upload as logos\n */\nconst pfpUploadStorage = multer.diskStorage({\n  destination: function (_, __, cb) {\n    const uploadOutput =\n      process.env.NODE_ENV === \"development\"\n        ? path.resolve(__dirname, `../../storage/assets/pfp`)\n        : path.resolve(process.env.STORAGE_DIR, \"assets/pfp\");\n    fs.mkdirSync(uploadOutput, { recursive: true });\n    return cb(null, uploadOutput);\n  },\n  filename: function (req, file, cb) {\n    const randomFileName = `${v4()}${path.extname(\n      normalizePath(file.originalname)\n    )}`;\n    req.randomFileName = randomFileName;\n    cb(null, randomFileName);\n  },\n});\n\n/**\n * Handle Generic file upload as documents from the GUI\n * @param {Request} request\n * @param {Response} response\n * @param {NextFunction} next\n */\nfunction handleFileUpload(request, response, next) {\n  const upload = multer({ storage: fileUploadStorage }).single(\"file\");\n  upload(request, response, function (err) {\n    if (err) {\n      response\n        .status(500)\n        .json({\n          success: false,\n          error: `Invalid file upload. ${err.message}`,\n        })\n        .end();\n      return;\n    }\n    next();\n  });\n}\n\n/**\n * Handle API file upload as documents - this does not manipulate the filename\n * at all for encoding/charset reasons.\n * @param {Request} request\n * @param {Response} response\n * @param {NextFunction} next\n */\nfunction handleAPIFileUpload(request, response, next) {\n  const upload = multer({ storage: fileAPIUploadStorage }).single(\"file\");\n  upload(request, response, function (err) {\n    if (err) {\n      response\n        .status(500)\n        .json({\n          success: false,\n          error: `Invalid file upload. ${err.message}`,\n        })\n        .end();\n      return;\n    }\n    next();\n  });\n}\n\n/**\n * Handle logo asset uploads\n */\nfunction handleAssetUpload(request, response, next) {\n  const upload = multer({ storage: assetUploadStorage }).single(\"logo\");\n  upload(request, response, function (err) {\n    if (err) {\n      response\n        .status(500)\n        .json({\n          success: false,\n          error: `Invalid file upload. ${err.message}`,\n        })\n        .end();\n      return;\n    }\n    next();\n  });\n}\n\n/**\n * Handle PFP file upload as logos\n */\nfunction handlePfpUpload(request, response, next) {\n  const upload = multer({ storage: pfpUploadStorage }).single(\"file\");\n  upload(request, response, function (err) {\n    if (err) {\n      response\n        .status(500)\n        .json({\n          success: false,\n          error: `Invalid file upload. ${err.message}`,\n        })\n        .end();\n      return;\n    }\n    next();\n  });\n}\n\nmodule.exports = {\n  handleFileUpload,\n  handleAPIFileUpload,\n  handleAssetUpload,\n  handlePfpUpload,\n};\n"
  },
  {
    "path": "server/utils/files/pfp.js",
    "content": "const path = require(\"path\");\nconst fs = require(\"fs\");\nconst { getType } = require(\"mime\");\nconst { User } = require(\"../../models/user\");\nconst { normalizePath, isWithin } = require(\".\");\nconst { Workspace } = require(\"../../models/workspace\");\n\nfunction fetchPfp(pfpPath) {\n  if (!fs.existsSync(pfpPath)) {\n    return {\n      found: false,\n      buffer: null,\n      size: 0,\n      mime: \"none/none\",\n    };\n  }\n\n  const mime = getType(pfpPath);\n  const buffer = fs.readFileSync(pfpPath);\n  return {\n    found: true,\n    buffer,\n    size: buffer.length,\n    mime,\n  };\n}\n\nasync function determinePfpFilepath(id) {\n  const numberId = Number(id);\n  const user = await User.get({ id: numberId });\n  const pfpFilename = user?.pfpFilename || null;\n  if (!pfpFilename) return null;\n\n  const basePath = process.env.STORAGE_DIR\n    ? path.join(process.env.STORAGE_DIR, \"assets/pfp\")\n    : path.join(__dirname, \"../../storage/assets/pfp\");\n  const pfpFilepath = path.join(basePath, normalizePath(pfpFilename));\n\n  if (!isWithin(path.resolve(basePath), path.resolve(pfpFilepath))) return null;\n  if (!fs.existsSync(pfpFilepath)) return null;\n  return pfpFilepath;\n}\n\nasync function determineWorkspacePfpFilepath(slug) {\n  const workspace = await Workspace.get({ slug });\n  const pfpFilename = workspace?.pfpFilename || null;\n  if (!pfpFilename) return null;\n\n  const basePath = process.env.STORAGE_DIR\n    ? path.join(process.env.STORAGE_DIR, \"assets/pfp\")\n    : path.join(__dirname, \"../../storage/assets/pfp\");\n  const pfpFilepath = path.join(basePath, normalizePath(pfpFilename));\n\n  if (!isWithin(path.resolve(basePath), path.resolve(pfpFilepath))) return null;\n  if (!fs.existsSync(pfpFilepath)) return null;\n  return pfpFilepath;\n}\n\nmodule.exports = {\n  fetchPfp,\n  determinePfpFilepath,\n  determineWorkspacePfpFilepath,\n};\n"
  },
  {
    "path": "server/utils/files/purgeDocument.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst {\n  purgeVectorCache,\n  purgeSourceDocument,\n  normalizePath,\n  isWithin,\n  documentsPath,\n} = require(\".\");\nconst { Document } = require(\"../../models/documents\");\nconst { Workspace } = require(\"../../models/workspace\");\n\nasync function purgeDocument(filename = null) {\n  if (!filename || !normalizePath(filename)) return;\n\n  await purgeVectorCache(filename);\n  await purgeSourceDocument(filename);\n  const workspaces = await Workspace.where();\n  for (const workspace of workspaces) {\n    await Document.removeDocuments(workspace, [filename]);\n  }\n  return;\n}\n\n/**\n * Purge a folder and all its contents. This will also remove all vector-cache files and workspace document associations\n * for the documents within the folder.\n * @notice This function is not recursive. It only purges the contents of the specified folder.\n * @notice You cannot purge the `custom-documents` folder.\n * @param {string} folderName - The name/path of the folder to purge.\n * @returns {Promise<void>}\n */\nasync function purgeFolder(folderName = null) {\n  if (!folderName) return;\n  const subFolder = normalizePath(folderName);\n  const subFolderPath = path.resolve(documentsPath, subFolder);\n  const validRemovableSubFolders = fs\n    .readdirSync(documentsPath)\n    .map((folder) => {\n      // Filter out any results which are not folders or\n      // are the protected custom-documents folder.\n      if (folder === \"custom-documents\") return null;\n      const subfolderPath = path.resolve(documentsPath, folder);\n      if (!fs.lstatSync(subfolderPath).isDirectory()) return null;\n      return folder;\n    })\n    .filter((subFolder) => !!subFolder);\n\n  if (\n    !validRemovableSubFolders.includes(subFolder) ||\n    !fs.existsSync(subFolderPath) ||\n    !isWithin(documentsPath, subFolderPath)\n  )\n    return;\n\n  const filenames = fs\n    .readdirSync(subFolderPath)\n    .map((file) =>\n      path.join(subFolderPath, file).replace(documentsPath + \"/\", \"\")\n    );\n  const workspaces = await Workspace.where();\n\n  const purgePromises = [];\n  // Remove associated Vector-cache files\n  for (const filename of filenames) {\n    const rmVectorCache = () =>\n      new Promise((resolve) =>\n        purgeVectorCache(filename).then(() => resolve(true))\n      );\n    purgePromises.push(rmVectorCache);\n  }\n\n  // Remove workspace document associations\n  for (const workspace of workspaces) {\n    const rmWorkspaceDoc = () =>\n      new Promise((resolve) =>\n        Document.removeDocuments(workspace, filenames).then(() => resolve(true))\n      );\n    purgePromises.push(rmWorkspaceDoc);\n  }\n\n  await Promise.all(purgePromises.flat().map((f) => f()));\n  fs.rmSync(subFolderPath, { recursive: true }); // Delete target document-folder and source files.\n\n  return;\n}\n\nmodule.exports = {\n  purgeDocument,\n  purgeFolder,\n};\n"
  },
  {
    "path": "server/utils/helpers/admin/index.js",
    "content": "const { User } = require(\"../../../models/user\");\nconst { ROLES } = require(\"../../middleware/multiUserProtected\");\n\n// When a user is updating or creating a user in multi-user, we need to check if they\n// are allowed to do this and that the new or existing user will be at or below their permission level.\n// the user executing this function should be an admin or manager.\nfunction validRoleSelection(currentUser = {}, newUserParams = {}) {\n  if (!newUserParams.hasOwnProperty(\"role\"))\n    return { valid: true, error: null }; // not updating role, so skip.\n  if (currentUser.role === ROLES.admin) return { valid: true, error: null };\n  if (currentUser.role === ROLES.manager) {\n    const validRoles = [ROLES.manager, ROLES.default];\n    if (!validRoles.includes(newUserParams.role))\n      return { valid: false, error: \"Invalid role selection for user.\" };\n    return { valid: true, error: null };\n  }\n  return { valid: false, error: \"Invalid condition for caller.\" };\n}\n\n// Check to make sure with this update that includes a role change to an existing admin to a non-admin\n// that we still have at least one admin left or else they will lock themselves out.\nasync function canModifyAdmin(userToModify, updates) {\n  // if updates don't include role property\n  // or the user being modified isn't an admin currently\n  // or the updates role is equal to the users current role.\n  // skip validation.\n  if (!updates.hasOwnProperty(\"role\")) return { valid: true, error: null };\n  if (userToModify.role !== ROLES.admin) return { valid: true, error: null };\n  if (updates.role === userToModify.role) return { valid: true, error: null };\n\n  const adminCount = await User.count({ role: ROLES.admin });\n  if (adminCount - 1 <= 0)\n    return {\n      valid: false,\n      error: \"No system admins will remain if you do this. Update failed.\",\n    };\n  return { valid: true, error: null };\n}\n\nfunction validCanModify(currentUser, existingUser) {\n  if (currentUser.role === ROLES.admin) return { valid: true, error: null };\n  if (currentUser.role === ROLES.manager) {\n    const validRoles = [ROLES.manager, ROLES.default];\n    if (!validRoles.includes(existingUser.role))\n      return { valid: false, error: \"Cannot perform that action on user.\" };\n    return { valid: true, error: null };\n  }\n\n  return { valid: false, error: \"Invalid condition for caller.\" };\n}\n\nmodule.exports = {\n  validCanModify,\n  validRoleSelection,\n  canModifyAdmin,\n};\n"
  },
  {
    "path": "server/utils/helpers/camelcase.js",
    "content": "const UPPERCASE = /[\\p{Lu}]/u;\nconst LOWERCASE = /[\\p{Ll}]/u;\nconst LEADING_CAPITAL = /^[\\p{Lu}](?![\\p{Lu}])/gu;\nconst IDENTIFIER = /([\\p{Alpha}\\p{N}_]|$)/u;\nconst SEPARATORS = /[_.\\- ]+/;\n\nconst LEADING_SEPARATORS = new RegExp(\"^\" + SEPARATORS.source);\nconst SEPARATORS_AND_IDENTIFIER = new RegExp(\n  SEPARATORS.source + IDENTIFIER.source,\n  \"gu\"\n);\nconst NUMBERS_AND_IDENTIFIER = new RegExp(\"\\\\d+\" + IDENTIFIER.source, \"gu\");\n\nconst preserveCamelCase = (\n  string,\n  toLowerCase,\n  toUpperCase,\n  preserveConsecutiveUppercase\n) => {\n  let isLastCharLower = false;\n  let isLastCharUpper = false;\n  let isLastLastCharUpper = false;\n  let isLastLastCharPreserved = false;\n\n  for (let index = 0; index < string.length; index++) {\n    const character = string[index];\n    isLastLastCharPreserved = index > 2 ? string[index - 3] === \"-\" : true;\n\n    if (isLastCharLower && UPPERCASE.test(character)) {\n      string = string.slice(0, index) + \"-\" + string.slice(index);\n      isLastCharLower = false;\n      isLastLastCharUpper = isLastCharUpper;\n      isLastCharUpper = true;\n      index++;\n    } else if (\n      isLastCharUpper &&\n      isLastLastCharUpper &&\n      LOWERCASE.test(character) &&\n      (!isLastLastCharPreserved || preserveConsecutiveUppercase)\n    ) {\n      string = string.slice(0, index - 1) + \"-\" + string.slice(index - 1);\n      isLastLastCharUpper = isLastCharUpper;\n      isLastCharUpper = false;\n      isLastCharLower = true;\n    } else {\n      isLastCharLower =\n        toLowerCase(character) === character &&\n        toUpperCase(character) !== character;\n      isLastLastCharUpper = isLastCharUpper;\n      isLastCharUpper =\n        toUpperCase(character) === character &&\n        toLowerCase(character) !== character;\n    }\n  }\n\n  return string;\n};\n\nconst preserveConsecutiveUppercase = (input, toLowerCase) => {\n  LEADING_CAPITAL.lastIndex = 0;\n\n  return input.replace(LEADING_CAPITAL, (m1) => toLowerCase(m1));\n};\n\nconst postProcess = (input, toUpperCase) => {\n  SEPARATORS_AND_IDENTIFIER.lastIndex = 0;\n  NUMBERS_AND_IDENTIFIER.lastIndex = 0;\n\n  return input\n    .replace(SEPARATORS_AND_IDENTIFIER, (_, identifier) =>\n      toUpperCase(identifier)\n    )\n    .replace(NUMBERS_AND_IDENTIFIER, (m) => toUpperCase(m));\n};\n\nfunction camelCase(input, options) {\n  if (!(typeof input === \"string\" || Array.isArray(input))) {\n    throw new TypeError(\"Expected the input to be `string | string[]`\");\n  }\n\n  options = {\n    pascalCase: true,\n    preserveConsecutiveUppercase: false,\n    ...options,\n  };\n\n  if (Array.isArray(input)) {\n    input = input\n      .map((x) => x.trim())\n      .filter((x) => x.length)\n      .join(\"-\");\n  } else {\n    input = input.trim();\n  }\n\n  if (input.length === 0) {\n    return \"\";\n  }\n\n  const toLowerCase =\n    options.locale === false\n      ? (string) => string.toLowerCase()\n      : (string) => string.toLocaleLowerCase(options.locale);\n\n  const toUpperCase =\n    options.locale === false\n      ? (string) => string.toUpperCase()\n      : (string) => string.toLocaleUpperCase(options.locale);\n\n  if (input.length === 1) {\n    if (SEPARATORS.test(input)) {\n      return \"\";\n    }\n\n    return options.pascalCase ? toUpperCase(input) : toLowerCase(input);\n  }\n\n  const hasUpperCase = input !== toLowerCase(input);\n\n  if (hasUpperCase) {\n    input = preserveCamelCase(\n      input,\n      toLowerCase,\n      toUpperCase,\n      options.preserveConsecutiveUppercase\n    );\n  }\n\n  input = input.replace(LEADING_SEPARATORS, \"\");\n  input = options.preserveConsecutiveUppercase\n    ? preserveConsecutiveUppercase(input, toLowerCase)\n    : toLowerCase(input);\n\n  if (options.pascalCase) {\n    input = toUpperCase(input.charAt(0)) + input.slice(1);\n  }\n\n  return postProcess(input, toUpperCase);\n}\n\nmodule.exports = {\n  camelCase,\n};\n"
  },
  {
    "path": "server/utils/helpers/chat/LLMPerformanceMonitor.js",
    "content": "const { TokenManager } = require(\"../tiktoken\");\n\n/**\n * @typedef {import(\"openai/streaming\").Stream<import(\"openai\").OpenAI.ChatCompletionChunk>} OpenAICompatibleStream\n * @typedef {(reportedUsage: {[key: string]: number, completion_tokens?: number, prompt_tokens?: number}) => StreamMetrics} EndMeasurementFunction\n * @typedef {Array<{content: string}>} Messages\n */\n\n/**\n * @typedef {Object} StreamMetrics\n * @property {number} prompt_tokens - the number of tokens in the prompt\n * @property {number} completion_tokens - the number of tokens in the completion\n * @property {number} total_tokens - the total number of tokens\n * @property {number} outputTps - the tokens per second of the output\n * @property {number} duration - the duration of the stream\n */\n\n/**\n * @typedef {Object} MonitoredStream\n * @property {number} start - the start time of the stream\n * @property {number} duration - the duration of the stream\n * @property {StreamMetrics} metrics - the metrics of the stream\n * @property {EndMeasurementFunction} endMeasurement - the method to end the stream and calculate the metrics\n */\n\nclass LLMPerformanceMonitor {\n  static tokenManager = new TokenManager();\n  /**\n   * Counts the tokens in the messages.\n   * @param {Array<{content: string}>} messages - the messages sent to the LLM so we can calculate the prompt tokens since most providers do not return this on stream\n   * @returns {number}\n   */\n  static countTokens(messages = []) {\n    try {\n      return this.tokenManager.statsFrom(messages);\n    } catch {\n      return 0;\n    }\n  }\n  /**\n   * Wraps a function and logs the duration (in seconds) of the function call.\n   * If the output contains a `usage.duration` property, it will be used instead of the calculated duration.\n   * This allows providers to supply more accurate timing information.\n   * @param {Function} func\n   * @returns {Promise<{output: any, duration: number}>}\n   */\n  static measureAsyncFunction(func) {\n    return (async () => {\n      const start = Date.now();\n      const output = await func; // is a promise\n      const end = Date.now();\n      const duration = output?.usage?.duration ?? (end - start) / 1000;\n      return { output, duration };\n    })();\n  }\n\n  /**\n   * Wraps a completion stream and and attaches a start time and duration property to the stream.\n   * Also attaches an `endMeasurement` method to the stream that will calculate the duration of the stream and metrics.\n   * @param {Object} opts\n   * @param {Promise<OpenAICompatibleStream>} opts.func\n   * @param {Messages} [opts.messages=[]] - the messages sent to the LLM so we can calculate the prompt tokens since most providers do not return this on stream\n   * @param {boolean} [opts.runPromptTokenCalculation=true] - whether to run the prompt token calculation to estimate the `prompt_tokens` metric. This is useful for providers that do not return this on stream.\n   * @param {string} [opts.modelTag=\"\"] - the tag of the model that was used to generate the stream (eg: gpt-4o, claude-3-5-sonnet, qwen3/72b-instruct, etc.)\n   * @param {string} [opts.provider=\"\"] - the class name of the LLM that was used to generate the stream (eg: OpenAI, Anthropic, LMStudio, ApiPie, etc.)\n   * @returns {Promise<MonitoredStream>}\n   */\n  static async measureStream({\n    func,\n    messages = [],\n    runPromptTokenCalculation = true,\n    modelTag = \"\",\n    provider = \"\",\n  }) {\n    const stream = await func;\n    stream.start = Date.now();\n    stream.duration = 0;\n    stream.metrics = {\n      completion_tokens: 0,\n      prompt_tokens: runPromptTokenCalculation ? this.countTokens(messages) : 0,\n      total_tokens: 0,\n      outputTps: 0,\n      duration: 0,\n      ...(modelTag ? { model: modelTag } : {}),\n      ...(provider ? { provider: provider } : {}),\n    };\n\n    stream.endMeasurement = (reportedUsage = {}) => {\n      const end = Date.now();\n      const estimatedDuration = (end - stream.start) / 1000;\n\n      // Merge the reported usage with the existing metrics\n      // so the math in the metrics object is correct when calculating\n      stream.metrics = {\n        ...stream.metrics,\n        ...reportedUsage,\n        duration: reportedUsage?.duration ?? estimatedDuration,\n        timestamp: new Date(),\n      };\n\n      stream.metrics.total_tokens =\n        stream.metrics.prompt_tokens + (stream.metrics.completion_tokens || 0);\n      stream.metrics.outputTps =\n        stream.metrics.completion_tokens / stream.metrics.duration;\n      return stream.metrics;\n    };\n    return stream;\n  }\n}\n\nmodule.exports = {\n  LLMPerformanceMonitor,\n};\n"
  },
  {
    "path": "server/utils/helpers/chat/convertTo.js",
    "content": "// Helpers that convert workspace chats to some supported format\n// for external use by the user.\n\nconst { WorkspaceChats } = require(\"../../../models/workspaceChats\");\nconst { EmbedChats } = require(\"../../../models/embedChats\");\nconst { safeJsonParse } = require(\"../../http\");\nconst { SystemSettings } = require(\"../../../models/systemSettings\");\n\nasync function convertToCSV(preparedData) {\n  const headers = new Set([\"id\", \"workspace\", \"prompt\", \"response\", \"sent_at\"]);\n  preparedData.forEach((item) =>\n    Object.keys(item).forEach((key) => headers.add(key))\n  );\n\n  const rows = [Array.from(headers).join(\",\")];\n\n  for (const item of preparedData) {\n    const record = Array.from(headers)\n      .map((header) => {\n        const value = item[header] ?? \"\";\n        return escapeCsv(String(value));\n      })\n      .join(\",\");\n    rows.push(record);\n  }\n  return rows.join(\"\\n\");\n}\n\nasync function convertToJSON(preparedData) {\n  return JSON.stringify(preparedData, null, 4);\n}\n\n// ref: https://raw.githubusercontent.com/gururise/AlpacaDataCleaned/main/alpaca_data.json\nasync function convertToJSONAlpaca(preparedData) {\n  return JSON.stringify(preparedData, null, 4);\n}\n\n// You can validate JSONL outputs on https://jsonlines.org/validator/\nasync function convertToJSONL(workspaceChatsMap) {\n  return Object.values(workspaceChatsMap)\n    .map((workspaceChats) => JSON.stringify(workspaceChats))\n    .join(\"\\n\");\n}\n\nasync function prepareChatsForExport(format = \"jsonl\", chatType = \"workspace\") {\n  if (!exportMap.hasOwnProperty(format))\n    throw new Error(`Invalid export type: ${format}`);\n\n  let chats;\n  if (chatType === \"workspace\") {\n    chats = await WorkspaceChats.whereWithData({}, null, null, {\n      id: \"asc\",\n    });\n  } else if (chatType === \"embed\") {\n    chats = await EmbedChats.whereWithEmbedAndWorkspace(\n      {},\n      null,\n      {\n        id: \"asc\",\n      },\n      null\n    );\n  } else {\n    throw new Error(`Invalid chat type: ${chatType}`);\n  }\n\n  if (format === \"csv\" || format === \"json\") {\n    const preparedData = chats.map((chat) => {\n      const responseJson = safeJsonParse(chat.response, {});\n      const baseData = {\n        id: chat.id,\n        prompt: chat.prompt,\n        response: responseJson.text,\n        sent_at: chat.createdAt,\n        // Only add attachments to the json format since we cannot arrange attachments in csv format\n        ...(format === \"json\"\n          ? {\n              attachments:\n                responseJson.attachments?.length > 0\n                  ? responseJson.attachments.map((attachment) => ({\n                      type: \"image\",\n                      image: attachmentToDataUrl(attachment),\n                    }))\n                  : [],\n            }\n          : {}),\n      };\n\n      if (chatType === \"embed\") {\n        return {\n          ...baseData,\n          workspace: chat.embed_config\n            ? chat.embed_config.workspace.name\n            : \"unknown workspace\",\n        };\n      }\n\n      return {\n        ...baseData,\n        workspace: chat.workspace ? chat.workspace.name : \"unknown workspace\",\n        username: chat.user\n          ? chat.user.username\n          : chat.api_session_id !== null\n            ? \"API\"\n            : \"unknown user\",\n        rating:\n          chat.feedbackScore === null\n            ? \"--\"\n            : chat.feedbackScore\n              ? \"GOOD\"\n              : \"BAD\",\n      };\n    });\n\n    return preparedData;\n  }\n\n  // jsonAlpaca format does not support array outputs\n  if (format === \"jsonAlpaca\") {\n    const preparedData = chats.map((chat) => {\n      const responseJson = safeJsonParse(chat.response, {});\n      return {\n        instruction: buildSystemPrompt(\n          chat,\n          chat.workspace ? chat.workspace.openAiPrompt : null\n        ),\n        input: chat.prompt,\n        output: responseJson.text,\n      };\n    });\n\n    return preparedData;\n  }\n\n  // Export to JSONL format (recommended for fine-tuning)\n  const workspaceChatsMap = chats.reduce((acc, chat) => {\n    const { prompt, response, workspaceId } = chat;\n    const responseJson = safeJsonParse(response, { attachments: [] });\n    const attachments = responseJson.attachments;\n\n    if (!acc[workspaceId]) {\n      acc[workspaceId] = {\n        messages: [\n          {\n            role: \"system\",\n            content: [\n              {\n                type: \"text\",\n                text:\n                  chat.workspace?.openAiPrompt ??\n                  SystemSettings.saneDefaultSystemPrompt,\n              },\n            ],\n          },\n        ],\n      };\n    }\n\n    acc[workspaceId].messages.push(\n      {\n        role: \"user\",\n        content: [\n          {\n            type: \"text\",\n            text: prompt,\n          },\n          ...(attachments?.length > 0\n            ? attachments.map((attachment) => ({\n                type: \"image\",\n                image: attachmentToDataUrl(attachment),\n              }))\n            : []),\n        ],\n      },\n      {\n        role: \"assistant\",\n        content: [\n          {\n            type: \"text\",\n            text: responseJson.text,\n          },\n        ],\n      }\n    );\n\n    return acc;\n  }, {});\n\n  return workspaceChatsMap;\n}\n\nconst exportMap = {\n  json: {\n    contentType: \"application/json\",\n    func: convertToJSON,\n  },\n  csv: {\n    contentType: \"text/csv\",\n    func: convertToCSV,\n  },\n  jsonl: {\n    contentType: \"application/jsonl\",\n    func: convertToJSONL,\n  },\n  jsonAlpaca: {\n    contentType: \"application/json\",\n    func: convertToJSONAlpaca,\n  },\n};\n\nfunction escapeCsv(str) {\n  if (str === null || str === undefined) return '\"\"';\n  return `\"${str.replace(/\"/g, '\"\"').replace(/\\n/g, \" \")}\"`;\n}\n\nasync function exportChatsAsType(format = \"jsonl\", chatType = \"workspace\") {\n  const { contentType, func } = exportMap.hasOwnProperty(format)\n    ? exportMap[format]\n    : exportMap.jsonl;\n  const chats = await prepareChatsForExport(format, chatType);\n  return {\n    contentType,\n    data: await func(chats),\n  };\n}\n\nfunction buildSystemPrompt(chat, prompt = null) {\n  const sources = safeJsonParse(chat.response)?.sources || [];\n  const contextTexts = sources.map((source) => source.text);\n  const context =\n    sources.length > 0\n      ? \"\\nContext:\\n\" +\n        contextTexts\n          .map((text, i) => {\n            return `[CONTEXT ${i}]:\\n${text}\\n[END CONTEXT ${i}]\\n\\n`;\n          })\n          .join(\"\")\n      : \"\";\n  return `${prompt ?? SystemSettings.saneDefaultSystemPrompt}${context}`;\n}\n\n/**\n * Converts an attachment's content string to a proper data URL format if needed\n * @param {Object} attachment - The attachment object containing contentString and mime type\n * @returns {string} The properly formatted data URL\n */\nfunction attachmentToDataUrl(attachment) {\n  return attachment.contentString.startsWith(\"data:\")\n    ? attachment.contentString\n    : `data:${attachment.mime};base64,${attachment.contentString}`;\n}\n\nmodule.exports = {\n  prepareChatsForExport,\n  exportChatsAsType,\n};\n"
  },
  {
    "path": "server/utils/helpers/chat/index.js",
    "content": "const { sourceIdentifier } = require(\"../../chats\");\nconst { safeJsonParse } = require(\"../../http\");\nconst { TokenManager } = require(\"../tiktoken\");\nconst { convertToPromptHistory } = require(\"./responses\");\n\n/*\nWhat is the message Array compressor?\nTLDR: So anyway, i started blasting (your prompts & stuff)\n\nmessageArrayCompressor arose out of a need for users to be able to insert unlimited token prompts\nand also maintain coherent history, system instructions and context, if applicable.\n\nWe took an opinionated approach that after much back-testing we have found retained a highly coherent answer\nunder most user conditions that a user would take while using this specific system. While other systems may\nuse a more advanced model for compressing message history or simplify text through a recursive approach - our is much more simple.\n\nWe \"cannonball\" the input.\nCannonball (verb): To ensure a prompt fits through a model window we blast a hole in the center of any inputs blocking our path to doing so.\nThis starts by dissecting the input as tokens and delete from the middle-out bi-directionally until the prompt window is satisfied.\nYou may think: \"Doesn't this result in massive data loss?\" - yes & no.\nUnder the use cases we expect the tool to be used, which is mostly chatting with documents, we are able to use this approach with minimal blowback\non the quality of responses.\n\nWe accomplish this by taking a rate-limit approach that is proportional to the model capacity. Since we support more than openAI models, this needs to \nbe generic and reliance on a \"better summary\" model just is not a luxury we can afford. The added latency overhead during prompting is also unacceptable.\nIn general:\n  system: at best 15% of token capacity\n  history: at best 15% of token capacity\n  prompt: at best 70% of token capacity.\n\nwe handle overflows by taking an aggressive path for two main cases.\n\n1. Very large user prompt\n- Likely uninterested in context, history, or even system prompt. This is a \"standalone\" prompt that highjacks the whole thread.\n- We run this prompt on its own since a prompt that is over 70% of context window certainly is standalone.\n\n2. Context window is exceeded in regular use.\n- We do not touch prompt since it is very likely to be <70% of window.\n- We check system prompt is not outrageous - if it is we cannonball it and keep context if present.\n- We check a sliding window of history, only allowing up to 15% of the history to pass through if it fits, with a \npreference for recent history if we can cannonball to fit it, otherwise it is omitted.\n\nWe end up with a rather large prompt that fits through a given window with a lot of room for response in most use-cases.\nWe also take the approach that history is the least important and most flexible of the items in this array of responses.\n\nThere is a supplemental version of this function that also returns a formatted string for models like Claude-2\n*/\n\nasync function messageArrayCompressor(llm, messages = [], rawHistory = []) {\n  // assume the response will be at least 600 tokens. If the total prompt + reply is over we need to proactively\n  // run the compressor to ensure the prompt has enough space to reply.\n  // realistically - most users will not be impacted by this.\n  const tokenBuffer = 600;\n  const tokenManager = new TokenManager(llm.model);\n  // If no work needs to be done, just pass through.\n  if (tokenManager.statsFrom(messages) + tokenBuffer < llm.promptWindowLimit())\n    return messages;\n\n  const system = messages.shift();\n  const user = messages.pop();\n  const userPromptSize = tokenManager.countFromString(user.content);\n\n  // User prompt is the main focus here - we we prioritize it and allow\n  // it to highjack the entire conversation thread. We are going to\n  // cannonball the prompt through to ensure the reply has at least 20% of\n  // the token supply to reply with.\n  if (userPromptSize > llm.limits.user) {\n    return [\n      {\n        role: \"user\",\n        content: cannonball({\n          input: user.content,\n          targetTokenSize: llm.promptWindowLimit() * 0.8,\n          tiktokenInstance: tokenManager,\n        }),\n      },\n    ];\n  }\n\n  const compressedSystem = new Promise(async (resolve) => {\n    const count = tokenManager.countFromString(system.content);\n    if (count < llm.limits.system) {\n      resolve(system);\n      return;\n    }\n\n    // Split context from system prompt - cannonball since its over the window.\n    // We assume the context + user prompt is enough tokens to fit.\n    const [prompt, context = \"\"] = system.content.split(\"Context:\");\n    let compressedPrompt;\n    let compressedContext;\n\n    // If the user system prompt contribution's to the system prompt is more than\n    // 25% of the system limit, we will cannonball it - this favors the context\n    // over the instruction from the user.\n    if (tokenManager.countFromString(prompt) >= llm.limits.system * 0.25) {\n      compressedPrompt = cannonball({\n        input: prompt,\n        targetTokenSize: llm.limits.system * 0.25,\n        tiktokenInstance: tokenManager,\n      });\n    } else {\n      compressedPrompt = prompt;\n    }\n\n    if (tokenManager.countFromString(context) >= llm.limits.system * 0.75) {\n      compressedContext = cannonball({\n        input: context,\n        targetTokenSize: llm.limits.system * 0.75,\n        tiktokenInstance: tokenManager,\n      });\n    } else {\n      compressedContext = context;\n    }\n\n    system.content = `${compressedPrompt}${\n      compressedContext ? `\\nContext: ${compressedContext}` : \"\"\n    }`;\n    resolve(system);\n  });\n\n  // Prompt is allowed to take up to 70% of window - we know its under\n  // if we are here, so passthrough.\n  const compressedPrompt = new Promise(async (resolve) => resolve(user));\n\n  // We always aggressively compress history because it is the least\n  // important data to retain in full-fidelity.\n  const compressedHistory = new Promise((resolve) => {\n    const eligibleHistoryItems = [];\n    var historyTokenCount = 0;\n\n    for (const [i, history] of rawHistory.reverse().entries()) {\n      const [user, assistant] = convertToPromptHistory([history]);\n      const [userTokens, assistantTokens] = [\n        tokenManager.countFromString(user.content),\n        tokenManager.countFromString(assistant.content),\n      ];\n      const total = userTokens + assistantTokens;\n\n      // If during the loop the token cost of adding this history\n      // is small, we can add it to history and move onto next.\n      if (historyTokenCount + total < llm.limits.history) {\n        eligibleHistoryItems.unshift(user, assistant);\n        historyTokenCount += total;\n        continue;\n      }\n\n      // If we reach here the overhead of adding this history item will\n      // be too much of the limit. So now, we are prioritizing\n      // the most recent 3 message pairs - if we are already past those - exit loop and stop\n      // trying to make history work.\n      if (i > 2) break;\n\n      // We are over the limit and we are within the first 3 most recent chats.\n      // so now we cannonball them to make them fit into the window.\n      // max size = llm.limit.history; Each component of the message, can at most\n      // be 50% of the history. We cannonball whichever is the problem.\n      // The math isnt perfect for tokens, so we have to add a fudge factor for safety.\n      const maxTargetSize = Math.floor(llm.limits.history / 2.2);\n      if (userTokens > maxTargetSize) {\n        user.content = cannonball({\n          input: user.content,\n          targetTokenSize: maxTargetSize,\n          tiktokenInstance: tokenManager,\n        });\n      }\n\n      if (assistantTokens > maxTargetSize) {\n        assistant.content = cannonball({\n          input: assistant.content,\n          targetTokenSize: maxTargetSize,\n          tiktokenInstance: tokenManager,\n        });\n      }\n\n      const newTotal = tokenManager.statsFrom([user, assistant]);\n      if (historyTokenCount + newTotal > llm.limits.history) continue;\n      eligibleHistoryItems.unshift(user, assistant);\n      historyTokenCount += newTotal;\n    }\n    resolve(eligibleHistoryItems);\n  });\n\n  const [cSystem, cHistory, cPrompt] = await Promise.all([\n    compressedSystem,\n    compressedHistory,\n    compressedPrompt,\n  ]);\n  return [cSystem, ...cHistory, cPrompt];\n}\n\n// Implementation of messageArrayCompressor, but for string only completion models\nasync function messageStringCompressor(llm, promptArgs = {}, rawHistory = []) {\n  const tokenBuffer = 600;\n  const tokenManager = new TokenManager(llm.model);\n  const initialPrompt = llm.constructPrompt(promptArgs);\n  if (\n    tokenManager.statsFrom(initialPrompt) + tokenBuffer <\n    llm.promptWindowLimit()\n  )\n    return initialPrompt;\n\n  const system = promptArgs.systemPrompt;\n  const user = promptArgs.userPrompt;\n  const userPromptSize = tokenManager.countFromString(user);\n\n  // User prompt is the main focus here - we we prioritize it and allow\n  // it to highjack the entire conversation thread. We are going to\n  // cannonball the prompt through to ensure the reply has at least 20% of\n  // the token supply to reply with.\n  if (userPromptSize > llm.limits.user) {\n    return llm.constructPrompt({\n      userPrompt: cannonball({\n        input: user,\n        targetTokenSize: llm.promptWindowLimit() * 0.8,\n        tiktokenInstance: tokenManager,\n      }),\n    });\n  }\n\n  const compressedSystem = new Promise(async (resolve) => {\n    const count = tokenManager.countFromString(system);\n    if (count < llm.limits.system) {\n      resolve(system);\n      return;\n    }\n    resolve(\n      cannonball({\n        input: system,\n        targetTokenSize: llm.limits.system,\n        tiktokenInstance: tokenManager,\n      })\n    );\n  });\n\n  // Prompt is allowed to take up to 70% of window - we know its under\n  // if we are here, so passthrough.\n  const compressedPrompt = new Promise(async (resolve) => resolve(user));\n\n  // We always aggressively compress history because it is the least\n  // important data to retain in full-fidelity.\n  const compressedHistory = new Promise((resolve) => {\n    const eligibleHistoryItems = [];\n    var historyTokenCount = 0;\n\n    for (const [i, history] of rawHistory.reverse().entries()) {\n      const [user, assistant] = convertToPromptHistory([history]);\n      const [userTokens, assistantTokens] = [\n        tokenManager.countFromString(user.content),\n        tokenManager.countFromString(assistant.content),\n      ];\n      const total = userTokens + assistantTokens;\n\n      // If during the loop the token cost of adding this history\n      // is small, we can add it to history and move onto next.\n      if (historyTokenCount + total < llm.limits.history) {\n        eligibleHistoryItems.unshift(user, assistant);\n        historyTokenCount += total;\n        continue;\n      }\n\n      // If we reach here the overhead of adding this history item will\n      // be too much of the limit. So now, we are prioritizing\n      // the most recent 3 message pairs - if we are already past those - exit loop and stop\n      // trying to make history work.\n      if (i > 2) break;\n\n      // We are over the limit and we are within the first 3 most recent chats.\n      // so now we cannonball them to make them fit into the window.\n      // max size = llm.limit.history; Each component of the message, can at most\n      // be 50% of the history. We cannonball whichever is the problem.\n      // The math isnt perfect for tokens, so we have to add a fudge factor for safety.\n      const maxTargetSize = Math.floor(llm.limits.history / 2.2);\n      if (userTokens > maxTargetSize) {\n        user.content = cannonball({\n          input: user.content,\n          targetTokenSize: maxTargetSize,\n          tiktokenInstance: tokenManager,\n        });\n      }\n\n      if (assistantTokens > maxTargetSize) {\n        assistant.content = cannonball({\n          input: assistant.content,\n          targetTokenSize: maxTargetSize,\n          tiktokenInstance: tokenManager,\n        });\n      }\n\n      const newTotal = tokenManager.statsFrom([user, assistant]);\n      if (historyTokenCount + newTotal > llm.limits.history) continue;\n      eligibleHistoryItems.unshift(user, assistant);\n      historyTokenCount += newTotal;\n    }\n    resolve(eligibleHistoryItems);\n  });\n\n  const [cSystem, cHistory, cPrompt] = await Promise.all([\n    compressedSystem,\n    compressedHistory,\n    compressedPrompt,\n  ]);\n\n  return llm.constructPrompt({\n    systemPrompt: cSystem,\n    contextTexts: promptArgs?.contextTexts || [],\n    chatHistory: cHistory,\n    userPrompt: cPrompt,\n  });\n}\n\n// Cannonball prompting: aka where we shoot a proportionally big cannonball through a proportional large prompt\n// Nobody should be sending prompts this big, but there is no reason we shouldn't allow it if results are good even by doing it.\nfunction cannonball({\n  input = \"\",\n  targetTokenSize = 0,\n  tiktokenInstance = null,\n  ellipsesStr = null,\n}) {\n  if (!input || !targetTokenSize) return input;\n  const tokenManager = tiktokenInstance || new TokenManager();\n  const truncText = ellipsesStr || \"\\n\\n--prompt truncated for brevity--\\n\\n\";\n  const initialInputSize = tokenManager.countFromString(input);\n  if (initialInputSize < targetTokenSize) return input;\n\n  // if the delta is the token difference between where our prompt is in size\n  // and where we ideally need to land.\n  const delta = initialInputSize - targetTokenSize;\n  const tokenChunks = tokenManager.tokensFromString(input);\n  const middleIdx = Math.floor(tokenChunks.length / 2);\n\n  // middle truncate the text going left and right of midpoint\n  const leftChunks = tokenChunks.slice(0, middleIdx - Math.round(delta / 2));\n  const rightChunks = tokenChunks.slice(middleIdx + Math.round(delta / 2));\n  const truncatedText =\n    tokenManager.bytesFromTokens(leftChunks) +\n    truncText +\n    tokenManager.bytesFromTokens(rightChunks);\n\n  console.log(\n    `Cannonball results ${initialInputSize} -> ${tokenManager.countFromString(\n      truncatedText\n    )} tokens.`\n  );\n  return truncatedText;\n}\n\n/**\n * Fill the sources window with the priority of\n * 1. Pinned documents (handled prior to function)\n * 2. VectorSearch results\n * 3. prevSources in chat history - starting from most recent.\n *\n * Ensuring the window always has the desired amount of sources so that followup questions\n * in any chat mode have relevant sources, but not infinite sources. This function is used during chatting\n * and allows follow-up questions within a query chat that otherwise would have zero sources and would fail.\n * The added benefit is that during regular RAG chat, we have better coherence of citations that otherwise would\n * also yield no results with no need for a ReRanker to run and take much longer to return a response.\n *\n * The side effect of this is follow-up unrelated questions now have citations that would look totally irrelevant, however\n * we would rather optimize on the correctness of a response vs showing extraneous sources during a response. Given search\n * results always take a priority a good unrelated question that produces RAG results will still function as desired and due to previous\n * history backfill sources \"changing context\" mid-chat is handled appropriately.\n * example:\n * ---previous implementation---\n * prompt 1: \"What is anythingllm?\" -> possibly get 4 good sources\n * prompt 2: \"Tell me some features\" -> possible get 0 - 1 maybe relevant source + previous answer response -> bad response due to bad context mgmt\n * ---next implementation---\n * prompt 1: \"What is anythingllm?\" -> possibly get 4 good sources\n * prompt 2: \"Tell me some features\" -> possible get 0 - 1 maybe relevant source + previous answer response -> backfill with 3 good sources from previous -> much better response\n *\n * @param {Object} config - params to call\n * @param {object} config.nDocs = fill size of the window\n * @param {object} config.searchResults = vector `similarityResponse` results for .sources\n * @param {object[]} config.history - rawHistory of chat containing sources\n * @param {string[]} config.filterIdentifiers - Pinned document identifiers to prevent duplicate context\n * @returns {{\n *   contextTexts: string[],\n *   sources: object[],\n * }} - Array of sources that should be added to window\n */\nfunction fillSourceWindow({\n  nDocs = 4, // Number of documents\n  searchResults = [], // Sources from similarity search\n  history = [], // Raw history\n  filterIdentifiers = [], // pinned document sources\n} = {}) {\n  const sources = [...searchResults];\n\n  if (sources.length >= nDocs || history.length === 0) {\n    return {\n      sources,\n      contextTexts: sources.map((src) => src.text),\n    };\n  }\n\n  const log = (text, ...args) => {\n    console.log(`\\x1b[36m[fillSourceWindow]\\x1b[0m ${text}`, ...args);\n  };\n\n  log(\n    `Need to backfill ${nDocs - searchResults.length} chunks to fill in the source window for RAG!`\n  );\n  const seenChunks = new Set(searchResults.map((source) => source.id));\n\n  // We need to reverse again because we need to iterate from bottom of array (most recent chats)\n  // Looking at this function by itself you may think that this loop could be extreme for long history chats,\n  // but this was already handled where `history` we derived. This comes from `recentChatHistory` which\n  // includes a limit for history (default: 20). So this loop does not look as extreme as on first glance.\n  for (const chat of history.reverse()) {\n    if (sources.length >= nDocs) {\n      log(\n        `Citations backfilled to ${nDocs} references from ${searchResults.length} original citations.`\n      );\n      break;\n    }\n\n    const chatSources =\n      safeJsonParse(chat.response, { sources: [] })?.sources || [];\n    if (!chatSources?.length || !Array.isArray(chatSources)) continue;\n\n    const validSources = chatSources.filter((source) => {\n      return (\n        filterIdentifiers.includes(sourceIdentifier(source)) == false && // source cannot be in current pins\n        source.hasOwnProperty(\"score\") && // source cannot have come from a pinned document that was previously pinned\n        source.hasOwnProperty(\"text\") && // source has a valid text property we can use\n        seenChunks.has(source.id) == false // is unique\n      );\n    });\n\n    for (const validSource of validSources) {\n      if (sources.length >= nDocs) break;\n      sources.push(validSource);\n      seenChunks.add(validSource.id);\n    }\n  }\n\n  return {\n    sources,\n    contextTexts: sources.map((src) => src.text),\n  };\n}\n\nmodule.exports = {\n  messageArrayCompressor,\n  messageStringCompressor,\n  fillSourceWindow,\n};\n"
  },
  {
    "path": "server/utils/helpers/chat/responses.js",
    "content": "const { v4: uuidv4 } = require(\"uuid\");\nconst moment = require(\"moment\");\n\nfunction clientAbortedHandler(resolve, fullText) {\n  console.log(\n    \"\\x1b[43m\\x1b[34m[STREAM ABORTED]\\x1b[0m Client requested to abort stream. Exiting LLM stream handler early.\"\n  );\n  resolve(fullText);\n  return;\n}\n\n/**\n * Handles the default stream response for a chat.\n * @param {import(\"express\").Response} response\n * @param {import('./LLMPerformanceMonitor').MonitoredStream} stream\n * @param {Object} responseProps\n * @returns {Promise<string>}\n */\nfunction handleDefaultStreamResponseV2(response, stream, responseProps) {\n  const { uuid = uuidv4(), sources = [] } = responseProps;\n\n  // Why are we doing this?\n  // OpenAI do enable the usage metrics in the stream response but:\n  // 1. This parameter is not available in our current API version (TODO: update)\n  // 2. The usage metrics are not available in _every_ provider that uses this function\n  // 3. We need to track the usage metrics for every provider that uses this function - not just OpenAI\n  // Other keys are added by the LLMPerformanceMonitor.measureStream method\n  let hasUsageMetrics = false;\n  let usage = {\n    // prompt_tokens can be in this object if the provider supports it - otherwise we manually count it\n    // When the stream is created in the LLMProviders `streamGetChatCompletion` `LLMPerformanceMonitor.measureStream` call.\n    completion_tokens: 0,\n  };\n\n  return new Promise(async (resolve) => {\n    let fullText = \"\";\n\n    // Establish listener to early-abort a streaming response\n    // in case things go sideways or the user does not like the response.\n    // We preserve the generated text but continue as if chat was completed\n    // to preserve previously generated content.\n    const handleAbort = () => {\n      stream?.endMeasurement(usage);\n      clientAbortedHandler(resolve, fullText);\n    };\n    response.on(\"close\", handleAbort);\n\n    // Now handle the chunks from the streamed response and append to fullText.\n    try {\n      for await (const chunk of stream) {\n        const message = chunk?.choices?.[0];\n        const token = message?.delta?.content;\n\n        // If we see usage metrics in the chunk, we can use them directly\n        // instead of estimating them, but we only want to assign values if\n        // the response object is the exact same key:value pair we expect.\n        if (\n          chunk.hasOwnProperty(\"usage\") && // exists\n          !!chunk.usage && // is not null\n          Object.values(chunk.usage).length > 0 // has values\n        ) {\n          if (chunk.usage.hasOwnProperty(\"prompt_tokens\")) {\n            usage.prompt_tokens = Number(chunk.usage.prompt_tokens);\n          }\n\n          if (chunk.usage.hasOwnProperty(\"completion_tokens\")) {\n            hasUsageMetrics = true; // to stop estimating counter\n            usage.completion_tokens = Number(chunk.usage.completion_tokens);\n          }\n        }\n\n        if (token) {\n          fullText += token;\n          // If we never saw a usage metric, we can estimate them by number of completion chunks\n          if (!hasUsageMetrics) usage.completion_tokens++;\n          writeResponseChunk(response, {\n            uuid,\n            sources: [],\n            type: \"textResponseChunk\",\n            textResponse: token,\n            close: false,\n            error: false,\n          });\n        }\n\n        // LocalAi returns '' and others return null on chunks - the last chunk is not \"\" or null.\n        // Either way, the key `finish_reason` must be present to determine ending chunk.\n        if (\n          message?.hasOwnProperty(\"finish_reason\") && // Got valid message and it is an object with finish_reason\n          message.finish_reason !== \"\" &&\n          message.finish_reason !== null\n        ) {\n          writeResponseChunk(response, {\n            uuid,\n            sources,\n            type: \"textResponseChunk\",\n            textResponse: \"\",\n            close: true,\n            error: false,\n          });\n          response.removeListener(\"close\", handleAbort);\n          stream?.endMeasurement(usage);\n          resolve(fullText);\n          break; // Break streaming when a valid finish_reason is first encountered\n        }\n      }\n    } catch (e) {\n      console.log(`\\x1b[43m\\x1b[34m[STREAMING ERROR]\\x1b[0m ${e.message}`);\n      writeResponseChunk(response, {\n        uuid,\n        type: \"abort\",\n        textResponse: null,\n        sources: [],\n        close: true,\n        error: e.message,\n      });\n      stream?.endMeasurement(usage);\n      resolve(fullText); // Return what we currently have - if anything.\n    }\n  });\n}\n\nfunction convertToChatHistory(history = []) {\n  const formattedHistory = [];\n  for (const record of history) {\n    const { prompt, response, createdAt, feedbackScore = null, id } = record;\n    const data = JSON.parse(response);\n\n    // In the event that a bad response was stored - we should skip its entire record\n    // because it was likely an error and cannot be used in chats and will fail to render on UI.\n    if (typeof prompt !== \"string\") {\n      console.log(\n        `[convertToChatHistory] ChatHistory #${record.id} prompt property is not a string - skipping record.`\n      );\n      continue;\n    } else if (typeof data.text !== \"string\") {\n      console.log(\n        `[convertToChatHistory] ChatHistory #${record.id} response.text property is not a string - skipping record.`\n      );\n      continue;\n    }\n\n    formattedHistory.push([\n      {\n        role: \"user\",\n        content: prompt,\n        sentAt: moment(createdAt).unix(),\n        attachments: data?.attachments ?? [],\n        chatId: id,\n      },\n      {\n        type: data?.type || \"chart\",\n        role: \"assistant\",\n        content: data.text,\n        sources: data.sources || [],\n        chatId: id,\n        sentAt: moment(createdAt).unix(),\n        feedbackScore,\n        metrics: data?.metrics || {},\n      },\n    ]);\n  }\n\n  return formattedHistory.flat();\n}\n\n/**\n * Converts a chat history to a prompt history.\n * @param {Object[]} history - The chat history to convert\n * @returns {{role: string, content: string, attachments?: import(\"..\").Attachment}[]}\n */\nfunction convertToPromptHistory(history = []) {\n  const formattedHistory = [];\n  for (const record of history) {\n    const { prompt, response } = record;\n    const data = JSON.parse(response);\n\n    // In the event that a bad response was stored - we should skip its entire record\n    // because it was likely an error and cannot be used in chats and will fail to render on UI.\n    if (typeof prompt !== \"string\") {\n      console.log(\n        `[convertToPromptHistory] ChatHistory #${record.id} prompt property is not a string - skipping record.`\n      );\n      continue;\n    } else if (typeof data.text !== \"string\") {\n      console.log(\n        `[convertToPromptHistory] ChatHistory #${record.id} response.text property is not a string - skipping record.`\n      );\n      continue;\n    }\n\n    formattedHistory.push([\n      {\n        role: \"user\",\n        content: prompt,\n        // if there are attachments, add them as a property to the user message so we can reuse them in chat history later if supported by the llm.\n        ...(data?.attachments?.length > 0\n          ? { attachments: data?.attachments }\n          : {}),\n      },\n      {\n        role: \"assistant\",\n        content: data.text,\n      },\n    ]);\n  }\n  return formattedHistory.flat();\n}\n\n/**\n * Safely stringifies any object containing BigInt values\n * @param {*} obj - Anything to stringify that might contain BigInt values\n * @returns {string} JSON string with BigInt values converted to strings\n */\nfunction safeJSONStringify(obj) {\n  return JSON.stringify(obj, (_, value) => {\n    if (typeof value === \"bigint\") return value.toString();\n    return value;\n  });\n}\n\nfunction writeResponseChunk(response, data) {\n  response.write(`data: ${safeJSONStringify(data)}\\n\\n`);\n  return;\n}\n\n/**\n * Formats the chat history to re-use attachments in the chat history\n * that might have existed in the conversation earlier.\n * @param {{role:string, content:string, attachments?: Object[]}[]} chatHistory\n * @param {function} formatterFunction - The function to format the chat history from the llm provider\n * @param {('asProperty'|'spread')} mode - \"asProperty\" or \"spread\". Determines how the content is formatted in the message object.\n * @returns {object[]}\n */\nfunction formatChatHistory(\n  chatHistory = [],\n  formatterFunction,\n  mode = \"asProperty\"\n) {\n  return chatHistory.map((historicalMessage) => {\n    if (\n      historicalMessage?.role !== \"user\" || // Only user messages can have attachments\n      !historicalMessage?.attachments || // If there are no attachments, we can skip this\n      !historicalMessage.attachments.length // If there is an array but it is empty, we can skip this\n    )\n      return historicalMessage;\n\n    // Some providers, like Ollama, expect the content to be embedded in the message object.\n    if (mode === \"spread\") {\n      return {\n        role: historicalMessage.role,\n        ...formatterFunction({\n          userPrompt: historicalMessage.content,\n          attachments: historicalMessage.attachments,\n        }),\n      };\n    }\n\n    // Most providers expect the content to be a property of the message object formatted like OpenAI models.\n    return {\n      role: historicalMessage.role,\n      content: formatterFunction({\n        userPrompt: historicalMessage.content,\n        attachments: historicalMessage.attachments,\n      }),\n    };\n  });\n}\n\nmodule.exports = {\n  handleDefaultStreamResponseV2,\n  convertToChatHistory,\n  convertToPromptHistory,\n  writeResponseChunk,\n  clientAbortedHandler,\n  formatChatHistory,\n  safeJSONStringify,\n};\n"
  },
  {
    "path": "server/utils/helpers/customModels.js",
    "content": "const { fetchOpenRouterModels } = require(\"../AiProviders/openRouter\");\nconst {\n  fetchOpenRouterEmbeddingModels,\n} = require(\"../EmbeddingEngines/openRouter\");\nconst { fetchApiPieModels } = require(\"../AiProviders/apipie\");\nconst { perplexityModels } = require(\"../AiProviders/perplexity\");\nconst { fireworksAiModels } = require(\"../AiProviders/fireworksAi\");\nconst { ElevenLabsTTS } = require(\"../TextToSpeech/elevenLabs\");\nconst { fetchNovitaModels } = require(\"../AiProviders/novita\");\nconst { parseLMStudioBasePath } = require(\"../AiProviders/lmStudio\");\nconst { parseNvidiaNimBasePath } = require(\"../AiProviders/nvidiaNim\");\nconst { fetchPPIOModels } = require(\"../AiProviders/ppio\");\nconst { GeminiLLM } = require(\"../AiProviders/gemini\");\nconst { fetchCometApiModels } = require(\"../AiProviders/cometapi\");\nconst { parseFoundryBasePath } = require(\"../AiProviders/foundry\");\nconst { getDockerModels } = require(\"../AiProviders/dockerModelRunner\");\nconst { getAllLemonadeModels } = require(\"../AiProviders/lemonade\");\n\nconst SUPPORT_CUSTOM_MODELS = [\n  \"openai\",\n  \"anthropic\",\n  \"localai\",\n  \"ollama\",\n  \"togetherai\",\n  \"fireworksai\",\n  \"nvidia-nim\",\n  \"mistral\",\n  \"perplexity\",\n  \"openrouter\",\n  \"lmstudio\",\n  \"koboldcpp\",\n  \"litellm\",\n  \"elevenlabs-tts\",\n  \"groq\",\n  \"deepseek\",\n  \"apipie\",\n  \"novita\",\n  \"cometapi\",\n  \"xai\",\n  \"gemini\",\n  \"ppio\",\n  \"dpais\",\n  \"moonshotai\",\n  \"foundry\",\n  \"cohere\",\n  \"zai\",\n  \"giteeai\",\n  \"docker-model-runner\",\n  \"privatemode\",\n  \"sambanova\",\n  \"lemonade\",\n  // Embedding Engines\n  \"native-embedder\",\n  \"cohere-embedder\",\n  \"openrouter-embedder\",\n  \"lemonade-embedder\",\n];\n\nasync function getCustomModels(provider = \"\", apiKey = null, basePath = null) {\n  if (!SUPPORT_CUSTOM_MODELS.includes(provider))\n    return { models: [], error: \"Invalid provider for custom models\" };\n\n  switch (provider) {\n    case \"openai\":\n      return await openAiModels(apiKey);\n    case \"anthropic\":\n      return await anthropicModels(apiKey);\n    case \"localai\":\n      return await localAIModels(basePath, apiKey);\n    case \"ollama\":\n      return await ollamaAIModels(basePath, apiKey);\n    case \"togetherai\":\n      return await getTogetherAiModels(apiKey);\n    case \"fireworksai\":\n      return await getFireworksAiModels(apiKey);\n    case \"mistral\":\n      return await getMistralModels(apiKey);\n    case \"perplexity\":\n      return await getPerplexityModels();\n    case \"openrouter\":\n      return await getOpenRouterModels();\n    case \"lmstudio\":\n      return await getLMStudioModels(basePath, apiKey);\n    case \"koboldcpp\":\n      return await getKoboldCPPModels(basePath);\n    case \"litellm\":\n      return await liteLLMModels(basePath, apiKey);\n    case \"elevenlabs-tts\":\n      return await getElevenLabsModels(apiKey);\n    case \"groq\":\n      return await getGroqAiModels(apiKey);\n    case \"deepseek\":\n      return await getDeepSeekModels(apiKey);\n    case \"apipie\":\n      return await getAPIPieModels(apiKey);\n    case \"novita\":\n      return await getNovitaModels();\n    case \"cometapi\":\n      return await getCometApiModels();\n    case \"xai\":\n      return await getXAIModels(apiKey);\n    case \"nvidia-nim\":\n      return await getNvidiaNimModels(basePath);\n    case \"gemini\":\n      return await getGeminiModels(apiKey);\n    case \"ppio\":\n      return await getPPIOModels(apiKey);\n    case \"dpais\":\n      return await getDellProAiStudioModels(basePath);\n    case \"moonshotai\":\n      return await getMoonshotAiModels(apiKey);\n    case \"foundry\":\n      return await getFoundryModels(basePath);\n    case \"cohere\":\n      return await getCohereModels(apiKey, \"chat\");\n    case \"zai\":\n      return await getZAiModels(apiKey);\n    case \"native-embedder\":\n      return await getNativeEmbedderModels();\n    case \"cohere-embedder\":\n      return await getCohereModels(apiKey, \"embed\");\n    case \"openrouter-embedder\":\n      return await getOpenRouterEmbeddingModels();\n    case \"giteeai\":\n      return await getGiteeAIModels(apiKey);\n    case \"docker-model-runner\":\n      return await getDockerModelRunnerModels(basePath);\n    case \"privatemode\":\n      return await getPrivatemodeModels(basePath, \"generate\");\n    case \"sambanova\":\n      return await getSambaNovaModels(apiKey);\n    case \"lemonade\":\n      return await getLemonadeModels(basePath);\n    case \"lemonade-embedder\":\n      return await getLemonadeModels(basePath, \"embedding\");\n    default:\n      return { models: [], error: \"Invalid provider for custom models\" };\n  }\n}\n\nasync function openAiModels(apiKey = null) {\n  const { OpenAI: OpenAIApi } = require(\"openai\");\n  const openai = new OpenAIApi({\n    apiKey: apiKey || process.env.OPEN_AI_KEY,\n  });\n  const allModels = await openai.models\n    .list()\n    .then((results) => results.data)\n    .catch((e) => {\n      console.error(`OpenAI:listModels`, e.message);\n      return [\n        {\n          name: \"gpt-3.5-turbo\",\n          id: \"gpt-3.5-turbo\",\n          object: \"model\",\n          created: 1677610602,\n          owned_by: \"openai\",\n          organization: \"OpenAi\",\n        },\n        {\n          name: \"gpt-4o\",\n          id: \"gpt-4o\",\n          object: \"model\",\n          created: 1677610602,\n          owned_by: \"openai\",\n          organization: \"OpenAi\",\n        },\n        {\n          name: \"gpt-4\",\n          id: \"gpt-4\",\n          object: \"model\",\n          created: 1687882411,\n          owned_by: \"openai\",\n          organization: \"OpenAi\",\n        },\n        {\n          name: \"gpt-4-turbo\",\n          id: \"gpt-4-turbo\",\n          object: \"model\",\n          created: 1712361441,\n          owned_by: \"system\",\n          organization: \"OpenAi\",\n        },\n        {\n          name: \"gpt-4-32k\",\n          id: \"gpt-4-32k\",\n          object: \"model\",\n          created: 1687979321,\n          owned_by: \"openai\",\n          organization: \"OpenAi\",\n        },\n        {\n          name: \"gpt-3.5-turbo-16k\",\n          id: \"gpt-3.5-turbo-16k\",\n          object: \"model\",\n          created: 1683758102,\n          owned_by: \"openai-internal\",\n          organization: \"OpenAi\",\n        },\n      ];\n    });\n\n  const gpts = allModels\n    .filter(\n      (model) =>\n        (model.id.includes(\"gpt\") && !model.id.startsWith(\"ft:\")) ||\n        model.id.startsWith(\"o\") // o1, o1-mini, o3, etc\n    )\n    .filter(\n      (model) =>\n        !model.id.includes(\"vision\") &&\n        !model.id.includes(\"instruct\") &&\n        !model.id.includes(\"audio\") &&\n        !model.id.includes(\"realtime\") &&\n        !model.id.includes(\"image\") &&\n        !model.id.includes(\"moderation\") &&\n        !model.id.includes(\"transcribe\")\n    )\n    .map((model) => {\n      return {\n        ...model,\n        name: model.id,\n        organization: \"OpenAi\",\n      };\n    });\n\n  const customModels = allModels\n    .filter(\n      (model) =>\n        !model.owned_by.includes(\"openai\") && model.owned_by !== \"system\"\n    )\n    .map((model) => {\n      return {\n        ...model,\n        name: model.id,\n        organization: \"Your Fine-Tunes\",\n      };\n    });\n\n  // Api Key was successful so lets save it for future uses\n  if ((gpts.length > 0 || customModels.length > 0) && !!apiKey)\n    process.env.OPEN_AI_KEY = apiKey;\n  return { models: [...gpts, ...customModels], error: null };\n}\n\nasync function anthropicModels(_apiKey = null) {\n  const apiKey =\n    _apiKey === true\n      ? process.env.ANTHROPIC_API_KEY\n      : _apiKey || process.env.ANTHROPIC_API_KEY || null;\n  const AnthropicAI = require(\"@anthropic-ai/sdk\");\n  const anthropic = new AnthropicAI({ apiKey });\n  const models = await anthropic.models\n    .list()\n    .then((results) => results.data)\n    .then((models) => {\n      return models\n        .filter((model) => model.type === \"model\")\n        .map((model) => {\n          return {\n            id: model.id,\n            name: model.display_name,\n          };\n        });\n    })\n    .catch((e) => {\n      console.error(`Anthropic:listModels`, e.message);\n      return [];\n    });\n\n  // Api Key was successful so lets save it for future uses\n  if (models.length > 0 && !!apiKey) process.env.ANTHROPIC_API_KEY = apiKey;\n  return { models, error: null };\n}\n\nasync function localAIModels(basePath = null, apiKey = null) {\n  const { OpenAI: OpenAIApi } = require(\"openai\");\n  const openai = new OpenAIApi({\n    baseURL: basePath || process.env.LOCAL_AI_BASE_PATH,\n    apiKey: apiKey || process.env.LOCAL_AI_API_KEY || null,\n  });\n  const models = await openai.models\n    .list()\n    .then((results) => results.data)\n    .catch((e) => {\n      console.error(`LocalAI:listModels`, e.message);\n      return [];\n    });\n\n  // Api Key was successful so lets save it for future uses\n  if (models.length > 0 && !!apiKey) process.env.LOCAL_AI_API_KEY = apiKey;\n  return { models, error: null };\n}\n\nasync function getGroqAiModels(_apiKey = null) {\n  const { OpenAI: OpenAIApi } = require(\"openai\");\n  const apiKey =\n    _apiKey === true\n      ? process.env.GROQ_API_KEY\n      : _apiKey || process.env.GROQ_API_KEY || null;\n  const openai = new OpenAIApi({\n    baseURL: \"https://api.groq.com/openai/v1\",\n    apiKey,\n  });\n  const models = (\n    await openai.models\n      .list()\n      .then((results) => results.data)\n      .catch((e) => {\n        console.error(`GroqAi:listModels`, e.message);\n        return [];\n      })\n  ).filter(\n    (model) => !model.id.includes(\"whisper\") && !model.id.includes(\"tool-use\")\n  );\n\n  // Api Key was successful so lets save it for future uses\n  if (models.length > 0 && !!apiKey) process.env.GROQ_API_KEY = apiKey;\n  return { models, error: null };\n}\n\nasync function liteLLMModels(basePath = null, apiKey = null) {\n  const { OpenAI: OpenAIApi } = require(\"openai\");\n  const openai = new OpenAIApi({\n    baseURL: basePath || process.env.LITE_LLM_BASE_PATH,\n    apiKey: apiKey || process.env.LITE_LLM_API_KEY || null,\n  });\n  const models = await openai.models\n    .list()\n    .then((results) => results.data)\n    .catch((e) => {\n      console.error(`LiteLLM:listModels`, e.message);\n      return [];\n    });\n\n  // Api Key was successful so lets save it for future uses\n  if (models.length > 0 && !!apiKey) process.env.LITE_LLM_API_KEY = apiKey;\n  return { models, error: null };\n}\n\nasync function getLMStudioModels(basePath = null, _apiKey = null) {\n  try {\n    const apiKey =\n      _apiKey === true\n        ? process.env.LMSTUDIO_AUTH_TOKEN\n        : _apiKey || process.env.LMSTUDIO_AUTH_TOKEN || null;\n\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    const openai = new OpenAIApi({\n      baseURL: parseLMStudioBasePath(\n        basePath || process.env.LMSTUDIO_BASE_PATH\n      ),\n      apiKey: apiKey || null,\n    });\n    const models = await openai.models\n      .list()\n      .then((results) => results.data)\n      .catch((e) => {\n        console.error(`LMStudio:listModels`, e.message);\n        return [];\n      });\n\n    return { models, error: null };\n  } catch (e) {\n    console.error(`LMStudio:getLMStudioModels`, e.message);\n    return { models: [], error: \"Could not fetch LMStudio Models\" };\n  }\n}\n\nasync function getKoboldCPPModels(basePath = null) {\n  try {\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    const openai = new OpenAIApi({\n      baseURL: basePath || process.env.KOBOLD_CPP_BASE_PATH,\n      apiKey: null,\n    });\n    const models = await openai.models\n      .list()\n      .then((results) => results.data)\n      .catch((e) => {\n        console.error(`KoboldCPP:listModels`, e.message);\n        return [];\n      });\n\n    return { models, error: null };\n  } catch (e) {\n    console.error(`KoboldCPP:getKoboldCPPModels`, e.message);\n    return { models: [], error: \"Could not fetch KoboldCPP Models\" };\n  }\n}\n\nasync function ollamaAIModels(basePath = null, _authToken = null) {\n  let url;\n  try {\n    let urlPath = basePath ?? process.env.OLLAMA_BASE_PATH;\n    new URL(urlPath);\n    if (urlPath.split(\"\").slice(-1)?.[0] === \"/\")\n      throw new Error(\"BasePath Cannot end in /!\");\n    url = urlPath;\n  } catch {\n    return { models: [], error: \"Not a valid URL.\" };\n  }\n\n  const authToken = _authToken || process.env.OLLAMA_AUTH_TOKEN || null;\n  const headers = authToken ? { Authorization: `Bearer ${authToken}` } : {};\n  const models = await fetch(`${url}/api/tags`, { headers: headers })\n    .then((res) => {\n      if (!res.ok)\n        throw new Error(`Could not reach Ollama server! ${res.status}`);\n      return res.json();\n    })\n    .then((data) => data?.models || [])\n    .then((models) =>\n      models.map((model) => {\n        return { id: model.name };\n      })\n    )\n    .catch((e) => {\n      console.error(e);\n      return [];\n    });\n\n  // Api Key was successful so lets save it for future uses\n  if (models.length > 0 && !!authToken)\n    process.env.OLLAMA_AUTH_TOKEN = authToken;\n  return { models, error: null };\n}\n\nasync function getTogetherAiModels(apiKey = null) {\n  const _apiKey =\n    apiKey === true\n      ? process.env.TOGETHER_AI_API_KEY\n      : apiKey || process.env.TOGETHER_AI_API_KEY || null;\n  try {\n    const { togetherAiModels } = require(\"../AiProviders/togetherAi\");\n    const models = await togetherAiModels(_apiKey);\n    if (models.length > 0 && !!_apiKey)\n      process.env.TOGETHER_AI_API_KEY = _apiKey;\n    return { models, error: null };\n  } catch (error) {\n    console.error(\"Error in getTogetherAiModels:\", error);\n    return { models: [], error: \"Failed to fetch Together AI models\" };\n  }\n}\n\nasync function getFireworksAiModels(apiKey = null) {\n  const knownModels = await fireworksAiModels(apiKey);\n  if (!Object.keys(knownModels).length === 0)\n    return { models: [], error: null };\n\n  const models = Object.values(knownModels).map((model) => {\n    return {\n      id: model.id,\n      organization: model.organization,\n      name: model.name,\n    };\n  });\n  return { models, error: null };\n}\n\nasync function getPerplexityModels() {\n  const knownModels = perplexityModels();\n  if (!Object.keys(knownModels).length === 0)\n    return { models: [], error: null };\n\n  const models = Object.values(knownModels).map((model) => {\n    return {\n      id: model.id,\n      name: model.name,\n    };\n  });\n  return { models, error: null };\n}\n\nasync function getOpenRouterModels() {\n  const knownModels = await fetchOpenRouterModels();\n  if (!Object.keys(knownModels).length === 0)\n    return { models: [], error: null };\n\n  const models = Object.values(knownModels).map((model) => {\n    return {\n      id: model.id,\n      organization: model.organization,\n      name: model.name,\n    };\n  });\n  return { models, error: null };\n}\n\nasync function getNovitaModels() {\n  const knownModels = await fetchNovitaModels();\n  if (!Object.keys(knownModels).length === 0)\n    return { models: [], error: null };\n  const models = Object.values(knownModels).map((model) => {\n    return {\n      id: model.id,\n      organization: model.organization,\n      name: model.name,\n    };\n  });\n  return { models, error: null };\n}\n\nasync function getCometApiModels() {\n  const knownModels = await fetchCometApiModels();\n  if (!Object.keys(knownModels).length === 0)\n    return { models: [], error: null };\n  const models = Object.values(knownModels).map((model) => {\n    return {\n      id: model.id,\n      organization: model.organization,\n      name: model.name,\n    };\n  });\n  return { models, error: null };\n}\n\nasync function getAPIPieModels(apiKey = null) {\n  const knownModels = await fetchApiPieModels(apiKey);\n  if (!Object.keys(knownModels).length === 0)\n    return { models: [], error: null };\n\n  const models = Object.values(knownModels)\n    .filter((model) => {\n      // Filter for chat models\n      return (\n        model.subtype &&\n        (model.subtype.includes(\"chat\") || model.subtype.includes(\"chatx\"))\n      );\n    })\n    .map((model) => {\n      return {\n        id: model.id,\n        organization: model.organization,\n        name: model.name,\n      };\n    });\n  return { models, error: null };\n}\n\nasync function getMistralModels(apiKey = null) {\n  const { OpenAI: OpenAIApi } = require(\"openai\");\n  const openai = new OpenAIApi({\n    apiKey: apiKey || process.env.MISTRAL_API_KEY || null,\n    baseURL: \"https://api.mistral.ai/v1\",\n  });\n  const models = await openai.models\n    .list()\n    .then((results) =>\n      results.data.filter((model) => !model.id.includes(\"embed\"))\n    )\n    .catch((e) => {\n      console.error(`Mistral:listModels`, e.message);\n      return [];\n    });\n\n  // Api Key was successful so lets save it for future uses\n  if (models.length > 0 && !!apiKey) process.env.MISTRAL_API_KEY = apiKey;\n  return { models, error: null };\n}\n\nasync function getElevenLabsModels(apiKey = null) {\n  const models = (await ElevenLabsTTS.voices(apiKey)).map((model) => {\n    return {\n      id: model.voice_id,\n      organization: model.category,\n      name: model.name,\n    };\n  });\n\n  if (models.length === 0) {\n    return {\n      models: [\n        {\n          id: \"21m00Tcm4TlvDq8ikWAM\",\n          organization: \"premade\",\n          name: \"Rachel (default)\",\n        },\n      ],\n      error: null,\n    };\n  }\n\n  if (models.length > 0 && !!apiKey) process.env.TTS_ELEVEN_LABS_KEY = apiKey;\n  return { models, error: null };\n}\n\nasync function getDeepSeekModels(apiKey = null) {\n  const { OpenAI: OpenAIApi } = require(\"openai\");\n  const openai = new OpenAIApi({\n    apiKey: apiKey || process.env.DEEPSEEK_API_KEY,\n    baseURL: \"https://api.deepseek.com/v1\",\n  });\n  const models = await openai.models\n    .list()\n    .then((results) => results.data)\n    .then((models) =>\n      models.map((model) => ({\n        id: model.id,\n        name: model.id,\n        organization: model.owned_by,\n      }))\n    )\n    .catch((e) => {\n      console.error(`DeepSeek:listModels`, e.message);\n      return [\n        {\n          id: \"deepseek-chat\",\n          name: \"deepseek-chat\",\n          organization: \"deepseek\",\n        },\n        {\n          id: \"deepseek-reasoner\",\n          name: \"deepseek-reasoner\",\n          organization: \"deepseek\",\n        },\n      ];\n    });\n\n  if (models.length > 0 && !!apiKey) process.env.DEEPSEEK_API_KEY = apiKey;\n  return { models, error: null };\n}\n\nasync function getGiteeAIModels() {\n  const { giteeAiModels } = require(\"../AiProviders/giteeai\");\n  const modelMap = await giteeAiModels();\n  if (!Object.keys(modelMap).length === 0) return { models: [], error: null };\n  const models = Object.values(modelMap).map((model) => {\n    return {\n      id: model.id,\n      organization: model.organization ?? \"GiteeAI\",\n      name: model.id,\n    };\n  });\n  return { models, error: null };\n}\n\nasync function getXAIModels(_apiKey = null) {\n  const { OpenAI: OpenAIApi } = require(\"openai\");\n  const apiKey =\n    _apiKey === true\n      ? process.env.XAI_LLM_API_KEY\n      : _apiKey || process.env.XAI_LLM_API_KEY || null;\n  const openai = new OpenAIApi({\n    baseURL: \"https://api.x.ai/v1\",\n    apiKey,\n  });\n  const models = await openai.models\n    .list()\n    .then((results) => results.data)\n    .catch((e) => {\n      console.error(`XAI:listModels`, e.message);\n      return [\n        {\n          created: 1725148800,\n          id: \"grok-beta\",\n          object: \"model\",\n          owned_by: \"xai\",\n        },\n      ];\n    });\n\n  // Api Key was successful so lets save it for future uses\n  if (models.length > 0 && !!apiKey) process.env.XAI_LLM_API_KEY = apiKey;\n  return { models, error: null };\n}\n\nasync function getNvidiaNimModels(basePath = null) {\n  try {\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    const openai = new OpenAIApi({\n      baseURL: parseNvidiaNimBasePath(\n        basePath ?? process.env.NVIDIA_NIM_LLM_BASE_PATH\n      ),\n      apiKey: null,\n    });\n    const modelResponse = await openai.models\n      .list()\n      .then((results) => results.data)\n      .catch((e) => {\n        throw new Error(e.message);\n      });\n\n    const models = modelResponse.map((model) => {\n      return {\n        id: model.id,\n        name: model.id,\n        organization: model.owned_by,\n      };\n    });\n\n    return { models, error: null };\n  } catch (e) {\n    console.error(`NVIDIA NIM:getNvidiaNimModels`, e.message);\n    return { models: [], error: \"Could not fetch NVIDIA NIM Models\" };\n  }\n}\n\nasync function getGeminiModels(_apiKey = null) {\n  const apiKey =\n    _apiKey === true\n      ? process.env.GEMINI_API_KEY\n      : _apiKey || process.env.GEMINI_API_KEY || null;\n  const models = await GeminiLLM.fetchModels(apiKey);\n  // Api Key was successful so lets save it for future uses\n  if (models.length > 0 && !!apiKey) process.env.GEMINI_API_KEY = apiKey;\n  return { models, error: null };\n}\n\nasync function getPPIOModels() {\n  const ppioModels = await fetchPPIOModels();\n  if (!Object.keys(ppioModels).length === 0) return { models: [], error: null };\n  const models = Object.values(ppioModels).map((model) => {\n    return {\n      id: model.id,\n      organization: model.organization,\n      name: model.name,\n    };\n  });\n  return { models, error: null };\n}\n\nasync function getDellProAiStudioModels(basePath = null) {\n  const { OpenAI: OpenAIApi } = require(\"openai\");\n  try {\n    const { origin } = new URL(\n      basePath || process.env.DELL_PRO_AI_STUDIO_BASE_PATH\n    );\n    const openai = new OpenAIApi({\n      baseURL: `${origin}/v1/openai`,\n      apiKey: null,\n    });\n    const models = await openai.models\n      .list()\n      .then((results) => results.data)\n      .then((models) => {\n        return models\n          .filter(\n            (model) => model?.capability?.includes(\"TextToText\") // Only include text-to-text models for this handler\n          )\n          .map((model) => {\n            return {\n              id: model.id,\n              name: model.name,\n              organization: model.owned_by,\n            };\n          });\n      })\n      .catch((e) => {\n        throw new Error(e.message);\n      });\n    return { models, error: null };\n  } catch (e) {\n    console.error(`getDellProAiStudioModels`, e.message);\n    return {\n      models: [],\n      error: \"Could not reach Dell Pro Ai Studio from the provided base path\",\n    };\n  }\n}\n\nfunction getNativeEmbedderModels() {\n  const { NativeEmbedder } = require(\"../EmbeddingEngines/native\");\n  return { models: NativeEmbedder.availableModels(), error: null };\n}\n\nasync function getMoonshotAiModels(_apiKey = null) {\n  const apiKey =\n    _apiKey === true\n      ? process.env.MOONSHOT_AI_API_KEY\n      : _apiKey || process.env.MOONSHOT_AI_API_KEY || null;\n\n  const { OpenAI: OpenAIApi } = require(\"openai\");\n  const openai = new OpenAIApi({\n    baseURL: \"https://api.moonshot.ai/v1\",\n    apiKey,\n  });\n  const models = await openai.models\n    .list()\n    .then((results) => results.data)\n    .catch((e) => {\n      console.error(`MoonshotAi:listModels`, e.message);\n      return [];\n    });\n\n  // Api Key was successful so lets save it for future uses\n  if (models.length > 0) process.env.MOONSHOT_AI_API_KEY = apiKey;\n  return { models, error: null };\n}\n\nasync function getFoundryModels(basePath = null) {\n  try {\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    const openai = new OpenAIApi({\n      baseURL: parseFoundryBasePath(basePath || process.env.FOUNDRY_BASE_PATH),\n      apiKey: null,\n    });\n    const models = await openai.models\n      .list()\n      .then((results) =>\n        results.data.map((model) => ({\n          ...model,\n          name: model.id,\n        }))\n      )\n      .catch((e) => {\n        console.error(`Foundry:listModels`, e.message);\n        return [];\n      });\n\n    return { models, error: null };\n  } catch (e) {\n    console.error(`Foundry:getFoundryModels`, e.message);\n    return { models: [], error: \"Could not fetch Foundry Models\" };\n  }\n}\n\n/**\n * Get Cohere models\n * @param {string} _apiKey - The API key to use\n * @param {'chat' | 'embed'} type - The type of model to get\n * @returns {Promise<{models: Array<{id: string, organization: string, name: string}>, error: string | null}>}\n */\nasync function getCohereModels(_apiKey = null, type = \"chat\") {\n  const apiKey =\n    _apiKey === true\n      ? process.env.COHERE_API_KEY\n      : _apiKey || process.env.COHERE_API_KEY || null;\n\n  const { CohereClient } = require(\"cohere-ai\");\n  const cohere = new CohereClient({\n    token: apiKey,\n  });\n  const models = await cohere.models\n    .list({ pageSize: 1000, endpoint: type })\n    .then((results) => results.models)\n    .then((models) =>\n      models.map((model) => ({\n        id: model.name,\n        name: model.name,\n      }))\n    )\n    .catch((e) => {\n      console.error(`Cohere:listModels`, e.message);\n      return [];\n    });\n\n  return { models, error: null };\n}\n\nasync function getZAiModels(_apiKey = null) {\n  const { OpenAI: OpenAIApi } = require(\"openai\");\n  const apiKey =\n    _apiKey === true\n      ? process.env.ZAI_API_KEY\n      : _apiKey || process.env.ZAI_API_KEY || null;\n  const openai = new OpenAIApi({\n    baseURL: \"https://api.z.ai/api/paas/v4\",\n    apiKey,\n  });\n  const models = await openai.models\n    .list()\n    .then((results) => results.data)\n    .catch((e) => {\n      console.error(`Z.AI:listModels`, e.message);\n      return [];\n    });\n\n  // Api Key was successful so lets save it for future uses\n  if (models.length > 0 && !!apiKey) process.env.ZAI_API_KEY = apiKey;\n  return { models, error: null };\n}\n\nasync function getOpenRouterEmbeddingModels() {\n  const knownModels = await fetchOpenRouterEmbeddingModels();\n  if (!Object.keys(knownModels).length === 0)\n    return { models: [], error: null };\n\n  const models = Object.values(knownModels).map((model) => {\n    return {\n      id: model.id,\n      organization: model.organization,\n      name: model.name,\n    };\n  });\n  return { models, error: null };\n}\n\nasync function getDockerModelRunnerModels(basePath = null) {\n  try {\n    const models = await getDockerModels(basePath);\n    return { models, error: null };\n  } catch (e) {\n    console.error(`DockerModelRunner:getDockerModelRunnerModels`, e.message);\n    return {\n      models: [],\n      error: \"Could not fetch Docker Model Runner Models\",\n    };\n  }\n}\n\nasync function getLemonadeModels(basePath = null, task = \"chat\") {\n  try {\n    const models = await getAllLemonadeModels(basePath, task);\n    return { models, error: null };\n  } catch (e) {\n    console.error(`Lemonade:getLemonadeModels`, e.message);\n    return { models: [], error: \"Could not fetch Lemonade Models\" };\n  }\n}\n\n/**\n * Get Privatemode models\n * @param {string} basePath - The base path of the Privatemode endpoint.\n * @param {'any' | 'generate' | 'embed' | 'transcribe'} task - The task to fetch the models for.\n * @returns {Promise<{models: Array<{id: string, organization: string, name: string}>, error: string | null}>}\n */\nasync function getPrivatemodeModels(basePath = null, task = \"any\") {\n  try {\n    const { PrivatemodeLLM } = require(\"../AiProviders/privatemode\");\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    const openai = new OpenAIApi({\n      baseURL: PrivatemodeLLM.parseBasePath(\n        basePath || process.env.PRIVATEMODE_LLM_BASE_PATH\n      ),\n      apiKey: null,\n    });\n    const models = await openai.models\n      .list()\n      .then((results) => results.data)\n      .then(\n        (models) =>\n          models\n            .filter((model) => !model.id.includes(\"/\")) // remove legacy prefixed models\n            .filter((model) =>\n              task === \"any\" ? true : model.tasks.includes(task)\n            ) // filter by task or show all if task is any\n      )\n      .then((models) =>\n        models.map((model) => ({\n          id: model.id,\n          organization: \"Privatemode\",\n          name: model.id\n            .split(\"-\")\n            .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n            .join(\" \"),\n        }))\n      )\n      .catch((e) => {\n        console.error(`Privatemode:listModels`, e.message);\n        return [];\n      });\n    return { models, error: null };\n  } catch (e) {\n    console.error(`Privatemode:getPrivatemodeModels`, e.message);\n    return { models: [], error: \"Could not fetch Privatemode Models\" };\n  }\n}\n\n/**\n * Get SambaNova models\n * @param {string} _apiKey - The API key to use\n * @returns {Promise<{models: Array<{id: string, organization: string, name: string}>, error: string | null}>}\n */\nasync function getSambaNovaModels(_apiKey = null) {\n  try {\n    const apiKey =\n      _apiKey === true\n        ? process.env.SAMBANOVA_LLM_API_KEY\n        : _apiKey || process.env.SAMBANOVA_LLM_API_KEY || null;\n    const { OpenAI: OpenAIApi } = require(\"openai\");\n    const openai = new OpenAIApi({\n      baseURL: \"https://api.sambanova.ai/v1\",\n      apiKey,\n    });\n    const models = await openai.models\n      .list()\n      .then((results) => results.data)\n      .then((models) =>\n        models.filter((model) => !model.id.toLowerCase().startsWith(\"whisper\"))\n      )\n      .then((models) =>\n        models.map((model) => {\n          const organization =\n            model.hasOwnProperty(\"owned_by\") &&\n            model.owned_by !== \"no-reply@sambanova.ai\"\n              ? model.owned_by\n              : \"SambaNova\";\n          return {\n            id: model.id,\n            organization,\n            name: model.id,\n          };\n        })\n      )\n      .catch((e) => {\n        console.error(`SambaNova:listModels`, e.message);\n        return [];\n      });\n    return { models, error: null };\n  } catch (e) {\n    console.error(`SambaNova:getSambaNovaModels`, e.message);\n    return { models: [], error: \"Could not fetch SambaNova Models\" };\n  }\n}\n\nmodule.exports = {\n  getCustomModels,\n  SUPPORT_CUSTOM_MODELS,\n};\n"
  },
  {
    "path": "server/utils/helpers/index.js",
    "content": "/**\n * File Attachment for automatic upload on the chat container page.\n * @typedef Attachment\n * @property {string} name - the given file name\n * @property {string} mime - the given file mime\n * @property {string} contentString - full base64 encoded string of file\n */\n\n/**\n * @typedef {Object} ResponseMetrics\n * @property {number} prompt_tokens - The number of prompt tokens used\n * @property {number} completion_tokens - The number of completion tokens used\n * @property {number} total_tokens - The total number of tokens used\n * @property {number} outputTps - The output tokens per second\n * @property {number} duration - The duration of the request in seconds\n *\n * @typedef {Object} ChatMessage\n * @property {string} role - The role of the message sender (e.g. 'user', 'assistant', 'system')\n * @property {string} content - The content of the message\n *\n * @typedef {Object} ChatCompletionResponse\n * @property {string} textResponse - The text response from the LLM\n * @property {ResponseMetrics} metrics - The response metrics\n *\n * @typedef {Object} ChatCompletionOptions\n * @property {number} temperature - The sampling temperature for the LLM response\n * @property {import(\"@prisma/client\").users} user - The user object for the chat completion to send to the LLM provider for user tracking (optional)\n *\n * @typedef {function(Array<ChatMessage>, ChatCompletionOptions): Promise<ChatCompletionResponse>} getChatCompletionFunction\n *\n * @typedef {function(Array<ChatMessage>, ChatCompletionOptions): Promise<import(\"./chat/LLMPerformanceMonitor\").MonitoredStream>} streamGetChatCompletionFunction\n */\n\n/**\n * @typedef {Object} BaseLLMProvider - A basic llm provider object\n * @property {Function} streamingEnabled - Checks if streaming is enabled for chat completions.\n * @property {Function} promptWindowLimit - Returns the token limit for the current model.\n * @property {Function} isValidChatCompletionModel - Validates if the provided model is suitable for chat completion.\n * @property {Function} constructPrompt - Constructs a formatted prompt for the chat completion request.\n * @property {getChatCompletionFunction} getChatCompletion - Gets a chat completion response from OpenAI.\n * @property {streamGetChatCompletionFunction} streamGetChatCompletion - Streams a chat completion response from OpenAI.\n * @property {Function} handleStream - Handles the streaming response.\n * @property {Function} embedTextInput - Embeds the provided text input using the specified embedder.\n * @property {Function} embedChunks - Embeds multiple chunks of text using the specified embedder.\n * @property {Function} compressMessages - Compresses chat messages to fit within the token limit.\n */\n\n/**\n * @typedef {Object} BaseLLMProviderClass - Class method of provider - not instantiated\n * @property {function(string): number} promptWindowLimit - Returns the token limit for the provided model.\n */\n\n/**\n * @typedef {Object} BaseVectorDatabaseProvider\n * @property {string} name - The name of the Vector Database instance.\n * @property {Function} connect - Connects to the Vector Database client.\n * @property {Function} totalVectors - Returns the total number of vectors in the database.\n * @property {Function} namespaceCount - Returns the count of vectors in a given namespace.\n * @property {Function} similarityResponse - Performs a similarity search on a given namespace.\n * @property {Function} rerankedSimilarityResponse - Performs a similarity search on a given namespace with reranking (if supported by provider).\n * @property {Function} namespace - Retrieves the specified namespace collection.\n * @property {Function} hasNamespace - Checks if a namespace exists.\n * @property {Function} namespaceExists - Verifies if a namespace exists in the client.\n * @property {Function} deleteVectorsInNamespace - Deletes all vectors in a specified namespace.\n * @property {Function} deleteDocumentFromNamespace - Deletes a document from a specified namespace.\n * @property {Function} addDocumentToNamespace - Adds a document to a specified namespace.\n * @property {Function} performSimilaritySearch - Performs a similarity search in the namespace.\n */\n\n/**\n * @typedef {Object} BaseEmbedderProvider\n * @property {string} model - The model used for embedding.\n * @property {number} maxConcurrentChunks - The maximum number of chunks processed concurrently.\n * @property {number} embeddingMaxChunkLength - The maximum length of each chunk for embedding.\n * @property {Function} embedTextInput - Embeds a single text input.\n * @property {Function} embedChunks - Embeds multiple chunks of text.\n */\n\n/**\n * Gets the systems current vector database provider.\n * @param {('pinecone' | 'chroma' | 'chromacloud' | 'lancedb' | 'weaviate' | 'qdrant' | 'milvus' | 'zilliz' | 'astra') | null} getExactly - If provided, this will return an explit provider.\n * @returns { BaseVectorDatabaseProvider}\n */\nfunction getVectorDbClass(getExactly = null) {\n  const vectorSelection = getExactly ?? process.env.VECTOR_DB ?? \"lancedb\";\n  switch (vectorSelection) {\n    case \"pinecone\":\n      const { Pinecone } = require(\"../vectorDbProviders/pinecone\");\n      return new Pinecone();\n    case \"chroma\":\n      const { Chroma } = require(\"../vectorDbProviders/chroma\");\n      return new Chroma();\n    case \"chromacloud\":\n      const { ChromaCloud } = require(\"../vectorDbProviders/chromacloud\");\n      return new ChromaCloud();\n    case \"lancedb\":\n      const { LanceDb } = require(\"../vectorDbProviders/lance\");\n      return new LanceDb();\n    case \"weaviate\":\n      const { Weaviate } = require(\"../vectorDbProviders/weaviate\");\n      return new Weaviate();\n    case \"qdrant\":\n      const { QDrant } = require(\"../vectorDbProviders/qdrant\");\n      return new QDrant();\n    case \"milvus\":\n      const { Milvus } = require(\"../vectorDbProviders/milvus\");\n      return new Milvus();\n    case \"zilliz\":\n      const { Zilliz } = require(\"../vectorDbProviders/zilliz\");\n      return new Zilliz();\n    case \"astra\":\n      const { AstraDB } = require(\"../vectorDbProviders/astra\");\n      return new AstraDB();\n    case \"pgvector\":\n      const { PGVector } = require(\"../vectorDbProviders/pgvector\");\n      return new PGVector();\n    default:\n      console.error(\n        `\\x1b[31m[ENV ERROR]\\x1b[0m No VECTOR_DB value found in environment! Falling back to LanceDB`\n      );\n      const { LanceDb: DefaultLanceDb } = require(\"../vectorDbProviders/lance\");\n      return new DefaultLanceDb();\n  }\n}\n\n/**\n * Returns the LLMProvider with its embedder attached via system or via defined provider.\n * @param {{provider: string | null, model: string | null} | null} params - Initialize params for LLMs provider\n * @returns {BaseLLMProvider}\n */\nfunction getLLMProvider({ provider = null, model = null } = {}) {\n  const LLMSelection = provider ?? process.env.LLM_PROVIDER ?? \"openai\";\n  const embedder = getEmbeddingEngineSelection();\n\n  switch (LLMSelection) {\n    case \"openai\":\n      const { OpenAiLLM } = require(\"../AiProviders/openAi\");\n      return new OpenAiLLM(embedder, model);\n    case \"azure\":\n      const { AzureOpenAiLLM } = require(\"../AiProviders/azureOpenAi\");\n      return new AzureOpenAiLLM(embedder, model);\n    case \"anthropic\":\n      const { AnthropicLLM } = require(\"../AiProviders/anthropic\");\n      return new AnthropicLLM(embedder, model);\n    case \"gemini\":\n      const { GeminiLLM } = require(\"../AiProviders/gemini\");\n      return new GeminiLLM(embedder, model);\n    case \"lmstudio\":\n      const { LMStudioLLM } = require(\"../AiProviders/lmStudio\");\n      return new LMStudioLLM(embedder, model);\n    case \"localai\":\n      const { LocalAiLLM } = require(\"../AiProviders/localAi\");\n      return new LocalAiLLM(embedder, model);\n    case \"ollama\":\n      const { OllamaAILLM } = require(\"../AiProviders/ollama\");\n      return new OllamaAILLM(embedder, model);\n    case \"togetherai\":\n      const { TogetherAiLLM } = require(\"../AiProviders/togetherAi\");\n      return new TogetherAiLLM(embedder, model);\n    case \"fireworksai\":\n      const { FireworksAiLLM } = require(\"../AiProviders/fireworksAi\");\n      return new FireworksAiLLM(embedder, model);\n    case \"perplexity\":\n      const { PerplexityLLM } = require(\"../AiProviders/perplexity\");\n      return new PerplexityLLM(embedder, model);\n    case \"openrouter\":\n      const { OpenRouterLLM } = require(\"../AiProviders/openRouter\");\n      return new OpenRouterLLM(embedder, model);\n    case \"mistral\":\n      const { MistralLLM } = require(\"../AiProviders/mistral\");\n      return new MistralLLM(embedder, model);\n    case \"huggingface\":\n      const { HuggingFaceLLM } = require(\"../AiProviders/huggingface\");\n      return new HuggingFaceLLM(embedder, model);\n    case \"groq\":\n      const { GroqLLM } = require(\"../AiProviders/groq\");\n      return new GroqLLM(embedder, model);\n    case \"koboldcpp\":\n      const { KoboldCPPLLM } = require(\"../AiProviders/koboldCPP\");\n      return new KoboldCPPLLM(embedder, model);\n    case \"textgenwebui\":\n      const { TextGenWebUILLM } = require(\"../AiProviders/textGenWebUI\");\n      return new TextGenWebUILLM(embedder, model);\n    case \"cohere\":\n      const { CohereLLM } = require(\"../AiProviders/cohere\");\n      return new CohereLLM(embedder, model);\n    case \"litellm\":\n      const { LiteLLM } = require(\"../AiProviders/liteLLM\");\n      return new LiteLLM(embedder, model);\n    case \"generic-openai\":\n      const { GenericOpenAiLLM } = require(\"../AiProviders/genericOpenAi\");\n      return new GenericOpenAiLLM(embedder, model);\n    case \"bedrock\":\n      const { AWSBedrockLLM } = require(\"../AiProviders/bedrock\");\n      return new AWSBedrockLLM(embedder, model);\n    case \"deepseek\":\n      const { DeepSeekLLM } = require(\"../AiProviders/deepseek\");\n      return new DeepSeekLLM(embedder, model);\n    case \"apipie\":\n      const { ApiPieLLM } = require(\"../AiProviders/apipie\");\n      return new ApiPieLLM(embedder, model);\n    case \"novita\":\n      const { NovitaLLM } = require(\"../AiProviders/novita\");\n      return new NovitaLLM(embedder, model);\n    case \"xai\":\n      const { XAiLLM } = require(\"../AiProviders/xai\");\n      return new XAiLLM(embedder, model);\n    case \"nvidia-nim\":\n      const { NvidiaNimLLM } = require(\"../AiProviders/nvidiaNim\");\n      return new NvidiaNimLLM(embedder, model);\n    case \"ppio\":\n      const { PPIOLLM } = require(\"../AiProviders/ppio\");\n      return new PPIOLLM(embedder, model);\n    case \"moonshotai\":\n      const { MoonshotAiLLM } = require(\"../AiProviders/moonshotAi\");\n      return new MoonshotAiLLM(embedder, model);\n    case \"dpais\":\n      const { DellProAiStudioLLM } = require(\"../AiProviders/dellProAiStudio\");\n      return new DellProAiStudioLLM(embedder, model);\n    case \"cometapi\":\n      const { CometApiLLM } = require(\"../AiProviders/cometapi\");\n      return new CometApiLLM(embedder, model);\n    case \"foundry\":\n      const { FoundryLLM } = require(\"../AiProviders/foundry\");\n      return new FoundryLLM(embedder, model);\n    case \"zai\":\n      const { ZAiLLM } = require(\"../AiProviders/zai\");\n      return new ZAiLLM(embedder, model);\n    case \"giteeai\":\n      const { GiteeAILLM } = require(\"../AiProviders/giteeai\");\n      return new GiteeAILLM(embedder, model);\n    case \"docker-model-runner\":\n      const {\n        DockerModelRunnerLLM,\n      } = require(\"../AiProviders/dockerModelRunner\");\n      return new DockerModelRunnerLLM(embedder, model);\n    case \"privatemode\":\n      const { PrivatemodeLLM } = require(\"../AiProviders/privatemode\");\n      return new PrivatemodeLLM(embedder, model);\n    case \"sambanova\":\n      const { SambaNovaLLM } = require(\"../AiProviders/sambanova\");\n      return new SambaNovaLLM(embedder, model);\n    case \"lemonade\":\n      const { LemonadeLLM } = require(\"../AiProviders/lemonade\");\n      return new LemonadeLLM(embedder, model);\n    default:\n      throw new Error(\n        `ENV: No valid LLM_PROVIDER value found in environment! Using ${process.env.LLM_PROVIDER}`\n      );\n  }\n}\n\n/**\n * Returns the EmbedderProvider by itself to whatever is currently in the system settings.\n * @returns {BaseEmbedderProvider}\n */\nfunction getEmbeddingEngineSelection() {\n  const { NativeEmbedder } = require(\"../EmbeddingEngines/native\");\n  const engineSelection = process.env.EMBEDDING_ENGINE;\n  switch (engineSelection) {\n    case \"openai\":\n      const { OpenAiEmbedder } = require(\"../EmbeddingEngines/openAi\");\n      return new OpenAiEmbedder();\n    case \"azure\":\n      const {\n        AzureOpenAiEmbedder,\n      } = require(\"../EmbeddingEngines/azureOpenAi\");\n      return new AzureOpenAiEmbedder();\n    case \"localai\":\n      const { LocalAiEmbedder } = require(\"../EmbeddingEngines/localAi\");\n      return new LocalAiEmbedder();\n    case \"ollama\":\n      const { OllamaEmbedder } = require(\"../EmbeddingEngines/ollama\");\n      return new OllamaEmbedder();\n    case \"native\":\n      return new NativeEmbedder();\n    case \"lmstudio\":\n      const { LMStudioEmbedder } = require(\"../EmbeddingEngines/lmstudio\");\n      return new LMStudioEmbedder();\n    case \"cohere\":\n      const { CohereEmbedder } = require(\"../EmbeddingEngines/cohere\");\n      return new CohereEmbedder();\n    case \"voyageai\":\n      const { VoyageAiEmbedder } = require(\"../EmbeddingEngines/voyageAi\");\n      return new VoyageAiEmbedder();\n    case \"litellm\":\n      const { LiteLLMEmbedder } = require(\"../EmbeddingEngines/liteLLM\");\n      return new LiteLLMEmbedder();\n    case \"mistral\":\n      const { MistralEmbedder } = require(\"../EmbeddingEngines/mistral\");\n      return new MistralEmbedder();\n    case \"generic-openai\":\n      const {\n        GenericOpenAiEmbedder,\n      } = require(\"../EmbeddingEngines/genericOpenAi\");\n      return new GenericOpenAiEmbedder();\n    case \"gemini\":\n      const { GeminiEmbedder } = require(\"../EmbeddingEngines/gemini\");\n      return new GeminiEmbedder();\n    case \"openrouter\":\n      const { OpenRouterEmbedder } = require(\"../EmbeddingEngines/openRouter\");\n      return new OpenRouterEmbedder();\n    case \"lemonade\":\n      const { LemonadeEmbedder } = require(\"../EmbeddingEngines/lemonade\");\n      return new LemonadeEmbedder();\n    default:\n      return new NativeEmbedder();\n  }\n}\n\n/**\n * Returns the LLMProviderClass - this is a helper method to access static methods on a class\n * @param {{provider: string | null} | null} params - Initialize params for LLMs provider\n * @returns {BaseLLMProviderClass}\n */\nfunction getLLMProviderClass({ provider = null } = {}) {\n  switch (provider) {\n    case \"openai\":\n      const { OpenAiLLM } = require(\"../AiProviders/openAi\");\n      return OpenAiLLM;\n    case \"azure\":\n      const { AzureOpenAiLLM } = require(\"../AiProviders/azureOpenAi\");\n      return AzureOpenAiLLM;\n    case \"anthropic\":\n      const { AnthropicLLM } = require(\"../AiProviders/anthropic\");\n      return AnthropicLLM;\n    case \"gemini\":\n      const { GeminiLLM } = require(\"../AiProviders/gemini\");\n      return GeminiLLM;\n    case \"lmstudio\":\n      const { LMStudioLLM } = require(\"../AiProviders/lmStudio\");\n      return LMStudioLLM;\n    case \"localai\":\n      const { LocalAiLLM } = require(\"../AiProviders/localAi\");\n      return LocalAiLLM;\n    case \"ollama\":\n      const { OllamaAILLM } = require(\"../AiProviders/ollama\");\n      return OllamaAILLM;\n    case \"togetherai\":\n      const { TogetherAiLLM } = require(\"../AiProviders/togetherAi\");\n      return TogetherAiLLM;\n    case \"fireworksai\":\n      const { FireworksAiLLM } = require(\"../AiProviders/fireworksAi\");\n      return FireworksAiLLM;\n    case \"perplexity\":\n      const { PerplexityLLM } = require(\"../AiProviders/perplexity\");\n      return PerplexityLLM;\n    case \"openrouter\":\n      const { OpenRouterLLM } = require(\"../AiProviders/openRouter\");\n      return OpenRouterLLM;\n    case \"mistral\":\n      const { MistralLLM } = require(\"../AiProviders/mistral\");\n      return MistralLLM;\n    case \"huggingface\":\n      const { HuggingFaceLLM } = require(\"../AiProviders/huggingface\");\n      return HuggingFaceLLM;\n    case \"groq\":\n      const { GroqLLM } = require(\"../AiProviders/groq\");\n      return GroqLLM;\n    case \"koboldcpp\":\n      const { KoboldCPPLLM } = require(\"../AiProviders/koboldCPP\");\n      return KoboldCPPLLM;\n    case \"textgenwebui\":\n      const { TextGenWebUILLM } = require(\"../AiProviders/textGenWebUI\");\n      return TextGenWebUILLM;\n    case \"cohere\":\n      const { CohereLLM } = require(\"../AiProviders/cohere\");\n      return CohereLLM;\n    case \"litellm\":\n      const { LiteLLM } = require(\"../AiProviders/liteLLM\");\n      return LiteLLM;\n    case \"generic-openai\":\n      const { GenericOpenAiLLM } = require(\"../AiProviders/genericOpenAi\");\n      return GenericOpenAiLLM;\n    case \"bedrock\":\n      const { AWSBedrockLLM } = require(\"../AiProviders/bedrock\");\n      return AWSBedrockLLM;\n    case \"deepseek\":\n      const { DeepSeekLLM } = require(\"../AiProviders/deepseek\");\n      return DeepSeekLLM;\n    case \"apipie\":\n      const { ApiPieLLM } = require(\"../AiProviders/apipie\");\n      return ApiPieLLM;\n    case \"novita\":\n      const { NovitaLLM } = require(\"../AiProviders/novita\");\n      return NovitaLLM;\n    case \"xai\":\n      const { XAiLLM } = require(\"../AiProviders/xai\");\n      return XAiLLM;\n    case \"nvidia-nim\":\n      const { NvidiaNimLLM } = require(\"../AiProviders/nvidiaNim\");\n      return NvidiaNimLLM;\n    case \"ppio\":\n      const { PPIOLLM } = require(\"../AiProviders/ppio\");\n      return PPIOLLM;\n    case \"dpais\":\n      const { DellProAiStudioLLM } = require(\"../AiProviders/dellProAiStudio\");\n      return DellProAiStudioLLM;\n    case \"moonshotai\":\n      const { MoonshotAiLLM } = require(\"../AiProviders/moonshotAi\");\n      return MoonshotAiLLM;\n    case \"cometapi\":\n      const { CometApiLLM } = require(\"../AiProviders/cometapi\");\n      return CometApiLLM;\n    case \"foundry\":\n      const { FoundryLLM } = require(\"../AiProviders/foundry\");\n      return FoundryLLM;\n    case \"zai\":\n      const { ZAiLLM } = require(\"../AiProviders/zai\");\n      return ZAiLLM;\n    case \"giteeai\":\n      const { GiteeAILLM } = require(\"../AiProviders/giteeai\");\n      return GiteeAILLM;\n    case \"docker-model-runner\":\n      const {\n        DockerModelRunnerLLM,\n      } = require(\"../AiProviders/dockerModelRunner\");\n      return DockerModelRunnerLLM;\n    case \"privatemode\":\n      const { PrivateModeLLM } = require(\"../AiProviders/privatemode\");\n      return PrivateModeLLM;\n    case \"sambanova\":\n      const { SambaNovaLLM } = require(\"../AiProviders/sambanova\");\n      return SambaNovaLLM;\n    case \"lemonade\":\n      const { LemonadeLLM } = require(\"../AiProviders/lemonade\");\n      return LemonadeLLM;\n    default:\n      return null;\n  }\n}\n\n/**\n * Returns the defined model (if available) for the given provider.\n * @param {{provider: string | null} | null} params - Initialize params for LLMs provider\n * @returns {string | null}\n */\nfunction getBaseLLMProviderModel({ provider = null } = {}) {\n  switch (provider) {\n    case \"openai\":\n      return process.env.OPEN_MODEL_PREF;\n    case \"azure\":\n      return process.env.AZURE_OPENAI_MODEL_PREF || process.env.OPEN_MODEL_PREF;\n    case \"anthropic\":\n      return process.env.ANTHROPIC_MODEL_PREF;\n    case \"gemini\":\n      return process.env.GEMINI_LLM_MODEL_PREF;\n    case \"lmstudio\":\n      return process.env.LMSTUDIO_MODEL_PREF;\n    case \"localai\":\n      return process.env.LOCAL_AI_MODEL_PREF;\n    case \"ollama\":\n      return process.env.OLLAMA_MODEL_PREF;\n    case \"togetherai\":\n      return process.env.TOGETHER_AI_MODEL_PREF;\n    case \"fireworksai\":\n      return process.env.FIREWORKS_AI_LLM_MODEL_PREF;\n    case \"perplexity\":\n      return process.env.PERPLEXITY_MODEL_PREF;\n    case \"openrouter\":\n      return process.env.OPENROUTER_MODEL_PREF;\n    case \"mistral\":\n      return process.env.MISTRAL_MODEL_PREF;\n    case \"huggingface\":\n      return null;\n    case \"groq\":\n      return process.env.GROQ_MODEL_PREF;\n    case \"koboldcpp\":\n      return process.env.KOBOLD_CPP_MODEL_PREF;\n    case \"textgenwebui\":\n      return null;\n    case \"cohere\":\n      return process.env.COHERE_MODEL_PREF;\n    case \"litellm\":\n      return process.env.LITE_LLM_MODEL_PREF;\n    case \"generic-openai\":\n      return process.env.GENERIC_OPEN_AI_MODEL_PREF;\n    case \"bedrock\":\n      return process.env.AWS_BEDROCK_LLM_MODEL_PREFERENCE;\n    case \"deepseek\":\n      return process.env.DEEPSEEK_MODEL_PREF;\n    case \"apipie\":\n      return process.env.APIPIE_LLM_MODEL_PREF;\n    case \"novita\":\n      return process.env.NOVITA_LLM_MODEL_PREF;\n    case \"xai\":\n      return process.env.XAI_LLM_MODEL_PREF;\n    case \"nvidia-nim\":\n      return process.env.NVIDIA_NIM_LLM_MODEL_PREF;\n    case \"ppio\":\n      return process.env.PPIO_MODEL_PREF;\n    case \"dpais\":\n      return process.env.DPAIS_LLM_MODEL_PREF;\n    case \"moonshotai\":\n      return process.env.MOONSHOT_AI_MODEL_PREF;\n    case \"cometapi\":\n      return process.env.COMETAPI_LLM_MODEL_PREF;\n    case \"foundry\":\n      return process.env.FOUNDRY_MODEL_PREF;\n    case \"zai\":\n      return process.env.ZAI_MODEL_PREF;\n    case \"giteeai\":\n      return process.env.GITEE_AI_MODEL_PREF;\n    case \"docker-model-runner\":\n      return process.env.DOCKER_MODEL_RUNNER_LLM_MODEL_PREF;\n    case \"privatemode\":\n      return process.env.PRIVATEMODE_LLM_MODEL_PREF;\n    case \"sambanova\":\n      return process.env.SAMBANOVA_LLM_MODEL_PREF;\n    case \"lemonade\":\n      return process.env.LEMONADE_LLM_MODEL_PREF;\n    default:\n      return null;\n  }\n}\n\n// Some models have lower restrictions on chars that can be encoded in a single pass\n// and by default we assume it can handle 1,000 chars, but some models use work with smaller\n// chars so here we can override that value when embedding information.\nfunction maximumChunkLength() {\n  if (\n    !!process.env.EMBEDDING_MODEL_MAX_CHUNK_LENGTH &&\n    !isNaN(process.env.EMBEDDING_MODEL_MAX_CHUNK_LENGTH) &&\n    Number(process.env.EMBEDDING_MODEL_MAX_CHUNK_LENGTH) > 1\n  )\n    return Number(process.env.EMBEDDING_MODEL_MAX_CHUNK_LENGTH);\n\n  return 1_000;\n}\n\nfunction toChunks(arr, size) {\n  return Array.from({ length: Math.ceil(arr.length / size) }, (_v, i) =>\n    arr.slice(i * size, i * size + size)\n  );\n}\n\nfunction humanFileSize(bytes, si = false, dp = 1) {\n  const thresh = si ? 1000 : 1024;\n\n  if (Math.abs(bytes) < thresh) {\n    return bytes + \" B\";\n  }\n\n  const units = si\n    ? [\"kB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"]\n    : [\"KiB\", \"MiB\", \"GiB\", \"TiB\", \"PiB\", \"EiB\", \"ZiB\", \"YiB\"];\n  let u = -1;\n  const r = 10 ** dp;\n\n  do {\n    bytes /= thresh;\n    ++u;\n  } while (\n    Math.round(Math.abs(bytes) * r) / r >= thresh &&\n    u < units.length - 1\n  );\n\n  return bytes.toFixed(dp) + \" \" + units[u];\n}\n\nmodule.exports = {\n  getEmbeddingEngineSelection,\n  maximumChunkLength,\n  getVectorDbClass,\n  getLLMProviderClass,\n  getBaseLLMProviderModel,\n  getLLMProvider,\n  toChunks,\n  humanFileSize,\n};\n"
  },
  {
    "path": "server/utils/helpers/portAvailabilityChecker.js",
    "content": "// Get all loopback addresses that are available for use or binding.\nfunction getLocalHosts() {\n  const os = require(\"os\");\n  const interfaces = os.networkInterfaces();\n  const results = new Set([undefined, \"0.0.0.0\"]);\n\n  for (const _interface of Object.values(interfaces)) {\n    for (const config of _interface) {\n      results.add(config.address);\n    }\n  }\n\n  return Array.from(results);\n}\n\nfunction checkPort(options = {}) {\n  const net = require(\"net\");\n  return new Promise((resolve, reject) => {\n    const server = net.createServer();\n    server.unref();\n    server.on(\"error\", reject);\n\n    server.listen(options, () => {\n      server.close(() => {\n        resolve(true);\n      });\n    });\n  });\n}\n\nasync function isPortInUse(port, host) {\n  try {\n    await checkPort({ port, host });\n    return true;\n  } catch (error) {\n    if (![\"EADDRNOTAVAIL\", \"EINVAL\"].includes(error.code)) {\n      return false;\n    }\n  }\n  return false;\n}\n\nmodule.exports = {\n  isPortInUse,\n  getLocalHosts,\n};\n"
  },
  {
    "path": "server/utils/helpers/search.js",
    "content": "const { Workspace } = require(\"../../models/workspace\");\nconst { WorkspaceThread } = require(\"../../models/workspaceThread\");\nconst fastLevenshtein = require(\"fast-levenshtein\");\n\n// allow a pretty loose levenshtein distance for the search\n// since we would rather show a few more results than less\nconst FAST_LEVENSHTEIN_DISTANCE = 3;\n\n/**\n * Search for workspaces and threads based on a search term with optional user context.\n * For each type of item we are looking at the `name` field.\n * - If the normalized name, starts with, includes, or ends with the search term => match\n * - If the normalized name is within 2 levenshtein distance of the search term => match\n * @param {string} searchTerm - The search term to search for.\n * @param {Object} user - The user to search for.\n * @returns {Promise<{workspaces: Array<{slug: string, name: string}>, threads: Array<{slug: string, name: string, workspace: {slug: string, name: string}}>}>} - The search results.\n */\nasync function searchWorkspaceAndThreads(searchTerm, user = null) {\n  searchTerm = String(searchTerm).trim(); // Ensure searchTerm is a string and trimmed.\n\n  if (!searchTerm || searchTerm.length < 3)\n    return { workspaces: [], threads: [] };\n  searchTerm = searchTerm.toLowerCase();\n\n  // To prevent duplicates in O(1) time, we use sets which will be\n  // STRINGIFIED results of matching workspaces or threads. We then\n  // parse them back into objects at the end.\n  const results = {\n    workspaces: new Set(),\n    threads: new Set(),\n  };\n\n  async function searchWorkspaces() {\n    const workspaces = !!user\n      ? await Workspace.whereWithUser(user)\n      : await Workspace.where();\n\n    for (const workspace of workspaces) {\n      const wsName = workspace.name.toLowerCase();\n      if (\n        wsName.startsWith(searchTerm) ||\n        wsName.includes(searchTerm) ||\n        wsName.endsWith(searchTerm) ||\n        fastLevenshtein.get(wsName, searchTerm) <= FAST_LEVENSHTEIN_DISTANCE\n      )\n        results.workspaces.add(\n          JSON.stringify({ slug: workspace.slug, name: workspace.name })\n        );\n    }\n  }\n\n  async function searchThreads() {\n    const threads = !!user\n      ? await WorkspaceThread.where(\n          { user_id: user.id },\n          undefined,\n          undefined,\n          { workspace: { select: { slug: true, name: true } } }\n        )\n      : await WorkspaceThread.where(undefined, undefined, undefined, {\n          workspace: { select: { slug: true, name: true } },\n        });\n\n    for (const thread of threads) {\n      const threadName = thread.name.toLowerCase();\n      if (\n        threadName.startsWith(searchTerm) ||\n        threadName.includes(searchTerm) ||\n        threadName.endsWith(searchTerm) ||\n        fastLevenshtein.get(threadName, searchTerm) <= FAST_LEVENSHTEIN_DISTANCE\n      )\n        results.threads.add(\n          JSON.stringify({\n            slug: thread.slug,\n            name: thread.name,\n            workspace: {\n              slug: thread.workspace.slug,\n              name: thread.workspace.name,\n            },\n          })\n        );\n    }\n  }\n\n  // Run both searches in parallel - this modifies the results set in place.\n  await Promise.all([searchWorkspaces(), searchThreads()]);\n\n  // Parse the results back into objects.\n  const workspaces = Array.from(results.workspaces).map(JSON.parse);\n  const threads = Array.from(results.threads).map(JSON.parse);\n  return { workspaces, threads };\n}\n\nmodule.exports = { searchWorkspaceAndThreads };\n"
  },
  {
    "path": "server/utils/helpers/shell.js",
    "content": "/**\n * Patch the shell environment path to ensure the PATH is properly set for the current platform.\n * On Docker, we are on Node v18 and cannot support fix-path v5.\n * So we need to use the ESM-style import() to import the fix-path module + add the strip-ansi call to patch the PATH, which is the only change between v4 and v5.\n * https://github.com/sindresorhus/fix-path/issues/6\n * @returns {Promise<{[key: string]: string}>} - Environment variables from shell\n */\nasync function patchShellEnvironmentPath() {\n  try {\n    if (process.platform === \"win32\") return process.env;\n    const { default: fixPath } = await import(\"fix-path\");\n    const { default: stripAnsi } = await import(\"strip-ansi\");\n    fixPath();\n    if (process.env.PATH) process.env.PATH = stripAnsi(process.env.PATH);\n    console.log(\"Shell environment path patched successfully.\");\n    return process.env;\n  } catch (error) {\n    console.error(\"Failed to patch shell environment path:\", error);\n    return process.env;\n  }\n}\n\nmodule.exports = {\n  patchShellEnvironmentPath,\n};\n"
  },
  {
    "path": "server/utils/helpers/tiktoken.js",
    "content": "const { getEncodingNameForModel, getEncoding } = require(\"js-tiktoken\");\n\n/**\n * @class TokenManager\n *\n * @notice\n * We cannot do estimation of tokens here like we do in the collector\n * because we need to know the model to do it.\n * Other issues are we also do reverse tokenization here for the chat history during cannonballing.\n * So here we are stuck doing the actual tokenization and encoding until we figure out what to do with prompt overflows.\n */\nclass TokenManager {\n  static instance = null;\n  static currentModel = null;\n\n  constructor(model = \"gpt-3.5-turbo\") {\n    if (TokenManager.instance && TokenManager.currentModel === model) {\n      this.log(\"Returning existing instance for model:\", model);\n      return TokenManager.instance;\n    }\n\n    this.model = model;\n    this.encoderName = this.#getEncodingFromModel(model);\n    this.encoder = getEncoding(this.encoderName);\n\n    TokenManager.instance = this;\n    TokenManager.currentModel = model;\n    this.log(\"Initialized new TokenManager instance for model:\", model);\n    return this;\n  }\n\n  log(text, ...args) {\n    console.log(`\\x1b[35m[TokenManager]\\x1b[0m ${text}`, ...args);\n  }\n\n  #getEncodingFromModel(model) {\n    try {\n      return getEncodingNameForModel(model);\n    } catch {\n      return \"cl100k_base\";\n    }\n  }\n\n  /**\n   * Pass in an empty array of disallowedSpecials to handle all tokens as text and to be tokenized.\n   * @param {string} input\n   * @returns {number[]}\n   */\n  tokensFromString(input = \"\") {\n    try {\n      const tokens = this.encoder.encode(String(input), undefined, []);\n      return tokens;\n    } catch (e) {\n      console.error(e);\n      return [];\n    }\n  }\n\n  /**\n   * Converts an array of tokens back to a string.\n   * @param {number[]} tokens\n   * @returns {string}\n   */\n  bytesFromTokens(tokens = []) {\n    const bytes = this.encoder.decode(tokens);\n    return bytes;\n  }\n\n  /**\n   * Counts the number of tokens in a string.\n   * @param {string} input\n   * @returns {number}\n   */\n  countFromString(input = \"\") {\n    const tokens = this.tokensFromString(input);\n    return tokens.length;\n  }\n\n  /**\n   * Estimates the number of tokens in a string or array of strings.\n   * @param {string | string[]} input\n   * @returns {number}\n   */\n  statsFrom(input) {\n    if (typeof input === \"string\") return this.countFromString(input);\n\n    // What is going on here?\n    // https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb Item 6.\n    // The only option is to estimate. From repeated testing using the static values in the code we are always 2 off,\n    // which means as of Nov 1, 2023 the additional factor on ln: 476 changed from 3 to 5.\n    if (Array.isArray(input)) {\n      const perMessageFactorTokens = input.length * 3;\n      const tokensFromContent = input.reduce(\n        (a, b) => a + this.countFromString(b.content),\n        0\n      );\n      const diffCoefficient = 5;\n      return perMessageFactorTokens + tokensFromContent + diffCoefficient;\n    }\n\n    throw new Error(\"Not a supported tokenized format.\");\n  }\n}\n\nmodule.exports = {\n  TokenManager,\n};\n"
  },
  {
    "path": "server/utils/helpers/updateENV.js",
    "content": "const { Telemetry } = require(\"../../models/telemetry\");\nconst {\n  SUPPORTED_CONNECTION_METHODS,\n} = require(\"../AiProviders/bedrock/utils\");\nconst { resetAllVectorStores } = require(\"../vectorStore/resetAllVectorStores\");\n\nconst KEY_MAPPING = {\n  LLMProvider: {\n    envKey: \"LLM_PROVIDER\",\n    checks: [isNotEmpty, supportedLLM],\n  },\n  // OpenAI Settings\n  OpenAiKey: {\n    envKey: \"OPEN_AI_KEY\",\n    checks: [isNotEmpty, validOpenAIKey],\n  },\n  OpenAiModelPref: {\n    envKey: \"OPEN_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  // Azure OpenAI Settings\n  AzureOpenAiEndpoint: {\n    envKey: \"AZURE_OPENAI_ENDPOINT\",\n    checks: [isNotEmpty],\n  },\n  AzureOpenAiTokenLimit: {\n    envKey: \"AZURE_OPENAI_TOKEN_LIMIT\",\n    checks: [validOpenAiTokenLimit],\n  },\n  AzureOpenAiKey: {\n    envKey: \"AZURE_OPENAI_KEY\",\n    checks: [isNotEmpty],\n  },\n  AzureOpenAiModelPref: {\n    envKey: \"AZURE_OPENAI_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  AzureOpenAiEmbeddingModelPref: {\n    envKey: \"EMBEDDING_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  AzureOpenAiModelType: {\n    envKey: \"AZURE_OPENAI_MODEL_TYPE\",\n    checks: [\n      (input) =>\n        [\"default\", \"reasoning\"].includes(input)\n          ? null\n          : \"Invalid model type. Must be one of: default, reasoning.\",\n    ],\n  },\n\n  // Anthropic Settings\n  AnthropicApiKey: {\n    envKey: \"ANTHROPIC_API_KEY\",\n    checks: [isNotEmpty, validAnthropicApiKey],\n  },\n  AnthropicModelPref: {\n    envKey: \"ANTHROPIC_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  AnthropicCacheControl: {\n    envKey: \"ANTHROPIC_CACHE_CONTROL\",\n    checks: [\n      (input) =>\n        [\"none\", \"5m\", \"1h\"].includes(input)\n          ? null\n          : \"Invalid cache control. Must be one of: 5m, 1h.\",\n    ],\n  },\n\n  GeminiLLMApiKey: {\n    envKey: \"GEMINI_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  GeminiLLMModelPref: {\n    envKey: \"GEMINI_LLM_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  GeminiSafetySetting: {\n    envKey: \"GEMINI_SAFETY_SETTING\",\n    checks: [validGeminiSafetySetting],\n  },\n\n  // LMStudio Settings\n  LMStudioBasePath: {\n    envKey: \"LMSTUDIO_BASE_PATH\",\n    checks: [isNotEmpty, validLLMExternalBasePath, validDockerizedUrl],\n  },\n  LMStudioModelPref: {\n    envKey: \"LMSTUDIO_MODEL_PREF\",\n    checks: [],\n  },\n  LMStudioTokenLimit: {\n    envKey: \"LMSTUDIO_MODEL_TOKEN_LIMIT\",\n    checks: [],\n  },\n  LMStudioAuthToken: {\n    envKey: \"LMSTUDIO_AUTH_TOKEN\",\n    checks: [],\n  },\n\n  // LocalAI Settings\n  LocalAiBasePath: {\n    envKey: \"LOCAL_AI_BASE_PATH\",\n    checks: [isNotEmpty, validLLMExternalBasePath, validDockerizedUrl],\n  },\n  LocalAiModelPref: {\n    envKey: \"LOCAL_AI_MODEL_PREF\",\n    checks: [],\n  },\n  LocalAiTokenLimit: {\n    envKey: \"LOCAL_AI_MODEL_TOKEN_LIMIT\",\n    checks: [nonZero],\n  },\n  LocalAiApiKey: {\n    envKey: \"LOCAL_AI_API_KEY\",\n    checks: [],\n  },\n\n  OllamaLLMBasePath: {\n    envKey: \"OLLAMA_BASE_PATH\",\n    checks: [isNotEmpty, validOllamaLLMBasePath, validDockerizedUrl],\n  },\n  OllamaLLMModelPref: {\n    envKey: \"OLLAMA_MODEL_PREF\",\n    checks: [],\n  },\n  OllamaLLMTokenLimit: {\n    envKey: \"OLLAMA_MODEL_TOKEN_LIMIT\",\n    checks: [],\n  },\n  OllamaLLMKeepAliveSeconds: {\n    envKey: \"OLLAMA_KEEP_ALIVE_TIMEOUT\",\n    checks: [isInteger],\n  },\n  OllamaLLMAuthToken: {\n    envKey: \"OLLAMA_AUTH_TOKEN\",\n    checks: [],\n  },\n\n  // Mistral AI API Settings\n  MistralApiKey: {\n    envKey: \"MISTRAL_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  MistralModelPref: {\n    envKey: \"MISTRAL_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // Hugging Face LLM Inference Settings\n  HuggingFaceLLMEndpoint: {\n    envKey: \"HUGGING_FACE_LLM_ENDPOINT\",\n    checks: [isNotEmpty, isValidURL, validHuggingFaceEndpoint],\n  },\n  HuggingFaceLLMAccessToken: {\n    envKey: \"HUGGING_FACE_LLM_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  HuggingFaceLLMTokenLimit: {\n    envKey: \"HUGGING_FACE_LLM_TOKEN_LIMIT\",\n    checks: [nonZero],\n  },\n\n  // KoboldCPP Settings\n  KoboldCPPBasePath: {\n    envKey: \"KOBOLD_CPP_BASE_PATH\",\n    checks: [isNotEmpty, isValidURL],\n  },\n  KoboldCPPModelPref: {\n    envKey: \"KOBOLD_CPP_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  KoboldCPPTokenLimit: {\n    envKey: \"KOBOLD_CPP_MODEL_TOKEN_LIMIT\",\n    checks: [nonZero],\n  },\n  KoboldCPPMaxTokens: {\n    envKey: \"KOBOLD_CPP_MAX_TOKENS\",\n    checks: [nonZero],\n  },\n\n  // Text Generation Web UI Settings\n  TextGenWebUIBasePath: {\n    envKey: \"TEXT_GEN_WEB_UI_BASE_PATH\",\n    checks: [isValidURL],\n  },\n  TextGenWebUITokenLimit: {\n    envKey: \"TEXT_GEN_WEB_UI_MODEL_TOKEN_LIMIT\",\n    checks: [nonZero],\n  },\n  TextGenWebUIAPIKey: {\n    envKey: \"TEXT_GEN_WEB_UI_API_KEY\",\n    checks: [],\n  },\n\n  // LiteLLM Settings\n  LiteLLMModelPref: {\n    envKey: \"LITE_LLM_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  LiteLLMTokenLimit: {\n    envKey: \"LITE_LLM_MODEL_TOKEN_LIMIT\",\n    checks: [nonZero],\n  },\n  LiteLLMBasePath: {\n    envKey: \"LITE_LLM_BASE_PATH\",\n    checks: [isValidURL],\n  },\n  LiteLLMApiKey: {\n    envKey: \"LITE_LLM_API_KEY\",\n    checks: [],\n  },\n\n  // Generic OpenAI InferenceSettings\n  GenericOpenAiBasePath: {\n    envKey: \"GENERIC_OPEN_AI_BASE_PATH\",\n    checks: [isValidURL],\n  },\n  GenericOpenAiModelPref: {\n    envKey: \"GENERIC_OPEN_AI_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  GenericOpenAiTokenLimit: {\n    envKey: \"GENERIC_OPEN_AI_MODEL_TOKEN_LIMIT\",\n    checks: [nonZero],\n  },\n  GenericOpenAiKey: {\n    envKey: \"GENERIC_OPEN_AI_API_KEY\",\n    checks: [],\n  },\n  GenericOpenAiMaxTokens: {\n    envKey: \"GENERIC_OPEN_AI_MAX_TOKENS\",\n    checks: [nonZero],\n  },\n\n  // AWS Bedrock LLM InferenceSettings\n  AwsBedrockLLMConnectionMethod: {\n    envKey: \"AWS_BEDROCK_LLM_CONNECTION_METHOD\",\n    checks: [\n      (input) =>\n        SUPPORTED_CONNECTION_METHODS.includes(input) ? null : \"invalid Value\",\n    ],\n  },\n  AwsBedrockLLMAccessKeyId: {\n    envKey: \"AWS_BEDROCK_LLM_ACCESS_KEY_ID\",\n    checks: [],\n  },\n  AwsBedrockLLMAccessKey: {\n    envKey: \"AWS_BEDROCK_LLM_ACCESS_KEY\",\n    checks: [],\n  },\n  AwsBedrockLLMSessionToken: {\n    envKey: \"AWS_BEDROCK_LLM_SESSION_TOKEN\",\n    checks: [],\n  },\n  AwsBedrockLLMAPIKey: {\n    envKey: \"AWS_BEDROCK_LLM_API_KEY\",\n    checks: [],\n  },\n  AwsBedrockLLMRegion: {\n    envKey: \"AWS_BEDROCK_LLM_REGION\",\n    checks: [isNotEmpty],\n  },\n  AwsBedrockLLMModel: {\n    envKey: \"AWS_BEDROCK_LLM_MODEL_PREFERENCE\",\n    checks: [isNotEmpty],\n  },\n  AwsBedrockLLMTokenLimit: {\n    envKey: \"AWS_BEDROCK_LLM_MODEL_TOKEN_LIMIT\",\n    checks: [nonZero],\n  },\n  AwsBedrockLLMMaxOutputTokens: {\n    envKey: \"AWS_BEDROCK_LLM_MAX_OUTPUT_TOKENS\",\n    checks: [nonZero],\n  },\n\n  // Dell Pro AI Studio Settings\n  DellProAiStudioBasePath: {\n    envKey: \"DPAIS_LLM_BASE_PATH\",\n    checks: [isNotEmpty, validDockerizedUrl],\n  },\n  DellProAiStudioModelPref: {\n    envKey: \"DPAIS_LLM_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  DellProAiStudioTokenLimit: {\n    envKey: \"DPAIS_LLM_MODEL_TOKEN_LIMIT\",\n    checks: [nonZero],\n  },\n\n  EmbeddingEngine: {\n    envKey: \"EMBEDDING_ENGINE\",\n    checks: [supportedEmbeddingModel],\n    postUpdate: [handleVectorStoreReset],\n  },\n  EmbeddingBasePath: {\n    envKey: \"EMBEDDING_BASE_PATH\",\n    checks: [isNotEmpty, validDockerizedUrl],\n  },\n  EmbeddingModelPref: {\n    envKey: \"EMBEDDING_MODEL_PREF\",\n    checks: [isNotEmpty],\n    postUpdate: [handleVectorStoreReset, downloadEmbeddingModelIfRequired],\n  },\n  EmbeddingModelMaxChunkLength: {\n    envKey: \"EMBEDDING_MODEL_MAX_CHUNK_LENGTH\",\n    checks: [nonZero],\n  },\n  EmbeddingOutputDimensions: {\n    envKey: \"EMBEDDING_OUTPUT_DIMENSIONS\",\n    checks: [],\n  },\n  OllamaEmbeddingBatchSize: {\n    envKey: \"OLLAMA_EMBEDDING_BATCH_SIZE\",\n    checks: [nonZero],\n  },\n\n  // Gemini Embedding Settings\n  GeminiEmbeddingApiKey: {\n    envKey: \"GEMINI_EMBEDDING_API_KEY\",\n    checks: [isNotEmpty],\n  },\n\n  // Generic OpenAI Embedding Settings\n  GenericOpenAiEmbeddingApiKey: {\n    envKey: \"GENERIC_OPEN_AI_EMBEDDING_API_KEY\",\n    checks: [],\n  },\n  GenericOpenAiEmbeddingMaxConcurrentChunks: {\n    envKey: \"GENERIC_OPEN_AI_EMBEDDING_MAX_CONCURRENT_CHUNKS\",\n    checks: [nonZero],\n  },\n\n  // Vector Database Selection Settings\n  VectorDB: {\n    envKey: \"VECTOR_DB\",\n    checks: [isNotEmpty, supportedVectorDB],\n    postUpdate: [handleVectorStoreReset],\n  },\n\n  // Chroma Options\n  ChromaEndpoint: {\n    envKey: \"CHROMA_ENDPOINT\",\n    checks: [isValidURL, validChromaURL, validDockerizedUrl],\n  },\n  ChromaApiHeader: {\n    envKey: \"CHROMA_API_HEADER\",\n    checks: [],\n  },\n  ChromaApiKey: {\n    envKey: \"CHROMA_API_KEY\",\n    checks: [],\n  },\n\n  // ChromaCloud Options\n  ChromaCloudApiKey: {\n    envKey: \"CHROMACLOUD_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  ChromaCloudTenant: {\n    envKey: \"CHROMACLOUD_TENANT\",\n    checks: [isNotEmpty],\n  },\n  ChromaCloudDatabase: {\n    envKey: \"CHROMACLOUD_DATABASE\",\n    checks: [isNotEmpty],\n  },\n\n  // Weaviate Options\n  WeaviateEndpoint: {\n    envKey: \"WEAVIATE_ENDPOINT\",\n    checks: [isValidURL, validDockerizedUrl],\n  },\n  WeaviateApiKey: {\n    envKey: \"WEAVIATE_API_KEY\",\n    checks: [],\n  },\n\n  // QDrant Options\n  QdrantEndpoint: {\n    envKey: \"QDRANT_ENDPOINT\",\n    checks: [isValidURL, validDockerizedUrl],\n  },\n  QdrantApiKey: {\n    envKey: \"QDRANT_API_KEY\",\n    checks: [],\n  },\n  PineConeKey: {\n    envKey: \"PINECONE_API_KEY\",\n    checks: [],\n  },\n  PineConeIndex: {\n    envKey: \"PINECONE_INDEX\",\n    checks: [],\n  },\n\n  // Milvus Options\n  MilvusAddress: {\n    envKey: \"MILVUS_ADDRESS\",\n    checks: [isValidURL, validDockerizedUrl],\n  },\n  MilvusUsername: {\n    envKey: \"MILVUS_USERNAME\",\n    checks: [isNotEmpty],\n  },\n  MilvusPassword: {\n    envKey: \"MILVUS_PASSWORD\",\n    checks: [isNotEmpty],\n  },\n\n  // Zilliz Cloud Options\n  ZillizEndpoint: {\n    envKey: \"ZILLIZ_ENDPOINT\",\n    checks: [isValidURL],\n  },\n  ZillizApiToken: {\n    envKey: \"ZILLIZ_API_TOKEN\",\n    checks: [isNotEmpty],\n  },\n\n  // Astra DB Options\n  AstraDBApplicationToken: {\n    envKey: \"ASTRA_DB_APPLICATION_TOKEN\",\n    checks: [isNotEmpty],\n  },\n  AstraDBEndpoint: {\n    envKey: \"ASTRA_DB_ENDPOINT\",\n    checks: [isNotEmpty],\n  },\n\n  /*\n  PGVector Options\n  - Does very simple validations - we should expand this in the future\n  - to ensure the connection string is valid and the table name is valid\n  - via direct query\n  */\n  PGVectorConnectionString: {\n    envKey: \"PGVECTOR_CONNECTION_STRING\",\n    checks: [isNotEmpty, looksLikePostgresConnectionString],\n    preUpdate: [validatePGVectorConnectionString],\n  },\n  PGVectorTableName: {\n    envKey: \"PGVECTOR_TABLE_NAME\",\n    checks: [isNotEmpty],\n    preUpdate: [validatePGVectorTableName],\n  },\n\n  // Together Ai Options\n  TogetherAiApiKey: {\n    envKey: \"TOGETHER_AI_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  TogetherAiModelPref: {\n    envKey: \"TOGETHER_AI_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // Fireworks AI Options\n  FireworksAiLLMApiKey: {\n    envKey: \"FIREWORKS_AI_LLM_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  FireworksAiLLMModelPref: {\n    envKey: \"FIREWORKS_AI_LLM_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // Perplexity Options\n  PerplexityApiKey: {\n    envKey: \"PERPLEXITY_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  PerplexityModelPref: {\n    envKey: \"PERPLEXITY_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // OpenRouter Options\n  OpenRouterApiKey: {\n    envKey: \"OPENROUTER_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  OpenRouterModelPref: {\n    envKey: \"OPENROUTER_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  OpenRouterTimeout: {\n    envKey: \"OPENROUTER_TIMEOUT_MS\",\n    checks: [],\n  },\n\n  // Novita Options\n  NovitaLLMApiKey: {\n    envKey: \"NOVITA_LLM_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  NovitaLLMModelPref: {\n    envKey: \"NOVITA_LLM_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  NovitaLLMTimeout: {\n    envKey: \"NOVITA_LLM_TIMEOUT_MS\",\n    checks: [],\n  },\n\n  // Groq Options\n  GroqApiKey: {\n    envKey: \"GROQ_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  GroqModelPref: {\n    envKey: \"GROQ_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // Cohere Options\n  CohereApiKey: {\n    envKey: \"COHERE_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  CohereModelPref: {\n    envKey: \"COHERE_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // VoyageAi Options\n  VoyageAiApiKey: {\n    envKey: \"VOYAGEAI_API_KEY\",\n    checks: [isNotEmpty],\n  },\n\n  // Whisper (transcription) providers\n  WhisperProvider: {\n    envKey: \"WHISPER_PROVIDER\",\n    checks: [isNotEmpty, supportedTranscriptionProvider],\n    postUpdate: [],\n  },\n  WhisperModelPref: {\n    envKey: \"WHISPER_MODEL_PREF\",\n    checks: [validLocalWhisper],\n    postUpdate: [],\n  },\n\n  // System Settings\n  AuthToken: {\n    envKey: \"AUTH_TOKEN\",\n    checks: [requiresForceMode, noRestrictedChars],\n  },\n  JWTSecret: {\n    envKey: \"JWT_SECRET\",\n    checks: [requiresForceMode],\n  },\n  DisableTelemetry: {\n    envKey: \"DISABLE_TELEMETRY\",\n    checks: [],\n    preUpdate: [\n      (_, __, nextValue) => {\n        if (nextValue === \"true\") Telemetry.sendTelemetry(\"telemetry_disabled\");\n      },\n    ],\n  },\n\n  // Agent Integration ENVs\n  AgentSerpApiKey: {\n    envKey: \"AGENT_SERPAPI_API_KEY\",\n    checks: [],\n  },\n  AgentSerpApiEngine: {\n    envKey: \"AGENT_SERPAPI_ENGINE\",\n    checks: [],\n  },\n  AgentSearchApiKey: {\n    envKey: \"AGENT_SEARCHAPI_API_KEY\",\n    checks: [],\n  },\n  AgentSearchApiEngine: {\n    envKey: \"AGENT_SEARCHAPI_ENGINE\",\n    checks: [],\n  },\n  AgentSerperApiKey: {\n    envKey: \"AGENT_SERPER_DEV_KEY\",\n    checks: [],\n  },\n  AgentBingSearchApiKey: {\n    envKey: \"AGENT_BING_SEARCH_API_KEY\",\n    checks: [],\n  },\n  AgentSerplyApiKey: {\n    envKey: \"AGENT_SERPLY_API_KEY\",\n    checks: [],\n  },\n  AgentSearXNGApiUrl: {\n    envKey: \"AGENT_SEARXNG_API_URL\",\n    checks: [],\n  },\n  AgentTavilyApiKey: {\n    envKey: \"AGENT_TAVILY_API_KEY\",\n    checks: [],\n  },\n  AgentExaApiKey: {\n    envKey: \"AGENT_EXA_API_KEY\",\n    checks: [],\n  },\n  AgentPerplexityApiKey: {\n    envKey: \"AGENT_PERPLEXITY_API_KEY\",\n    checks: [],\n  },\n\n  // TTS/STT Integration ENVS\n  TextToSpeechProvider: {\n    envKey: \"TTS_PROVIDER\",\n    checks: [supportedTTSProvider],\n  },\n\n  // TTS OpenAI\n  TTSOpenAIKey: {\n    envKey: \"TTS_OPEN_AI_KEY\",\n    checks: [validOpenAIKey],\n  },\n  TTSOpenAIVoiceModel: {\n    envKey: \"TTS_OPEN_AI_VOICE_MODEL\",\n    checks: [],\n  },\n\n  // TTS ElevenLabs\n  TTSElevenLabsKey: {\n    envKey: \"TTS_ELEVEN_LABS_KEY\",\n    checks: [isNotEmpty],\n  },\n  TTSElevenLabsVoiceModel: {\n    envKey: \"TTS_ELEVEN_LABS_VOICE_MODEL\",\n    checks: [],\n  },\n\n  // PiperTTS Local\n  TTSPiperTTSVoiceModel: {\n    envKey: \"TTS_PIPER_VOICE_MODEL\",\n    checks: [],\n  },\n\n  // OpenAI Generic TTS\n  TTSOpenAICompatibleKey: {\n    envKey: \"TTS_OPEN_AI_COMPATIBLE_KEY\",\n    checks: [],\n  },\n  TTSOpenAICompatibleModel: {\n    envKey: \"TTS_OPEN_AI_COMPATIBLE_MODEL\",\n    checks: [],\n  },\n  TTSOpenAICompatibleVoiceModel: {\n    envKey: \"TTS_OPEN_AI_COMPATIBLE_VOICE_MODEL\",\n    checks: [isNotEmpty],\n  },\n  TTSOpenAICompatibleEndpoint: {\n    envKey: \"TTS_OPEN_AI_COMPATIBLE_ENDPOINT\",\n    checks: [isValidURL],\n  },\n\n  // DeepSeek Options\n  DeepSeekApiKey: {\n    envKey: \"DEEPSEEK_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  DeepSeekModelPref: {\n    envKey: \"DEEPSEEK_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // APIPie Options\n  ApipieLLMApiKey: {\n    envKey: \"APIPIE_LLM_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  ApipieLLMModelPref: {\n    envKey: \"APIPIE_LLM_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // xAI Options\n  XAIApiKey: {\n    envKey: \"XAI_LLM_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  XAIModelPref: {\n    envKey: \"XAI_LLM_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // Nvidia NIM Options\n  NvidiaNimLLMBasePath: {\n    envKey: \"NVIDIA_NIM_LLM_BASE_PATH\",\n    checks: [isValidURL],\n    postUpdate: [\n      (_, __, nextValue) => {\n        const { parseNvidiaNimBasePath } = require(\"../AiProviders/nvidiaNim\");\n        process.env.NVIDIA_NIM_LLM_BASE_PATH =\n          parseNvidiaNimBasePath(nextValue);\n      },\n    ],\n  },\n  NvidiaNimLLMModelPref: {\n    envKey: \"NVIDIA_NIM_LLM_MODEL_PREF\",\n    checks: [],\n    postUpdate: [\n      async (_, __, nextValue) => {\n        const { NvidiaNimLLM } = require(\"../AiProviders/nvidiaNim\");\n        await NvidiaNimLLM.setModelTokenLimit(nextValue);\n      },\n    ],\n  },\n\n  // PPIO Options\n  PPIOApiKey: {\n    envKey: \"PPIO_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  PPIOModelPref: {\n    envKey: \"PPIO_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // Moonshot AI Options\n  MoonshotAiApiKey: {\n    envKey: \"MOONSHOT_AI_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  MoonshotAiModelPref: {\n    envKey: \"MOONSHOT_AI_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // Foundry Options\n  FoundryBasePath: {\n    envKey: \"FOUNDRY_BASE_PATH\",\n    checks: [isNotEmpty],\n  },\n  FoundryModelPref: {\n    envKey: \"FOUNDRY_MODEL_PREF\",\n    checks: [isNotEmpty],\n    postUpdate: [\n      // On new model selection, re-cache the context windows\n      async (_, prevValue, __) => {\n        const { FoundryLLM } = require(\"../AiProviders/foundry\");\n        await FoundryLLM.unloadModelFromEngine(prevValue);\n        await FoundryLLM.cacheContextWindows(true);\n      },\n    ],\n  },\n  FoundryModelTokenLimit: {\n    envKey: \"FOUNDRY_MODEL_TOKEN_LIMIT\",\n    checks: [],\n  },\n\n  // CometAPI Options\n  CometApiLLMApiKey: {\n    envKey: \"COMETAPI_LLM_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  CometApiLLMModelPref: {\n    envKey: \"COMETAPI_LLM_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  CometApiLLMTimeout: {\n    envKey: \"COMETAPI_LLM_TIMEOUT_MS\",\n    checks: [],\n  },\n\n  // Z.AI Options\n  ZAiApiKey: {\n    envKey: \"ZAI_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  ZAiModelPref: {\n    envKey: \"ZAI_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // GiteeAI Options\n  GiteeAIApiKey: {\n    envKey: \"GITEE_AI_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  GiteeAIModelPref: {\n    envKey: \"GITEE_AI_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  GiteeAITokenLimit: {\n    envKey: \"GITEE_AI_MODEL_TOKEN_LIMIT\",\n    checks: [nonZero],\n  },\n\n  // Docker Model Runner Options\n  DockerModelRunnerBasePath: {\n    envKey: \"DOCKER_MODEL_RUNNER_BASE_PATH\",\n    checks: [isValidURL],\n  },\n  DockerModelRunnerModelPref: {\n    envKey: \"DOCKER_MODEL_RUNNER_LLM_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  DockerModelRunnerModelTokenLimit: {\n    envKey: \"DOCKER_MODEL_RUNNER_LLM_MODEL_TOKEN_LIMIT\",\n    checks: [nonZero],\n  },\n\n  // Privatemode Options\n  PrivateModeBasePath: {\n    envKey: \"PRIVATEMODE_LLM_BASE_PATH\",\n    checks: [isValidURL],\n  },\n  PrivateModeModelPref: {\n    envKey: \"PRIVATEMODE_LLM_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // SambaNova Options\n  SambaNovaLLMApiKey: {\n    envKey: \"SAMBANOVA_LLM_API_KEY\",\n    checks: [isNotEmpty],\n  },\n  SambaNovaLLMModelPref: {\n    envKey: \"SAMBANOVA_LLM_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n\n  // Lemonade Options\n  LemonadeLLMBasePath: {\n    envKey: \"LEMONADE_LLM_BASE_PATH\",\n    checks: [isValidURL],\n  },\n  LemonadeLLMModelPref: {\n    envKey: \"LEMONADE_LLM_MODEL_PREF\",\n    checks: [isNotEmpty],\n  },\n  LemonadeLLMModelTokenLimit: {\n    envKey: \"LEMONADE_LLM_MODEL_TOKEN_LIMIT\",\n    checks: [nonZero],\n  },\n\n  // Agent Skill Settings\n  AgentSkillMaxToolCalls: {\n    envKey: \"AGENT_MAX_TOOL_CALLS\",\n    checks: [nonZero],\n  },\n  AgentSkillRerankerEnabled: {\n    envKey: \"AGENT_SKILL_RERANKER_ENABLED\",\n    checks: [],\n  },\n  AgentSkillRerankerTopN: {\n    envKey: \"AGENT_SKILL_RERANKER_TOP_N\",\n    checks: [nonZero],\n  },\n};\n\nfunction isNotEmpty(input = \"\") {\n  return !input || input.length === 0 ? \"Value cannot be empty\" : null;\n}\n\nfunction nonZero(input = \"\") {\n  if (isNaN(Number(input))) return \"Value must be a number\";\n  return Number(input) <= 0 ? \"Value must be greater than zero\" : null;\n}\n\nfunction isInteger(input = \"\") {\n  if (isNaN(Number(input))) return \"Value must be a number\";\n  return Number(input);\n}\n\nfunction isValidURL(input = \"\") {\n  try {\n    new URL(input);\n    return null;\n  } catch {\n    return \"URL is not a valid URL.\";\n  }\n}\n\nfunction validOpenAIKey(input = \"\") {\n  return input.startsWith(\"sk-\") ? null : \"OpenAI Key must start with sk-\";\n}\n\nfunction validAnthropicApiKey(input = \"\") {\n  return input.startsWith(\"sk-ant-\")\n    ? null\n    : \"Anthropic Key must start with sk-ant-\";\n}\n\nfunction validLLMExternalBasePath(input = \"\") {\n  try {\n    new URL(input);\n    if (!input.includes(\"v1\")) return \"URL must include /v1\";\n    if (input.split(\"\").slice(-1)?.[0] === \"/\")\n      return \"URL cannot end with a slash\";\n    return null;\n  } catch {\n    return \"Not a valid URL\";\n  }\n}\n\nfunction validOllamaLLMBasePath(input = \"\") {\n  try {\n    new URL(input);\n    if (input.split(\"\").slice(-1)?.[0] === \"/\")\n      return \"URL cannot end with a slash\";\n    return null;\n  } catch {\n    return \"Not a valid URL\";\n  }\n}\n\nfunction supportedTTSProvider(input = \"\") {\n  const validSelection = [\n    \"native\",\n    \"openai\",\n    \"elevenlabs\",\n    \"piper_local\",\n    \"generic-openai\",\n  ].includes(input);\n  return validSelection ? null : `${input} is not a valid TTS provider.`;\n}\n\nfunction validLocalWhisper(input = \"\") {\n  const validSelection = [\n    \"Xenova/whisper-small\",\n    \"Xenova/whisper-large\",\n  ].includes(input);\n  return validSelection\n    ? null\n    : `${input} is not a valid Whisper model selection.`;\n}\n\nfunction supportedLLM(input = \"\") {\n  const validSelection = [\n    \"openai\",\n    \"azure\",\n    \"anthropic\",\n    \"gemini\",\n    \"lmstudio\",\n    \"localai\",\n    \"ollama\",\n    \"togetherai\",\n    \"fireworksai\",\n    \"mistral\",\n    \"huggingface\",\n    \"perplexity\",\n    \"openrouter\",\n    \"novita\",\n    \"groq\",\n    \"koboldcpp\",\n    \"textgenwebui\",\n    \"cohere\",\n    \"litellm\",\n    \"generic-openai\",\n    \"bedrock\",\n    \"deepseek\",\n    \"apipie\",\n    \"xai\",\n    \"nvidia-nim\",\n    \"ppio\",\n    \"dpais\",\n    \"moonshotai\",\n    \"cometapi\",\n    \"foundry\",\n    \"zai\",\n    \"giteeai\",\n    \"docker-model-runner\",\n    \"privatemode\",\n    \"sambanova\",\n    \"lemonade\",\n  ].includes(input);\n  return validSelection ? null : `${input} is not a valid LLM provider.`;\n}\n\nfunction supportedTranscriptionProvider(input = \"\") {\n  const validSelection = [\"openai\", \"local\"].includes(input);\n  return validSelection\n    ? null\n    : `${input} is not a valid transcription model provider.`;\n}\n\nfunction validGeminiSafetySetting(input = \"\") {\n  const validModes = [\n    \"BLOCK_NONE\",\n    \"BLOCK_ONLY_HIGH\",\n    \"BLOCK_MEDIUM_AND_ABOVE\",\n    \"BLOCK_LOW_AND_ABOVE\",\n  ];\n  return validModes.includes(input)\n    ? null\n    : `Invalid Safety setting. Must be one of ${validModes.join(\", \")}.`;\n}\n\nfunction supportedEmbeddingModel(input = \"\") {\n  const supported = [\n    \"openai\",\n    \"azure\",\n    \"gemini\",\n    \"localai\",\n    \"native\",\n    \"ollama\",\n    \"lmstudio\",\n    \"cohere\",\n    \"voyageai\",\n    \"litellm\",\n    \"generic-openai\",\n    \"mistral\",\n    \"openrouter\",\n    \"lemonade\",\n  ];\n  return supported.includes(input)\n    ? null\n    : `Invalid Embedding model type. Must be one of ${supported.join(\", \")}.`;\n}\n\nfunction supportedVectorDB(input = \"\") {\n  const supported = [\n    \"chroma\",\n    \"chromacloud\",\n    \"pinecone\",\n    \"lancedb\",\n    \"weaviate\",\n    \"qdrant\",\n    \"milvus\",\n    \"zilliz\",\n    \"astra\",\n    \"pgvector\",\n  ];\n  return supported.includes(input)\n    ? null\n    : `Invalid VectorDB type. Must be one of ${supported.join(\", \")}.`;\n}\n\nfunction validChromaURL(input = \"\") {\n  return input.slice(-1) === \"/\"\n    ? `Chroma Instance URL should not end in a trailing slash.`\n    : null;\n}\n\nfunction validOpenAiTokenLimit(input = \"\") {\n  const tokenLimit = Number(input);\n  if (isNaN(tokenLimit)) return \"Token limit is not a number\";\n  return null;\n}\n\nfunction requiresForceMode(_, forceModeEnabled = false) {\n  return forceModeEnabled === true ? null : \"Cannot set this setting.\";\n}\n\nasync function validDockerizedUrl(input = \"\") {\n  if (process.env.ANYTHING_LLM_RUNTIME !== \"docker\") return null;\n\n  try {\n    const { isPortInUse, getLocalHosts } = require(\"./portAvailabilityChecker\");\n    const localInterfaces = getLocalHosts();\n    const url = new URL(input);\n    const hostname = url.hostname.toLowerCase();\n    const port = parseInt(url.port, 10);\n\n    // If not a loopback, skip this check.\n    if (!localInterfaces.includes(hostname)) return null;\n    if (isNaN(port)) return \"Invalid URL: Port is not specified or invalid\";\n\n    const isPortAvailableFromDocker = await isPortInUse(port, hostname);\n    if (isPortAvailableFromDocker)\n      return \"Port is not running a reachable service on loopback address from inside the AnythingLLM container. Please use host.docker.internal (for linux use 172.17.0.1), a real machine ip, or domain to connect to your service.\";\n  } catch (error) {\n    console.error(error.message);\n    return \"An error occurred while validating the URL\";\n  }\n\n  return null;\n}\n\nfunction validHuggingFaceEndpoint(input = \"\") {\n  return input.slice(-6) !== \".cloud\"\n    ? `Your HF Endpoint should end in \".cloud\"`\n    : null;\n}\n\nfunction noRestrictedChars(input = \"\") {\n  const regExp = new RegExp(/^[a-zA-Z0-9_\\-!@$%^&*();]+$/);\n  return !regExp.test(input)\n    ? `Your password has restricted characters in it. Allowed symbols are _,-,!,@,$,%,^,&,*,(,),;`\n    : null;\n}\n\nasync function handleVectorStoreReset(key, prevValue, nextValue) {\n  if (prevValue === nextValue) return;\n  if (key === \"VectorDB\") {\n    console.log(\n      `Vector configuration changed from ${prevValue} to ${nextValue} - resetting ${prevValue} namespaces`\n    );\n    return await resetAllVectorStores({ vectorDbKey: prevValue });\n  }\n\n  if (key === \"EmbeddingEngine\" || key === \"EmbeddingModelPref\") {\n    console.log(\n      `${key} changed from ${prevValue} to ${nextValue} - resetting ${process.env.VECTOR_DB} namespaces`\n    );\n    return await resetAllVectorStores({ vectorDbKey: process.env.VECTOR_DB });\n  }\n  return false;\n}\n\n/**\n * Downloads the embedding model in background if the user has selected a different model\n * - Only supported for the native embedder\n * - Must have the native embedder selected prior (otherwise will download on embed)\n */\nasync function downloadEmbeddingModelIfRequired(key, prevValue, nextValue) {\n  if (prevValue === nextValue) return;\n  if (key !== \"EmbeddingModelPref\" || process.env.EMBEDDING_ENGINE !== \"native\")\n    return;\n\n  const { NativeEmbedder } = require(\"../EmbeddingEngines/native\");\n  if (!NativeEmbedder.supportedModels[nextValue]) return; // if the model is not supported, don't download it\n  new NativeEmbedder().embedderClient();\n  return false;\n}\n\n/**\n * Validates the Postgres connection string for the PGVector options.\n * @param {string} input - The Postgres connection string to validate.\n * @returns {string} - An error message if the connection string is invalid, otherwise null.\n */\nasync function looksLikePostgresConnectionString(connectionString = null) {\n  if (!connectionString || !connectionString.startsWith(\"postgresql://\"))\n    return \"Invalid Postgres connection string. Must start with postgresql://\";\n  if (connectionString.includes(\" \"))\n    return \"Invalid Postgres connection string. Must not contain spaces.\";\n  return null;\n}\n\n/**\n * Validates the Postgres connection string for the PGVector options.\n * @param {string} key - The ENV key we are validating.\n * @param {string} prevValue - The previous value of the key.\n * @param {string} nextValue - The next value of the key.\n * @returns {string} - An error message if the connection string is invalid, otherwise null.\n */\nasync function validatePGVectorConnectionString(key, prevValue, nextValue) {\n  const envKey = KEY_MAPPING[key].envKey;\n\n  if (prevValue === nextValue) return; // If the value is the same as the previous value, don't validate it.\n  if (!nextValue) return; // If the value is not set, don't validate it.\n  if (nextValue === process.env[envKey]) return; // If the value is the same as the current connection string, don't validate it.\n\n  const { PGVector } = require(\"../vectorDbProviders/pgvector\");\n  const { error, success } = await PGVector.validateConnection({\n    connectionString: nextValue,\n  });\n  if (!success) return error;\n\n  // Set the ENV variable for the PGVector connection string early so we can use it in the table check.\n  process.env[envKey] = nextValue;\n  return null;\n}\n\n/**\n * Validates the Postgres table name for the PGVector options.\n * - Table should not already exist in the database.\n * @param {string} key - The ENV key we are validating.\n * @param {string} prevValue - The previous value of the key.\n * @param {string} nextValue - The next value of the key.\n * @returns {string} - An error message if the table name is invalid, otherwise null.\n */\nasync function validatePGVectorTableName(key, prevValue, nextValue) {\n  const envKey = KEY_MAPPING[key].envKey;\n\n  if (prevValue === nextValue) return; // If the value is the same as the previous value, don't validate it.\n  if (!nextValue) return; // If the value is not set, don't validate it.\n  if (nextValue === process.env[envKey]) return; // If the value is the same as the current table name, don't validate it.\n  if (!process.env.PGVECTOR_CONNECTION_STRING) return; // if connection string is not set, don't validate it since it will fail.\n\n  const { PGVector } = require(\"../vectorDbProviders/pgvector\");\n  const { error, success } = await PGVector.validateConnection({\n    connectionString: process.env.PGVECTOR_CONNECTION_STRING,\n    tableName: nextValue,\n  });\n  if (!success) return error;\n\n  return null;\n}\n\n// This will force update .env variables which for any which reason were not able to be parsed or\n// read from an ENV file as this seems to be a complicating step for many so allowing people to write\n// to the process will at least alleviate that issue. It does not perform comprehensive validity checks or sanity checks\n// and is simply for debugging when the .env not found issue many come across.\nasync function updateENV(newENVs = {}, force = false, userId = null) {\n  let error = \"\";\n  const runAfterAll = [];\n  const validKeys = Object.keys(KEY_MAPPING);\n  const ENV_KEYS = Object.keys(newENVs).filter(\n    (key) => validKeys.includes(key) && !newENVs[key].includes(\"******\") // strip out answers where the value is all asterisks\n  );\n  const newValues = {};\n\n  for (const key of ENV_KEYS) {\n    const {\n      envKey,\n      checks,\n      preUpdate = [], // Functions to run before updating a specific ENV variable\n      postUpdate = [], // Functions to run after updating a specific ENV variable\n      postSettled = [], // Functions to run after all ENV variables have been updated\n    } = KEY_MAPPING[key];\n    runAfterAll.push(...postSettled);\n    const prevValue = process.env[envKey];\n    const nextValue = newENVs[key];\n    let errors = await executeValidationChecks(checks, nextValue, force);\n\n    // If there are any errors from regular simple validation checks\n    // exit early.\n    if (errors.length > 0) {\n      error += errors.join(\"\\n\");\n      break;\n    }\n\n    // Accumulate errors from preUpdate functions\n    errors = [];\n    for (const preUpdateFunc of preUpdate) {\n      const errorMsg = await preUpdateFunc(key, prevValue, nextValue);\n      if (!!errorMsg && typeof errorMsg === \"string\") errors.push(errorMsg);\n    }\n\n    // If there are any errors from preUpdate functions\n    // exit early.\n    if (errors.length > 0) {\n      error += errors.join(\"\\n\");\n      break;\n    }\n\n    newValues[key] = nextValue;\n    process.env[envKey] = nextValue;\n\n    for (const postUpdateFunc of postUpdate)\n      await postUpdateFunc(key, prevValue, nextValue);\n  }\n\n  for (const runAfterAllFunc of runAfterAll)\n    await runAfterAllFunc(newValues, userId);\n\n  await logChangesToEventLog(newValues, userId);\n  if (process.env.NODE_ENV === \"production\") dumpENV();\n  return { newValues, error: error?.length > 0 ? error : false };\n}\n\nasync function executeValidationChecks(checks, value, force) {\n  const results = await Promise.all(\n    checks.map((validator) => validator(value, force))\n  );\n  return results.filter((err) => typeof err === \"string\");\n}\n\nasync function logChangesToEventLog(newValues = {}, userId = null) {\n  const { EventLogs } = require(\"../../models/eventLogs\");\n  const eventMapping = {\n    LLMProvider: \"update_llm_provider\",\n    EmbeddingEngine: \"update_embedding_engine\",\n    VectorDB: \"update_vector_db\",\n  };\n\n  for (const [key, eventName] of Object.entries(eventMapping)) {\n    if (!newValues.hasOwnProperty(key)) continue;\n    await EventLogs.logEvent(eventName, {}, userId);\n  }\n  return;\n}\n\nfunction dumpENV() {\n  const fs = require(\"fs\");\n  const path = require(\"path\");\n\n  const frozenEnvs = {};\n  const protectedKeys = [\n    ...Object.values(KEY_MAPPING).map((values) => values.envKey),\n    // Manually Add Keys here which are not already defined in KEY_MAPPING\n    // and are either managed or manually set ENV key:values.\n    \"JWT_EXPIRY\",\n\n    \"STORAGE_DIR\",\n    \"SERVER_PORT\",\n    // For persistent data encryption\n    \"SIG_KEY\",\n    \"SIG_SALT\",\n    // Password Schema Keys if present.\n    \"PASSWORDMINCHAR\",\n    \"PASSWORDMAXCHAR\",\n    \"PASSWORDLOWERCASE\",\n    \"PASSWORDUPPERCASE\",\n    \"PASSWORDNUMERIC\",\n    \"PASSWORDSYMBOL\",\n    \"PASSWORDREQUIREMENTS\",\n    // HTTPS SETUP KEYS\n    \"ENABLE_HTTPS\",\n    \"HTTPS_CERT_PATH\",\n    \"HTTPS_KEY_PATH\",\n    // Other Configuration Keys\n    \"DISABLE_VIEW_CHAT_HISTORY\",\n    // Simple SSO\n    \"SIMPLE_SSO_ENABLED\",\n    \"SIMPLE_SSO_NO_LOGIN\",\n    \"SIMPLE_SSO_NO_LOGIN_REDIRECT\",\n    // Community Hub\n    \"COMMUNITY_HUB_BUNDLE_DOWNLOADS_ENABLED\",\n\n    // Nvidia NIM Keys that are automatically managed\n    \"NVIDIA_NIM_LLM_MODEL_TOKEN_LIMIT\",\n\n    // OCR Language Support\n    \"TARGET_OCR_LANG\",\n\n    // Collector API common ENV - allows bypassing URL validation checks\n    \"COLLECTOR_ALLOW_ANY_IP\",\n\n    // Allow disabling of streaming for generic openai\n    \"GENERIC_OPENAI_STREAMING_DISABLED\",\n    // Custom headers for Generic OpenAI\n    \"GENERIC_OPEN_AI_CUSTOM_HEADERS\",\n\n    // Specify Chromium args for collector\n    \"ANYTHINGLLM_CHROMIUM_ARGS\",\n\n    // Allow setting a custom response timeout for Ollama\n    \"OLLAMA_RESPONSE_TIMEOUT\",\n\n    // Allow disabling of MCP tool cooldown\n    \"MCP_NO_COOLDOWN\",\n\n    // Allow disabling of streaming for AWS Bedrock\n    \"AWS_BEDROCK_STREAMING_DISABLED\",\n\n    // Allow native tool calling for specific providers.\n    \"PROVIDER_SUPPORTS_NATIVE_TOOL_CALLING\",\n  ];\n\n  // Simple sanitization of each value to prevent ENV injection via newline or quote escaping.\n  function sanitizeValue(value) {\n    const offendingChars =\n      /[\\n\\r\\t\\v\\f\\u0085\\u00a0\\u1680\\u180e\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\"'`#]/;\n    const firstOffendingCharIndex = value.search(offendingChars);\n    if (firstOffendingCharIndex === -1) return value;\n\n    return value.substring(0, firstOffendingCharIndex);\n  }\n\n  for (const key of protectedKeys) {\n    const envValue = process.env?.[key] || null;\n    if (!envValue) continue;\n    frozenEnvs[key] = process.env?.[key] || null;\n  }\n\n  var envResult = `# Auto-dump ENV from system call on ${new Date().toTimeString()}\\n`;\n  envResult += Object.entries(frozenEnvs)\n    .map(([key, value]) => `${key}='${sanitizeValue(value)}'`)\n    .join(\"\\n\");\n\n  const envPath = path.join(__dirname, \"../../.env\");\n  fs.writeFileSync(envPath, envResult, { encoding: \"utf8\", flag: \"w\" });\n  return true;\n}\n\nmodule.exports = {\n  dumpENV,\n  updateENV,\n};\n"
  },
  {
    "path": "server/utils/http/index.js",
    "content": "process.env.NODE_ENV === \"development\"\n  ? require(\"dotenv\").config({ path: `.env.${process.env.NODE_ENV}` })\n  : require(\"dotenv\").config();\nconst JWT = require(\"jsonwebtoken\");\nconst { User } = require(\"../../models/user\");\nconst { jsonrepair } = require(\"jsonrepair\");\nconst extract = require(\"extract-json-from-string\");\n\nfunction reqBody(request) {\n  return typeof request.body === \"string\"\n    ? JSON.parse(request.body)\n    : request.body;\n}\n\nfunction queryParams(request) {\n  return request.query;\n}\n\n/**\n * Creates a JWT with the given info and expiry\n * @param {object} info - The info to include in the JWT\n * @param {string} expiry - The expiry time for the JWT (default: 30 days)\n * @returns {string} The JWT\n */\nfunction makeJWT(info = {}, expiry = \"30d\") {\n  if (!process.env.JWT_SECRET)\n    throw new Error(\"Cannot create JWT as JWT_SECRET is unset.\");\n  return JWT.sign(info, process.env.JWT_SECRET, { expiresIn: expiry });\n}\n\n/**\n * Gets the user from the session\n * Note: Only valid for multi-user mode\n * as single-user mode with password is not a \"user\"\n * @param {import(\"express\").Request} request - The request object\n * @param {import(\"express\").Response} response - The response object\n * @returns {Promise<import(\"@prisma/client\").users | null>} The user\n */\nasync function userFromSession(request, response = null) {\n  if (!!response && !!response.locals?.user) {\n    return response.locals.user;\n  }\n\n  const auth = request.header(\"Authorization\");\n  const token = auth ? auth.split(\" \")[1] : null;\n\n  if (!token) {\n    return null;\n  }\n\n  const valid = decodeJWT(token);\n  if (!valid || !valid.id) {\n    return null;\n  }\n\n  const user = await User.get({ id: valid.id });\n  return user;\n}\n\nfunction decodeJWT(jwtToken) {\n  try {\n    return JWT.verify(jwtToken, process.env.JWT_SECRET);\n  } catch {}\n  return { p: null, id: null, username: null };\n}\n\nfunction multiUserMode(response) {\n  return response?.locals?.multiUserMode;\n}\n\nfunction parseAuthHeader(headerValue = null, apiKey = null) {\n  if (headerValue === null || apiKey === null) return {};\n  if (headerValue === \"Authorization\")\n    return { Authorization: `Bearer ${apiKey}` };\n  return { [headerValue]: apiKey };\n}\n\nfunction safeJsonParse(jsonString, fallback = null) {\n  if (jsonString === null) return fallback;\n\n  try {\n    return JSON.parse(jsonString);\n  } catch {}\n\n  if (jsonString?.startsWith(\"[\") || jsonString?.startsWith(\"{\")) {\n    try {\n      const repairedJson = jsonrepair(jsonString);\n      return JSON.parse(repairedJson);\n    } catch {}\n  }\n\n  try {\n    return extract(jsonString)?.[0] || fallback;\n  } catch {}\n\n  return fallback;\n}\n\nfunction isValidUrl(urlString = \"\") {\n  try {\n    const url = new URL(urlString);\n    if (![\"http:\", \"https:\"].includes(url.protocol)) return false;\n    return true;\n  } catch {}\n  return false;\n}\n\nfunction toValidNumber(number = null, fallback = null) {\n  if (isNaN(Number(number))) return fallback;\n  return Number(number);\n}\n\n/**\n * Decode HTML entities from a string.\n * The DMR response is encoded with HTML entities, so we need to decode them\n * so we can parse the JSON and report the progress percentage.\n * @param {string} str - The string to decode.\n * @returns {string} The decoded string.\n */\nfunction decodeHtmlEntities(str) {\n  return str\n    .replace(/&#34;/g, '\"')\n    .replace(/&#39;/g, \"'\")\n    .replace(/&lt;/g, \"<\")\n    .replace(/&gt;/g, \">\")\n    .replace(/&amp;/g, \"&\");\n}\n\nmodule.exports = {\n  reqBody,\n  multiUserMode,\n  queryParams,\n  makeJWT,\n  decodeJWT,\n  userFromSession,\n  parseAuthHeader,\n  safeJsonParse,\n  isValidUrl,\n  toValidNumber,\n  decodeHtmlEntities,\n};\n"
  },
  {
    "path": "server/utils/logger/index.js",
    "content": "const winston = require(\"winston\");\n\nclass Logger {\n  logger = console;\n  static _instance;\n  constructor() {\n    if (Logger._instance) return Logger._instance;\n    this.logger =\n      process.env.NODE_ENV === \"production\" ? this.getWinstonLogger() : console;\n    Logger._instance = this;\n  }\n\n  getWinstonLogger() {\n    const logger = winston.createLogger({\n      level: \"info\",\n      defaultMeta: { service: \"backend\" },\n      transports: [\n        new winston.transports.Console({\n          format: winston.format.combine(\n            winston.format.colorize(),\n            winston.format.printf(\n              ({ level, message, service, origin = \"\" }) => {\n                return `\\x1b[36m[${service}]\\x1b[0m${origin ? `\\x1b[33m[${origin}]\\x1b[0m` : \"\"} ${level}: ${message}`;\n              }\n            )\n          ),\n        }),\n      ],\n    });\n\n    function formatArgs(args) {\n      return args\n        .map((arg) => {\n          if (arg instanceof Error) {\n            return arg.stack; // If argument is an Error object, return its stack trace\n          } else if (typeof arg === \"object\") {\n            return JSON.stringify(arg); // Convert objects to JSON string\n          } else {\n            return arg; // Otherwise, return as-is\n          }\n        })\n        .join(\" \");\n    }\n\n    console.log = function (...args) {\n      logger.info(formatArgs(args));\n    };\n    console.error = function (...args) {\n      logger.error(formatArgs(args));\n    };\n    console.info = function (...args) {\n      logger.warn(formatArgs(args));\n    };\n    return logger;\n  }\n}\n\n/**\n * Sets and overrides Console methods for logging when called.\n * This is a singleton method and will not create multiple loggers.\n * @returns {winston.Logger | console} - instantiated logger interface.\n */\nfunction setLogger() {\n  return new Logger().logger;\n}\nmodule.exports = setLogger;\n"
  },
  {
    "path": "server/utils/middleware/chatHistoryViewable.js",
    "content": "/**\n * A simple middleware that validates that the chat history is viewable.\n * via the `DISABLE_VIEW_CHAT_HISTORY` environment variable being set AT ALL.\n * @param {Request} request - The request object.\n * @param {Response} response - The response object.\n * @param {NextFunction} next - The next function.\n */\nfunction chatHistoryViewable(_request, response, next) {\n  if (\"DISABLE_VIEW_CHAT_HISTORY\" in process.env)\n    return response\n      .status(422)\n      .send(\"This feature has been disabled by the administrator.\");\n  next();\n}\n\nmodule.exports = {\n  chatHistoryViewable,\n};\n"
  },
  {
    "path": "server/utils/middleware/communityHubDownloadsEnabled.js",
    "content": "const { CommunityHub } = require(\"../../models/communityHub\");\nconst { reqBody } = require(\"../http\");\n\n/**\n * ### Must be called after `communityHubItem`\n * Checks if community hub bundle downloads are enabled. The reason this functionality is disabled\n * by default is that since AgentSkills, Workspaces, and DataConnectors are all imported from the\n * community hub via unzipping a bundle - it would be possible for a malicious user to craft and\n * download a malicious bundle and import it into their own hosted instance. To avoid this, this\n * functionality is disabled by default and must be enabled manually by the system administrator.\n *\n * On hosted systems, this would not be an issue since the user cannot modify this setting, but those\n * who self-host can still unlock this feature manually by setting the environment variable\n * which would require someone who likely has the capacity to understand the risks and the\n * implications of importing unverified items that can run code on their system, container, or instance.\n * @see {@link https://docs.anythingllm.com/docs/community-hub/import}\n * @param {import(\"express\").Request} request\n * @param {import(\"express\").Response} response\n * @param {import(\"express\").NextFunction} next\n * @returns {void}\n */\nfunction communityHubDownloadsEnabled(request, response, next) {\n  if (!(\"COMMUNITY_HUB_BUNDLE_DOWNLOADS_ENABLED\" in process.env)) {\n    return response.status(422).json({\n      error:\n        \"Community Hub bundle downloads are not enabled. The system administrator must enable this feature manually to allow this instance to download these types of items. See https://docs.anythingllm.com/configuration#anythingllm-hub-agent-skills\",\n    });\n  }\n\n  // If the admin specifically did not set the system to `allow_all` then downloads are limited to verified items or private items only.\n  // This is to prevent users from downloading unverified items and importing them into their own instance without understanding the risks.\n  const item = response.locals.bundleItem;\n  if (\n    !item.verified &&\n    item.visibility !== \"private\" &&\n    process.env.COMMUNITY_HUB_BUNDLE_DOWNLOADS_ENABLED !== \"allow_all\"\n  ) {\n    return response.status(422).json({\n      error:\n        \"Community hub bundle downloads are limited to verified public items or private team items only. Please contact the system administrator to review or modify this setting. See https://docs.anythingllm.com/configuration#anythingllm-hub-agent-skills\",\n    });\n  }\n  next();\n}\n\n/**\n * Fetch the bundle item from the community hub.\n * Sets `response.locals.bundleItem` and `response.locals.bundleUrl`.\n */\nasync function communityHubItem(request, response, next) {\n  const { importId } = reqBody(request);\n  if (!importId)\n    return response.status(500).json({\n      success: false,\n      error: \"Import ID is required\",\n    });\n\n  const {\n    url,\n    item,\n    error: fetchError,\n  } = await CommunityHub.getBundleItem(importId);\n  if (fetchError)\n    return response.status(500).json({\n      success: false,\n      error: fetchError,\n    });\n\n  response.locals.bundleItem = item;\n  response.locals.bundleUrl = url;\n  next();\n}\n\nmodule.exports = {\n  communityHubItem,\n  communityHubDownloadsEnabled,\n};\n"
  },
  {
    "path": "server/utils/middleware/embedMiddleware.js",
    "content": "const { v4: uuidv4, validate } = require(\"uuid\");\nconst { VALID_CHAT_MODE } = require(\"../chats/stream\");\nconst { EmbedChats } = require(\"../../models/embedChats\");\nconst { EmbedConfig } = require(\"../../models/embedConfig\");\nconst { reqBody } = require(\"../http\");\n\n// Finds or Aborts request for a /:embedId/ url. This should always\n// be the first middleware and the :embedID should be in the URL.\nasync function validEmbedConfig(request, response, next) {\n  const { embedId } = request.params;\n\n  const embed = await EmbedConfig.getWithWorkspace({ uuid: embedId });\n  if (!embed) {\n    response.sendStatus(404).end();\n    return;\n  }\n\n  response.locals.embedConfig = embed;\n  next();\n}\n\nfunction setConnectionMeta(request, response, next) {\n  response.locals.connection = {\n    host: request.headers?.origin,\n    ip: request?.ip,\n  };\n  next();\n}\n\nasync function validEmbedConfigId(request, response, next) {\n  const { embedId } = request.params;\n\n  const embed = await EmbedConfig.get({ id: Number(embedId) });\n  if (!embed) {\n    response.sendStatus(404).end();\n    return;\n  }\n\n  response.locals.embedConfig = embed;\n  next();\n}\n\nasync function canRespond(request, response, next) {\n  try {\n    const embed = response.locals.embedConfig;\n    if (!embed) {\n      response.sendStatus(404).end();\n      return;\n    }\n\n    // Block if disabled by admin.\n    if (!embed.enabled) {\n      response.status(503).json({\n        id: uuidv4(),\n        type: \"abort\",\n        textResponse: null,\n        sources: [],\n        close: true,\n        error:\n          \"This chat has been disabled by the administrator - try again later.\",\n      });\n      return;\n    }\n\n    // Check if requester hostname is in the valid allowlist of domains.\n    const host = request.headers.origin ?? \"\";\n    const allowedHosts = EmbedConfig.parseAllowedHosts(embed);\n    if (allowedHosts !== null && !allowedHosts.includes(host)) {\n      response.status(401).json({\n        id: uuidv4(),\n        type: \"abort\",\n        textResponse: null,\n        sources: [],\n        close: true,\n        error: \"Invalid request.\",\n      });\n      return;\n    }\n\n    const { sessionId, message } = reqBody(request);\n    if (typeof sessionId !== \"string\" || !validate(String(sessionId))) {\n      response.status(404).json({\n        id: uuidv4(),\n        type: \"abort\",\n        textResponse: null,\n        sources: [],\n        close: true,\n        error: \"Invalid session ID.\",\n      });\n      return;\n    }\n\n    if (!message?.length || !VALID_CHAT_MODE.includes(embed.chat_mode)) {\n      response.status(400).json({\n        id: uuidv4(),\n        type: \"abort\",\n        textResponse: null,\n        sources: [],\n        close: true,\n        error: !message?.length\n          ? \"Message is empty.\"\n          : `${embed.chat_mode} is not a valid mode.`,\n      });\n      return;\n    }\n\n    if (\n      !isNaN(embed.max_chats_per_day) &&\n      Number(embed.max_chats_per_day) > 0\n    ) {\n      const dailyChatCount = await EmbedChats.count({\n        embed_id: embed.id,\n        createdAt: {\n          gte: new Date(new Date() - 24 * 60 * 60 * 1000),\n        },\n      });\n\n      if (dailyChatCount >= Number(embed.max_chats_per_day)) {\n        response.status(429).json({\n          id: uuidv4(),\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error: \"Rate limit exceeded\",\n          errorMsg:\n            \"The quota for this chat has been reached. Try again later or contact the site owner.\",\n        });\n        return;\n      }\n    }\n\n    if (\n      !isNaN(embed.max_chats_per_session) &&\n      Number(embed.max_chats_per_session) > 0\n    ) {\n      const dailySessionCount = await EmbedChats.count({\n        embed_id: embed.id,\n        session_id: sessionId,\n        createdAt: {\n          gte: new Date(new Date() - 24 * 60 * 60 * 1000),\n        },\n      });\n\n      if (dailySessionCount >= Number(embed.max_chats_per_session)) {\n        response.status(429).json({\n          id: uuidv4(),\n          type: \"abort\",\n          textResponse: null,\n          sources: [],\n          close: true,\n          error:\n            \"Your quota for this chat has been reached. Try again later or contact the site owner.\",\n        });\n        return;\n      }\n    }\n\n    next();\n  } catch {\n    response.status(500).json({\n      id: uuidv4(),\n      type: \"abort\",\n      textResponse: null,\n      sources: [],\n      close: true,\n      error: \"Invalid request.\",\n    });\n    return;\n  }\n}\n\nmodule.exports = {\n  setConnectionMeta,\n  validEmbedConfig,\n  validEmbedConfigId,\n  canRespond,\n};\n"
  },
  {
    "path": "server/utils/middleware/featureFlagEnabled.js",
    "content": "const { SystemSettings } = require(\"../../models/systemSettings\");\n\n// Explicitly check that a specific feature flag is enabled.\n// This should match the key in the SystemSetting label.\nfunction featureFlagEnabled(featureFlagKey = null) {\n  return async (_, response, next) => {\n    if (!featureFlagKey) return response.sendStatus(401).end();\n\n    const flagValue = (\n      await SystemSettings.get({ label: String(featureFlagKey) })\n    )?.value;\n    if (!flagValue) return response.sendStatus(401).end();\n\n    if (flagValue === \"enabled\") {\n      next();\n      return;\n    }\n\n    return response.sendStatus(401).end();\n  };\n}\nmodule.exports = {\n  featureFlagEnabled,\n};\n"
  },
  {
    "path": "server/utils/middleware/isSupportedRepoProviders.js",
    "content": "// Middleware to validate that a repo provider URL is supported.\nconst REPO_PLATFORMS = [\"github\", \"gitlab\"];\n\nfunction isSupportedRepoProvider(request, response, next) {\n  const { repo_platform = null } = request.params;\n  if (!repo_platform || !REPO_PLATFORMS.includes(repo_platform))\n    return response\n      .status(500)\n      .text(`Unsupported repo platform ${repo_platform}`);\n  next();\n}\nmodule.exports = { isSupportedRepoProvider };\n"
  },
  {
    "path": "server/utils/middleware/multiUserProtected.js",
    "content": "const { SystemSettings } = require(\"../../models/systemSettings\");\nconst { userFromSession } = require(\"../http\");\nconst ROLES = {\n  all: \"<all>\",\n  admin: \"admin\",\n  manager: \"manager\",\n  default: \"default\",\n};\nconst DEFAULT_ROLES = [ROLES.admin, ROLES.admin];\n\n/**\n * Explicitly check that multi user mode is enabled as well as that the\n * requesting user has the appropriate role to modify or call the URL.\n * @param {string[]} allowedRoles - The roles that are allowed to access the route\n * @returns {function}\n */\nfunction strictMultiUserRoleValid(allowedRoles = DEFAULT_ROLES) {\n  return async (request, response, next) => {\n    // If the access-control is allowable for all - skip validations and continue;\n    if (allowedRoles.includes(ROLES.all)) {\n      next();\n      return;\n    }\n\n    const multiUserMode =\n      response.locals?.multiUserMode ??\n      (await SystemSettings.isMultiUserMode());\n    if (!multiUserMode) return response.sendStatus(401).end();\n\n    const user =\n      response.locals?.user ?? (await userFromSession(request, response));\n    if (allowedRoles.includes(user?.role)) {\n      next();\n      return;\n    }\n    return response.sendStatus(401).end();\n  };\n}\n\n/**\n * Apply role permission checks IF the current system is in multi-user mode.\n * This is relevant for routes that are shared between MUM and single-user mode.\n * @param {string[]} allowedRoles - The roles that are allowed to access the route\n * @returns {function}\n */\nfunction flexUserRoleValid(allowedRoles = DEFAULT_ROLES) {\n  return async (request, response, next) => {\n    // If the access-control is allowable for all - skip validations and continue;\n    // It does not matter if multi-user or not.\n    if (allowedRoles.includes(ROLES.all)) {\n      next();\n      return;\n    }\n\n    // Bypass if not in multi-user mode\n    const multiUserMode =\n      response.locals?.multiUserMode ??\n      (await SystemSettings.isMultiUserMode());\n    if (!multiUserMode) {\n      next();\n      return;\n    }\n\n    const user =\n      response.locals?.user ?? (await userFromSession(request, response));\n    if (allowedRoles.includes(user?.role)) {\n      next();\n      return;\n    }\n    return response.sendStatus(401).end();\n  };\n}\n\n// Middleware check on a public route if the instance is in a valid\n// multi-user set up.\nasync function isMultiUserSetup(_request, response, next) {\n  const multiUserMode = await SystemSettings.isMultiUserMode();\n  if (!multiUserMode) {\n    response.status(403).json({\n      error: \"Invalid request\",\n    });\n    return;\n  }\n\n  next();\n  return;\n}\n\nmodule.exports = {\n  ROLES,\n  strictMultiUserRoleValid,\n  flexUserRoleValid,\n  isMultiUserSetup,\n};\n"
  },
  {
    "path": "server/utils/middleware/simpleSSOEnabled.js",
    "content": "const { SystemSettings } = require(\"../../models/systemSettings\");\n\n/**\n * Checks if simple SSO is enabled for issuance of temporary auth tokens.\n * Note: This middleware must be called after `validApiKey`.\n * @param {import(\"express\").Request} request\n * @param {import(\"express\").Response} response\n * @param {import(\"express\").NextFunction} next\n * @returns {void}\n */\nasync function simpleSSOEnabled(_, response, next) {\n  if (!(\"SIMPLE_SSO_ENABLED\" in process.env)) {\n    return response\n      .status(403)\n      .send(\n        \"Simple SSO is not enabled. It must be enabled to validate or issue temporary auth tokens.\"\n      );\n  }\n\n  // If the multi-user mode response local is not set, we need to check if it's enabled.\n  if (!(\"multiUserMode\" in response.locals)) {\n    const multiUserMode = await SystemSettings.isMultiUserMode();\n    response.locals.multiUserMode = multiUserMode;\n  }\n\n  if (!response.locals.multiUserMode) {\n    return response\n      .status(403)\n      .send(\n        \"Multi-User mode is not enabled. It must be enabled to use Simple SSO.\"\n      );\n  }\n\n  next();\n}\n\n/**\n * Checks if simple SSO login is disabled by checking if the\n * SIMPLE_SSO_NO_LOGIN environment variable is set as well as\n * SIMPLE_SSO_ENABLED is set.\n *\n * This check should only be run when in multi-user mode when used.\n * @returns {boolean}\n */\nfunction simpleSSOLoginDisabled() {\n  return (\n    \"SIMPLE_SSO_ENABLED\" in process.env && \"SIMPLE_SSO_NO_LOGIN\" in process.env\n  );\n}\n\n/**\n * Middleware that checks if simple SSO login is disabled by checking if the\n * SIMPLE_SSO_NO_LOGIN environment variable is set as well as\n * SIMPLE_SSO_ENABLED is set.\n *\n * This middleware will 403 if SSO is enabled and no login is allowed and\n * the system is in multi-user mode. Otherwise, it will call next.\n *\n * @param {import(\"express\").Request} request\n * @param {import(\"express\").Response} response\n * @param {import(\"express\").NextFunction} next\n * @returns {void}\n */\nasync function simpleSSOLoginDisabledMiddleware(_, response, next) {\n  if (!(\"multiUserMode\" in response.locals)) {\n    const multiUserMode = await SystemSettings.isMultiUserMode();\n    response.locals.multiUserMode = multiUserMode;\n  }\n\n  if (response.locals.multiUserMode && simpleSSOLoginDisabled()) {\n    response.status(403).json({\n      success: false,\n      error: \"Login via credentials has been disabled by the administrator.\",\n    });\n    return;\n  }\n  next();\n}\n\nmodule.exports = {\n  simpleSSOEnabled,\n  simpleSSOLoginDisabled,\n  simpleSSOLoginDisabledMiddleware,\n};\n"
  },
  {
    "path": "server/utils/middleware/validApiKey.js",
    "content": "const { ApiKey } = require(\"../../models/apiKeys\");\nconst { SystemSettings } = require(\"../../models/systemSettings\");\n\nasync function validApiKey(request, response, next) {\n  const multiUserMode = await SystemSettings.isMultiUserMode();\n  response.locals.multiUserMode = multiUserMode;\n\n  const auth = request.header(\"Authorization\");\n  const bearerKey = auth ? auth.split(\" \")[1] : null;\n  if (!bearerKey) {\n    response.status(403).json({\n      error: \"No valid api key found.\",\n    });\n    return;\n  }\n\n  if (!(await ApiKey.get({ secret: bearerKey }))) {\n    response.status(403).json({\n      error: \"No valid api key found.\",\n    });\n    return;\n  }\n\n  next();\n}\n\nmodule.exports = {\n  validApiKey,\n};\n"
  },
  {
    "path": "server/utils/middleware/validBrowserExtensionApiKey.js",
    "content": "const {\n  BrowserExtensionApiKey,\n} = require(\"../../models/browserExtensionApiKey\");\nconst { SystemSettings } = require(\"../../models/systemSettings\");\nconst { User } = require(\"../../models/user\");\n\nasync function validBrowserExtensionApiKey(request, response, next) {\n  const multiUserMode = await SystemSettings.isMultiUserMode();\n  response.locals.multiUserMode = multiUserMode;\n\n  const auth = request.header(\"Authorization\");\n  const bearerKey = auth ? auth.split(\" \")[1] : null;\n  if (!bearerKey) {\n    response.status(403).json({\n      error: \"No valid API key found.\",\n    });\n    return;\n  }\n\n  const apiKey = await BrowserExtensionApiKey.validate(bearerKey);\n  if (!apiKey) {\n    response.status(403).json({\n      error: \"No valid API key found.\",\n    });\n    return;\n  }\n\n  if (multiUserMode) {\n    const user = await User.get({ id: apiKey.user_id });\n    if (!user) {\n      response.status(403).json({\n        error: \"User not found.\",\n      });\n      return;\n    }\n\n    if (user.suspended) {\n      response.status(401).json({\n        error: \"User is suspended from system\",\n      });\n      return;\n    }\n\n    response.locals.user = user;\n  }\n\n  response.locals.apiKey = apiKey;\n  next();\n}\n\nmodule.exports = { validBrowserExtensionApiKey };\n"
  },
  {
    "path": "server/utils/middleware/validWorkspace.js",
    "content": "const { Workspace } = require(\"../../models/workspace\");\nconst { WorkspaceThread } = require(\"../../models/workspaceThread\");\nconst { userFromSession, multiUserMode } = require(\"../http\");\n\n// Will pre-validate and set the workspace for a request if the slug is provided in the URL path.\nasync function validWorkspaceSlug(request, response, next) {\n  const { slug } = request.params;\n  const user = await userFromSession(request, response);\n  const workspace = multiUserMode(response)\n    ? await Workspace.getWithUser(user, { slug })\n    : await Workspace.get({ slug });\n\n  if (!workspace) {\n    response.status(404).send(\"Workspace does not exist.\");\n    return;\n  }\n\n  response.locals.workspace = workspace;\n  next();\n}\n\n// Will pre-validate and set the workspace AND a thread for a request if the slugs are provided in the URL path.\nasync function validWorkspaceAndThreadSlug(request, response, next) {\n  const { slug, threadSlug } = request.params;\n  const user = await userFromSession(request, response);\n  const workspace = multiUserMode(response)\n    ? await Workspace.getWithUser(user, { slug })\n    : await Workspace.get({ slug });\n\n  if (!workspace) {\n    response.status(404).send(\"Workspace does not exist.\");\n    return;\n  }\n\n  const thread = await WorkspaceThread.get({\n    slug: threadSlug,\n    user_id: user?.id || null,\n  });\n  if (!thread) {\n    response.status(404).send(\"Workspace thread does not exist.\");\n    return;\n  }\n\n  response.locals.workspace = workspace;\n  response.locals.thread = thread;\n  next();\n}\n\nmodule.exports = {\n  validWorkspaceSlug,\n  validWorkspaceAndThreadSlug,\n};\n"
  },
  {
    "path": "server/utils/middleware/validatedRequest.js",
    "content": "const { SystemSettings } = require(\"../../models/systemSettings\");\nconst { User } = require(\"../../models/user\");\nconst { EncryptionManager } = require(\"../EncryptionManager\");\nconst { decodeJWT } = require(\"../http\");\nconst EncryptionMgr = new EncryptionManager();\n\nasync function validatedRequest(request, response, next) {\n  const multiUserMode = await SystemSettings.isMultiUserMode();\n  response.locals.multiUserMode = multiUserMode;\n  if (multiUserMode)\n    return await validateMultiUserRequest(request, response, next);\n\n  // When in development passthrough auth token for ease of development.\n  // Or if the user simply did not set an Auth token or JWT Secret\n  if (\n    process.env.NODE_ENV === \"development\" ||\n    !process.env.AUTH_TOKEN ||\n    !process.env.JWT_SECRET\n  ) {\n    next();\n    return;\n  }\n\n  if (!process.env.AUTH_TOKEN) {\n    response.status(401).json({\n      error: \"You need to set an AUTH_TOKEN environment variable.\",\n    });\n    return;\n  }\n\n  const auth = request.header(\"Authorization\");\n  const token = auth ? auth.split(\" \")[1] : null;\n\n  if (!token) {\n    response.status(401).json({\n      error: \"No auth token found.\",\n    });\n    return;\n  }\n\n  const bcrypt = require(\"bcryptjs\");\n  const { p } = decodeJWT(token);\n\n  if (p === null || !/\\w{32}:\\w{32}/.test(p)) {\n    response.status(401).json({\n      error: \"Token expired or failed validation.\",\n    });\n    return;\n  }\n\n  // Since the blame of this comment we have been encrypting the `p` property of JWTs with the persistent\n  // encryptionManager PEM's. This prevents us from storing the `p` unencrypted in the JWT itself, which could\n  // be unsafe. As a consequence, existing JWTs with invalid `p` values that do not match the regex\n  // in ln:44 will be marked invalid so they can be logged out and forced to log back in and obtain an encrypted token.\n  // This kind of methodology only applies to single-user password mode.\n  if (\n    !bcrypt.compareSync(\n      EncryptionMgr.decrypt(p),\n      bcrypt.hashSync(process.env.AUTH_TOKEN, 10)\n    )\n  ) {\n    response.status(401).json({\n      error: \"Invalid auth credentials.\",\n    });\n    return;\n  }\n\n  next();\n}\n\nasync function validateMultiUserRequest(request, response, next) {\n  const auth = request.header(\"Authorization\");\n  const token = auth ? auth.split(\" \")[1] : null;\n\n  if (!token) {\n    response.status(401).json({\n      error: \"No auth token found.\",\n    });\n    return;\n  }\n\n  const valid = decodeJWT(token);\n  if (!valid || !valid.id) {\n    response.status(401).json({\n      error: \"Invalid auth token.\",\n    });\n    return;\n  }\n\n  const user = await User.get({ id: valid.id });\n  if (!user) {\n    response.status(401).json({\n      error: \"Invalid auth for user.\",\n    });\n    return;\n  }\n\n  if (user.suspended) {\n    response.status(401).json({\n      error: \"User is suspended from system\",\n    });\n    return;\n  }\n\n  response.locals.user = user;\n  next();\n}\n\nmodule.exports = {\n  validatedRequest,\n};\n"
  },
  {
    "path": "server/utils/prisma/PRISMA.md",
    "content": "# Prisma Setup and Usage Guide\n\nThis guide will help you set up and use Prisma for the project. Prisma is a powerful ORM for Node.js and TypeScript, helping developers build faster and make fewer errors. Follow the guide to understand how to use Prisma and the scripts available in the project to manage the Prisma setup.\n\n## Setting Up Prisma\n\nTo get started with setting up Prisma, you should run the setup script from the project root directory:\n\n```sh\nyarn setup\n```\n\nThis script will install the necessary node modules in both the server and frontend directories, set up the environment files, and set up Prisma (generate client, run migrations, and seed the database).\n\n## Prisma Scripts\n\nIn the project root's `package.json`, there are several scripts set up to help you manage Prisma:\n\n- **prisma:generate**: Generates the Prisma client.\n- **prisma:migrate**: Runs the migrations to ensure the database is in sync with the schema.\n- **prisma:seed**: Seeds the database with initial data.\n- **prisma:setup**: A convenience script that runs `prisma:generate`, `prisma:migrate`, and `prisma:seed` in sequence.\n- **sqlite:migrate**: (To be run from the `server` directory) This script is for users transitioning from the old SQLite custom ORM setup to Prisma and will migrate all existing data over to Prisma. If you're a new user, your setup will already use Prisma.\n\nTo run any of these scripts, use `yarn` followed by the script name from the project root directory. For example:\n\n```sh\nyarn prisma:setup\n```\n\n## Manual Prisma Commands\n\nWhile the scripts should cover most of your needs, you may sometimes want to run Prisma commands manually. Here are some commands you might find useful, along with their descriptions:\n\n- `npx prisma introspect`: Introspects the database to update the Prisma schema by reading the schema of the existing database.\n- `npx prisma generate`: Generates the Prisma client.\n- `npx prisma migrate dev --name init`: Ensures the database is in sync with the schema, naming the migration 'init'.\n- `npx prisma migrate reset`: Resets the database, deleting all data and recreating the schema.\n\nThese commands should be run from the `server` directory, where the Prisma schema is located.\n\n## Notes\n\n- Always make sure to run scripts from the root level to avoid path issues.\n- Before running migrations, ensure that the Prisma schema is correctly defined to prevent data loss or corruption.\n- If you are adding a new feature or making changes that require a change in the database schema, create a new migration rather than editing existing migrations.\n- For users transitioning from the old SQLite ORM, navigate to the `server` directory and run the `sqlite:migrate` script to smoothly transition to Prisma. If you're setting up the project fresh, this step is unnecessary as the setup will already be using Prisma.\n"
  },
  {
    "path": "server/utils/prisma/index.js",
    "content": "const { PrismaClient } = require(\"@prisma/client\");\n\n// npx prisma introspect\n// npx prisma generate\n// npx prisma migrate dev --name init -> ensures that db is in sync with schema\n// npx prisma migrate reset -> resets the db\n\nconst logLevels = [\"error\", \"info\", \"warn\"]; // add \"query\" to debug query logs\nconst prisma = new PrismaClient({\n  log: logLevels,\n});\n\nmodule.exports = prisma;\n"
  },
  {
    "path": "server/utils/telemetry/index.js",
    "content": "const { getGitVersion } = require(\"../../endpoints/utils\");\nconst { Telemetry } = require(\"../../models/telemetry\");\n\n// Telemetry is anonymized and your data is never read. This can be disabled by setting\n// DISABLE_TELEMETRY=true in the `.env` of however you setup. Telemetry helps us determine use\n// of how AnythingLLM is used and how to improve this product!\n// You can see all Telemetry events by ctrl+f `Telemetry.sendTelemetry` calls to verify this claim.\nasync function setupTelemetry() {\n  if (process.env.DISABLE_TELEMETRY === \"true\") {\n    console.log(\n      `\\x1b[31m[TELEMETRY DISABLED]\\x1b[0m Telemetry is marked as disabled - no events will send. Telemetry helps Mintplex Labs Inc improve AnythingLLM.`\n    );\n    return true;\n  }\n\n  if (Telemetry.isDev()) {\n    console.log(\n      `\\x1b[33m[TELEMETRY STUBBED]\\x1b[0m Anonymous Telemetry stubbed in development.`\n    );\n    return;\n  }\n\n  console.log(\n    `\\x1b[32m[TELEMETRY ENABLED]\\x1b[0m Anonymous Telemetry enabled. Telemetry helps Mintplex Labs Inc improve AnythingLLM.`\n  );\n  await Telemetry.findOrCreateId();\n  await Telemetry.sendTelemetry(\"server_boot\", {\n    commit: getGitVersion(),\n  });\n  return;\n}\n\nmodule.exports = setupTelemetry;\n"
  },
  {
    "path": "server/utils/vectorDbProviders/astra/ASTRA_SETUP.md",
    "content": "# How to setup Astra Vector Database for AnythingLLM\n\n[Official Astra DB Docs](https://docs.datastax.com/en/astra/astra-db-vector/get-started/quickstart.html) for reference.\n\n### How to get started\n\n**Requirements**\n\n- Astra Vector Database with active status.\n\n**Instructions**\n\n- [Create an Astra account or sign in to an existing Astra account](https://astra.datastax.com)\n- Create an Astra Serverless(Vector) Database.\n- Make sure DB is in active state.\n- Get `API ENDPOINT`and `Application Token` from Overview screen\n\n```\nVECTOR_DB=\"astra\"\nASTRA_DB_ENDPOINT=Astra DB API endpoint\nASTRA_DB_APPLICATION_TOKEN=AstraCS:..\n```\n"
  },
  {
    "path": "server/utils/vectorDbProviders/astra/index.js",
    "content": "const { AstraDB: AstraClient } = require(\"@datastax/astra-db-ts\");\nconst { TextSplitter } = require(\"../../TextSplitter\");\nconst { SystemSettings } = require(\"../../../models/systemSettings\");\nconst { storeVectorResult, cachedVectorInformation } = require(\"../../files\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst { toChunks, getEmbeddingEngineSelection } = require(\"../../helpers\");\nconst { sourceIdentifier } = require(\"../../chats\");\nconst { VectorDatabase } = require(\"../base\");\n\nconst sanitizeNamespace = (namespace) => {\n  // If namespace already starts with ns_, don't add it again\n  if (namespace.startsWith(\"ns_\")) return namespace;\n\n  // Remove any invalid characters, ensure starts with letter\n  return `ns_${namespace.replace(/[^a-zA-Z0-9_]/g, \"_\")}`;\n};\n\n// Add this helper method to check if collection exists more reliably\nconst collectionExists = async function (client, namespace) {\n  try {\n    const collections = await AstraDB.allNamespaces(client);\n    if (collections) {\n      return collections.includes(namespace);\n    }\n  } catch (error) {\n    this.logger(\"collectionExists check error\", error?.message || error);\n    return false; // Return false for any error to allow creation attempt\n  }\n};\n\nclass AstraDB extends VectorDatabase {\n  constructor() {\n    super();\n  }\n\n  get name() {\n    return \"AstraDB\";\n  }\n\n  async connect() {\n    if (process.env.VECTOR_DB !== \"astra\")\n      throw new Error(\"AstraDB::Invalid ENV settings\");\n\n    const client = new AstraClient(\n      process?.env?.ASTRA_DB_APPLICATION_TOKEN,\n      process?.env?.ASTRA_DB_ENDPOINT\n    );\n    return { client };\n  }\n\n  async heartbeat() {\n    return { heartbeat: Number(new Date()) };\n  }\n\n  // Astra interface will return a valid collection object even if the collection\n  // does not actually exist. So we run a simple check which will always throw\n  // when the table truly does not exist. Faster than iterating all collections.\n  async isRealCollection(astraCollection = null) {\n    if (!astraCollection) return false;\n    return await astraCollection\n      .countDocuments()\n      .then(() => true)\n      .catch(() => false);\n  }\n\n  async totalVectors() {\n    const { client } = await this.connect();\n    const collectionNames = await this.allNamespaces(client);\n    var totalVectors = 0;\n    for (const name of collectionNames) {\n      const collection = await client.collection(name).catch(() => null);\n      const count = await collection.countDocuments().catch(() => 0);\n      totalVectors += count ? count : 0;\n    }\n    return totalVectors;\n  }\n\n  async namespaceCount(_namespace = null) {\n    const { client } = await this.connect();\n    const namespace = await this.namespace(client, _namespace);\n    return namespace?.vectorCount || 0;\n  }\n\n  async namespace(client, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    const sanitizedNamespace = sanitizeNamespace(namespace);\n    const collection = await client\n      .collection(sanitizedNamespace)\n      .catch(() => null);\n    if (!(await this.isRealCollection(collection))) return null;\n\n    const count = await collection.countDocuments().catch((e) => {\n      this.logger(\"namespaceExists\", e.message);\n      return null;\n    });\n\n    return {\n      name: namespace,\n      ...collection,\n      vectorCount: typeof count === \"number\" ? count : 0,\n    };\n  }\n\n  async hasNamespace(namespace = null) {\n    if (!namespace) return false;\n    const { client } = await this.connect();\n    return await this.namespaceExists(client, namespace);\n  }\n\n  async namespaceExists(client, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    const sanitizedNamespace = sanitizeNamespace(namespace);\n    const collection = await client.collection(sanitizedNamespace);\n    return await this.isRealCollection(collection);\n  }\n\n  async deleteVectorsInNamespace(client, namespace = null) {\n    const sanitizedNamespace = sanitizeNamespace(namespace);\n    await client.dropCollection(sanitizedNamespace);\n    return true;\n  }\n\n  // AstraDB requires a dimension aspect for collection creation\n  // we pass this in from the first chunk to infer the dimensions like other\n  // providers do.\n  async getOrCreateCollection(client, namespace, dimensions = null) {\n    const sanitizedNamespace = sanitizeNamespace(namespace);\n    try {\n      const exists = await collectionExists(client, sanitizedNamespace);\n\n      if (!exists) {\n        if (!dimensions) {\n          throw new Error(\n            `AstraDB:getOrCreateCollection Unable to infer vector dimension from input. Open an issue on Github for support.`\n          );\n        }\n\n        // Create new collection\n        await client.createCollection(sanitizedNamespace, {\n          vector: {\n            dimension: dimensions,\n            metric: \"cosine\",\n          },\n        });\n\n        // Get the newly created collection\n        return await client.collection(sanitizedNamespace);\n      }\n\n      return await client.collection(sanitizedNamespace);\n    } catch (error) {\n      this.logger(\"getOrCreateCollection\", error?.message || error);\n      throw error;\n    }\n  }\n\n  async addDocumentToNamespace(\n    namespace,\n    documentData = {},\n    fullFilePath = null,\n    skipCache = false\n  ) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    try {\n      let vectorDimension = null;\n      const { pageContent, docId, ...metadata } = documentData;\n      if (!pageContent || pageContent.length == 0) return false;\n\n      this.logger(\"Adding new vectorized document into namespace\", namespace);\n      if (!skipCache) {\n        const cacheResult = await cachedVectorInformation(fullFilePath);\n        if (cacheResult.exists) {\n          const { client } = await this.connect();\n          const { chunks } = cacheResult;\n          const documentVectors = [];\n          vectorDimension = chunks[0][0].values.length || null;\n\n          const collection = await this.getOrCreateCollection(\n            client,\n            namespace,\n            vectorDimension\n          );\n          if (!(await this.isRealCollection(collection)))\n            throw new Error(\"Failed to create new AstraDB collection!\", {\n              namespace,\n            });\n\n          for (const chunk of chunks) {\n            // Before sending to Astra and saving the records to our db\n            // we need to assign the id of each chunk that is stored in the cached file.\n            const newChunks = chunk.map((chunk) => {\n              const _id = uuidv4();\n              documentVectors.push({ docId, vectorId: _id });\n              return {\n                _id: _id,\n                $vector: chunk.values,\n                metadata: chunk.metadata || {},\n              };\n            });\n\n            await collection.insertMany(newChunks);\n          }\n          await DocumentVectors.bulkInsert(documentVectors);\n          return { vectorized: true, error: null };\n        }\n      }\n\n      const EmbedderEngine = getEmbeddingEngineSelection();\n      const textSplitter = new TextSplitter({\n        chunkSize: Math.min(\n          7500,\n          TextSplitter.determineMaxChunkSize(\n            await SystemSettings.getValueOrFallback({\n              label: \"text_splitter_chunk_size\",\n            }),\n            EmbedderEngine?.embeddingMaxChunkLength\n          )\n        ),\n        chunkOverlap: await SystemSettings.getValueOrFallback(\n          { label: \"text_splitter_chunk_overlap\" },\n          20\n        ),\n        chunkHeaderMeta: TextSplitter.buildHeaderMeta(metadata),\n        chunkPrefix: EmbedderEngine?.embeddingPrefix,\n      });\n      const textChunks = await textSplitter.splitText(pageContent);\n\n      this.logger(\"Snippets created from document:\", textChunks.length);\n      const documentVectors = [];\n      const vectors = [];\n      const vectorValues = await EmbedderEngine.embedChunks(textChunks);\n\n      if (!!vectorValues && vectorValues.length > 0) {\n        for (const [i, vector] of vectorValues.entries()) {\n          if (!vectorDimension) vectorDimension = vector.length;\n          const vectorRecord = {\n            _id: uuidv4(),\n            $vector: vector,\n            metadata: { ...metadata, text: textChunks[i] },\n          };\n\n          vectors.push(vectorRecord);\n          documentVectors.push({ docId, vectorId: vectorRecord._id });\n        }\n      } else {\n        throw new Error(\n          \"Could not embed document chunks! This document will not be recorded.\"\n        );\n      }\n      const { client } = await this.connect();\n      const collection = await this.getOrCreateCollection(\n        client,\n        namespace,\n        vectorDimension\n      );\n      if (!(await this.isRealCollection(collection)))\n        throw new Error(\"Failed to create new AstraDB collection!\", {\n          namespace,\n        });\n\n      if (vectors.length > 0) {\n        const chunks = [];\n\n        this.logger(\"Inserting vectorized chunks into Astra DB.\");\n\n        // AstraDB has maximum upsert size of 20 records per-request so we have to use a lower chunk size here\n        // in order to do the queries - this takes a lot more time than other providers but there\n        // is no way around it. This will save the vector-cache with the same layout, so we don't\n        // have to chunk again for cached files.\n        for (const chunk of toChunks(vectors, 20)) {\n          chunks.push(\n            chunk.map((c) => {\n              return { id: c._id, values: c.$vector, metadata: c.metadata };\n            })\n          );\n          await collection.insertMany(chunk);\n        }\n        await storeVectorResult(chunks, fullFilePath);\n      }\n\n      await DocumentVectors.bulkInsert(documentVectors);\n      return { vectorized: true, error: null };\n    } catch (e) {\n      this.logger(\"addDocumentToNamespace\", e.message);\n      return { vectorized: false, error: e.message };\n    }\n  }\n\n  async deleteDocumentFromNamespace(namespace, docId) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    const { client } = await this.connect();\n    namespace = sanitizeNamespace(namespace);\n    if (!(await this.namespaceExists(client, namespace)))\n      throw new Error(\n        \"Invalid namespace - has it been collected and populated yet?\"\n      );\n    const collection = await client.collection(namespace);\n\n    const knownDocuments = await DocumentVectors.where({ docId });\n    if (knownDocuments.length === 0) return;\n\n    const vectorIds = knownDocuments.map((doc) => doc.vectorId);\n    for (const id of vectorIds) {\n      await collection.deleteMany({\n        _id: id,\n      });\n    }\n\n    const indexes = knownDocuments.map((doc) => doc.id);\n    await DocumentVectors.deleteIds(indexes);\n    return true;\n  }\n\n  async performSimilaritySearch({\n    namespace = null,\n    input = \"\",\n    LLMConnector = null,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    if (!namespace || !input || !LLMConnector)\n      throw new Error(\"Invalid request to performSimilaritySearch.\");\n\n    const { client } = await this.connect();\n    // Sanitize namespace before checking existence\n    const sanitizedNamespace = sanitizeNamespace(namespace);\n\n    if (!(await this.namespaceExists(client, sanitizedNamespace))) {\n      return {\n        contextTexts: [],\n        sources: [],\n        message:\n          \"Invalid query - no namespace found for workspace in vector db!\",\n      };\n    }\n\n    const queryVector = await LLMConnector.embedTextInput(input);\n    const { contextTexts, sourceDocuments } = await this.similarityResponse({\n      client,\n      namespace: sanitizedNamespace,\n      queryVector,\n      similarityThreshold,\n      topN,\n      filterIdentifiers,\n    });\n\n    const sources = sourceDocuments.map((metadata, i) => {\n      return { ...metadata, text: contextTexts[i] };\n    });\n    return {\n      contextTexts,\n      sources: this.curateSources(sources),\n      message: false,\n    };\n  }\n\n  async similarityResponse({\n    client,\n    namespace,\n    queryVector,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    const result = {\n      contextTexts: [],\n      sourceDocuments: [],\n      scores: [],\n    };\n    // Namespace should already be sanitized, but let's be defensive\n    const sanitizedNamespace = sanitizeNamespace(namespace);\n    const collection = await client.collection(sanitizedNamespace);\n    const responses = await collection\n      .find(\n        {},\n        {\n          sort: { $vector: queryVector },\n          limit: topN,\n          includeSimilarity: true,\n        }\n      )\n      .toArray();\n\n    responses.forEach((response) => {\n      if (response.$similarity < similarityThreshold) return;\n      if (filterIdentifiers.includes(sourceIdentifier(response.metadata))) {\n        this.logger(\n          \"A source was filtered from context as it's parent document is pinned.\"\n        );\n        return;\n      }\n      result.contextTexts.push(response.metadata.text);\n      result.sourceDocuments.push({\n        ...response.metadata,\n        score: response.$similarity,\n      });\n      result.scores.push(response.$similarity);\n    });\n    return result;\n  }\n\n  async allNamespaces(client) {\n    try {\n      let header = new Headers();\n      header.append(\"Token\", client?.httpClient?.applicationToken);\n      header.append(\"Content-Type\", \"application/json\");\n\n      let raw = JSON.stringify({\n        findCollections: {},\n      });\n\n      let requestOptions = {\n        method: \"POST\",\n        headers: header,\n        body: raw,\n        redirect: \"follow\",\n      };\n\n      const call = await fetch(client?.httpClient?.baseUrl, requestOptions);\n      const resp = await call?.text();\n      const collections = resp ? JSON.parse(resp)?.status?.collections : [];\n      return collections;\n    } catch (e) {\n      this.logger(\"AllNamespace\", e);\n      return [];\n    }\n  }\n\n  async \"namespace-stats\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    if (!namespace) throw new Error(\"namespace required\");\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace)))\n      throw new Error(\"Namespace by that name does not exist.\");\n    const stats = await this.namespace(client, namespace);\n    return stats\n      ? stats\n      : { message: \"No stats were able to be fetched from DB for namespace\" };\n  }\n\n  async \"delete-namespace\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace)))\n      throw new Error(\"Namespace by that name does not exist.\");\n\n    const details = await this.namespace(client, namespace);\n    await this.deleteVectorsInNamespace(client, namespace);\n    return {\n      message: `Namespace ${namespace} was deleted along with ${\n        details?.vectorCount || \"all\"\n      } vectors.`,\n    };\n  }\n\n  curateSources(sources = []) {\n    const documents = [];\n    for (const source of sources) {\n      if (Object.keys(source).length > 0) {\n        const metadata = source.hasOwnProperty(\"metadata\")\n          ? source.metadata\n          : source;\n        documents.push({\n          ...metadata,\n        });\n      }\n    }\n\n    return documents;\n  }\n}\n\nmodule.exports.AstraDB = AstraDB;\n"
  },
  {
    "path": "server/utils/vectorDbProviders/base.js",
    "content": "/* eslint-disable unused-imports/no-unused-vars */\n\n/* Base class for all Vector Database providers.\n * All vector database providers should extend this class and implement/override the necessary methods.\n */\nclass VectorDatabase {\n  get name() {\n    return \"VectorDatabase\";\n  }\n\n  constructor() {\n    if (this.constructor === VectorDatabase) {\n      throw new Error(\"VectorDatabase cannot be instantiated directly\");\n    }\n  }\n\n  /**\n   * Connect to vector database client\n   * @returns {Promise<{client: any}>}\n   */\n  async connect() {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Heartbeat check for vector database client\n   * @returns {Promise<{heartbeat: number}>}\n   */\n  async heartbeat() {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Get total number of vectors across all namespaces\n   * @returns {Promise<number>}\n   */\n  async totalVectors() {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Get count of vectors in a specific namespace\n   * @param {string} namespace - Namespace to count vectors in\n   * @returns {Promise<number>}\n   */\n  async namespaceCount(namespace = null) {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Get namespace details\n   * @param {any} client - Vector database client\n   * @param {string} namespace - Namespace to get\n   * @returns {Promise<any>}\n   */\n  async namespace(client, namespace = null) {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Check if a namespace exists\n   * @param {string} namespace - Namespace to check\n   * @returns {Promise<boolean>}\n   */\n  async hasNamespace(namespace = null) {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Check if a namespace exists with a client\n   * @param {any} client - Vector database client\n   * @param {string} namespace - Namespace to check\n   * @returns {Promise<boolean>}\n   */\n  async namespaceExists(client, namespace = null) {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Delete all vectors in a namespace\n   * @param {any} client - Vector database client\n   * @param {string} namespace - Namespace to delete vectors from\n   * @returns {Promise<boolean>}\n   */\n  async deleteVectorsInNamespace(client, namespace = null) {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Add a document to a namespace\n   * @param {string} namespace - Namespace to add document to\n   * @param {Object} documentData - Document data\n   * @param {string} fullFilePath - Full file path\n   * @param {boolean} skipCache - Skip cache\n   * @returns {Promise<{vectorized: boolean, error: string|null}>}\n   */\n  async addDocumentToNamespace(\n    namespace,\n    documentData = {},\n    fullFilePath = null,\n    skipCache = false\n  ) {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Delete a document from namespace\n   * @param {string} namespace - Namespace to delete document from\n   * @param {string} docId - Document id\n   * @returns {Promise<boolean>}\n   */\n  async deleteDocumentFromNamespace(namespace, docId) {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Perform a similarity search\n   * @param {Object} params - Search parameters\n   * @param {string} params.namespace - Namespace to search in\n   * @param {string} params.input - Input text to search for\n   * @param {any} params.LLMConnector - LLM connector for embeddings\n   * @param {number} params.similarityThreshold - Similarity threshold\n   * @param {number} params.topN - Number of results to return\n   * @param {string[]} params.filterIdentifiers - Identifiers to filter out\n   * @returns {Promise<{contextTexts: string[], sources: any[], message: string|boolean}>}\n   */\n  async performSimilaritySearch({\n    namespace = null,\n    input = \"\",\n    LLMConnector = null,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Perform a similarity search and return raw results\n   * @param {Object} params - Search parameters\n   * @param {any} params.client - Vector database client\n   * @param {string} params.namespace - Namespace to search in\n   * @param {number[]} params.queryVector - Query vector\n   * @param {number} params.similarityThreshold - Similarity threshold\n   * @param {number} params.topN - Number of results to return\n   * @param {string[]} params.filterIdentifiers - Identifiers to filter out\n   * @returns {Promise<{contextTexts: string[], sourceDocuments: any[], scores: number[]}>}\n   */\n  async similarityResponse({\n    client,\n    namespace,\n    queryVector,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Get namespace stats\n   * @param {Object} reqBody - Request body\n   * @param {string} reqBody.namespace - Namespace to get stats for\n   * @returns {Promise<any>}\n   */\n  async \"namespace-stats\"(reqBody = {}) {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Delete a namespace\n   * @param {Object} reqBody - Request body\n   * @param {string} reqBody.namespace - Namespace to delete\n   * @returns {Promise<{message: string}>}\n   */\n  async \"delete-namespace\"(reqBody = {}) {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Reset vector database (delete all data)\n   * @returns {Promise<{reset: boolean}>}\n   */\n  async reset() {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  /**\n   * Curate sources from search results\n   * @param {any[]} sources - Sources to curate\n   * @returns {any[]}\n   */\n  curateSources(sources = []) {\n    throw new Error(\"Must be implemented by provider\");\n  }\n\n  logger(message = null, ...args) {\n    console.log(`\\x1b[36m[VectorDB::${this.name}]\\x1b[0m ${message}`, ...args);\n  }\n}\n\nmodule.exports = { VectorDatabase };\n"
  },
  {
    "path": "server/utils/vectorDbProviders/chroma/index.js",
    "content": "const { ChromaClient } = require(\"chromadb\");\nconst { TextSplitter } = require(\"../../TextSplitter\");\nconst { SystemSettings } = require(\"../../../models/systemSettings\");\nconst { storeVectorResult, cachedVectorInformation } = require(\"../../files\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst { toChunks, getEmbeddingEngineSelection } = require(\"../../helpers\");\nconst { parseAuthHeader } = require(\"../../http\");\nconst { sourceIdentifier } = require(\"../../chats\");\nconst { VectorDatabase } = require(\"../base\");\nconst COLLECTION_REGEX = new RegExp(\n  /^(?!\\d+\\.\\d+\\.\\d+\\.\\d+$)(?!.*\\.\\.)(?=^[a-zA-Z0-9][a-zA-Z0-9_-]{1,61}[a-zA-Z0-9]$).{3,63}$/\n);\n\nclass Chroma extends VectorDatabase {\n  constructor() {\n    super();\n  }\n\n  get name() {\n    return \"Chroma\";\n  }\n\n  // Chroma DB has specific requirements for collection names:\n  // (1) Must contain 3-63 characters\n  // (2) Must start and end with an alphanumeric character\n  // (3) Can only contain alphanumeric characters, underscores, or hyphens\n  // (4) Cannot contain two consecutive periods (..)\n  // (5) Cannot be a valid IPv4 address\n  // We need to enforce these rules by normalizing the collection names\n  // before communicating with the Chroma DB.\n  normalize(inputString) {\n    if (COLLECTION_REGEX.test(inputString)) return inputString;\n    let normalized = inputString.replace(/[^a-zA-Z0-9_-]/g, \"-\");\n\n    // Replace consecutive periods with a single period (if any)\n    normalized = normalized.replace(/\\.\\.+/g, \".\");\n\n    // Ensure the name doesn't start with a non-alphanumeric character\n    if (normalized[0] && !/^[a-zA-Z0-9]$/.test(normalized[0])) {\n      normalized = \"anythingllm-\" + normalized.slice(1);\n    }\n\n    // Ensure the name doesn't end with a non-alphanumeric character\n    if (\n      normalized[normalized.length - 1] &&\n      !/^[a-zA-Z0-9]$/.test(normalized[normalized.length - 1])\n    ) {\n      normalized = normalized.slice(0, -1);\n    }\n\n    // Ensure the length is between 3 and 63 characters\n    if (normalized.length < 3) {\n      normalized = `anythingllm-${normalized}`;\n    } else if (normalized.length > 63) {\n      // Recheck the norm'd name if sliced since its ending can still be invalid.\n      normalized = this.normalize(normalized.slice(0, 63));\n    }\n\n    // Ensure the name is not an IPv4 address\n    if (/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(normalized)) {\n      normalized = \"-\" + normalized.slice(1);\n    }\n\n    return normalized;\n  }\n\n  async connect() {\n    if (process.env.VECTOR_DB !== \"chroma\")\n      throw new Error(\"Chroma::Invalid ENV settings\");\n\n    const client = new ChromaClient({\n      path: process.env.CHROMA_ENDPOINT, // if not set will fallback to localhost:8000\n      ...(!!process.env.CHROMA_API_HEADER && !!process.env.CHROMA_API_KEY\n        ? {\n            fetchOptions: {\n              headers: parseAuthHeader(\n                process.env.CHROMA_API_HEADER || \"X-Api-Key\",\n                process.env.CHROMA_API_KEY\n              ),\n            },\n          }\n        : {}),\n    });\n\n    const isAlive = await client.heartbeat();\n    if (!isAlive)\n      throw new Error(\n        \"ChromaDB::Invalid Heartbeat received - is the instance online?\"\n      );\n    return { client };\n  }\n\n  async heartbeat() {\n    const { client } = await this.connect();\n    return { heartbeat: await client.heartbeat() };\n  }\n\n  async totalVectors() {\n    const { client } = await this.connect();\n    const collections = await client.listCollections();\n    var totalVectors = 0;\n    for (const collectionObj of collections) {\n      const collection = await client\n        .getCollection({ name: collectionObj.name })\n        .catch(() => null);\n      if (!collection) continue;\n      totalVectors += await collection.count();\n    }\n    return totalVectors;\n  }\n\n  distanceToSimilarity(distance = null) {\n    if (distance === null || typeof distance !== \"number\") return 0.0;\n    if (distance >= 1.0) return 1;\n    if (distance < 0) return 1 - Math.abs(distance);\n    return 1 - distance;\n  }\n\n  async namespaceCount(_namespace = null) {\n    const { client } = await this.connect();\n    const namespace = await this.namespace(client, this.normalize(_namespace));\n    return namespace?.vectorCount || 0;\n  }\n\n  async similarityResponse({\n    client,\n    namespace,\n    queryVector,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    const collection = await client.getCollection({\n      name: this.normalize(namespace),\n    });\n    const result = {\n      contextTexts: [],\n      sourceDocuments: [],\n      scores: [],\n    };\n\n    const response = await collection.query({\n      queryEmbeddings: queryVector,\n      nResults: topN,\n    });\n\n    response.ids[0].forEach((_, i) => {\n      const similarity = this.distanceToSimilarity(response.distances[0][i]);\n      if (similarity < similarityThreshold) return;\n\n      if (\n        filterIdentifiers.includes(sourceIdentifier(response.metadatas[0][i]))\n      ) {\n        this.logger(\n          \"A source was filtered from context as it's parent document is pinned.\"\n        );\n        return;\n      }\n\n      result.contextTexts.push(response.documents[0][i]);\n      result.sourceDocuments.push(response.metadatas[0][i]);\n      result.scores.push(similarity);\n    });\n\n    return result;\n  }\n\n  async namespace(client, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    const collection = await client\n      .getCollection({ name: this.normalize(namespace) })\n      .catch(() => null);\n    if (!collection) return null;\n\n    return {\n      ...collection,\n      vectorCount: await collection.count(),\n    };\n  }\n\n  async hasNamespace(namespace = null) {\n    if (!namespace) return false;\n    const { client } = await this.connect();\n    return await this.namespaceExists(client, this.normalize(namespace));\n  }\n\n  async namespaceExists(client, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    const collection = await client\n      .getCollection({ name: this.normalize(namespace) })\n      .catch((e) => {\n        this.logger(\"namespaceExists\", e.message);\n        return null;\n      });\n    return !!collection;\n  }\n\n  async deleteVectorsInNamespace(client, namespace = null) {\n    await client.deleteCollection({ name: this.normalize(namespace) });\n    return true;\n  }\n\n  async addDocumentToNamespace(\n    namespace,\n    documentData = {},\n    fullFilePath = null,\n    skipCache = false\n  ) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    try {\n      const { pageContent, docId, ...metadata } = documentData;\n      if (!pageContent || pageContent.length == 0) return false;\n\n      this.logger(\"Adding new vectorized document into namespace\", namespace);\n      if (!skipCache) {\n        const cacheResult = await cachedVectorInformation(fullFilePath);\n        if (cacheResult.exists) {\n          const { client } = await this.connect();\n          const collection = await client.getOrCreateCollection({\n            name: this.normalize(namespace),\n            // returns [-1, 1] unit vector\n            metadata: { \"hnsw:space\": \"cosine\" },\n          });\n          const { chunks } = cacheResult;\n          const documentVectors = [];\n\n          for (const chunk of chunks) {\n            const submission = {\n              ids: [],\n              embeddings: [],\n              metadatas: [],\n              documents: [],\n            };\n\n            // Before sending to Chroma and saving the records to our db\n            // we need to assign the id of each chunk that is stored in the cached file.\n            chunk.forEach((chunk) => {\n              const id = uuidv4();\n              const { id: _id, ...metadata } = chunk.metadata;\n              documentVectors.push({ docId, vectorId: id });\n              submission.ids.push(id);\n              submission.embeddings.push(chunk.values);\n              submission.metadatas.push(metadata);\n              submission.documents.push(metadata.text);\n            });\n\n            await this.smartAdd(collection, submission);\n          }\n\n          await DocumentVectors.bulkInsert(documentVectors);\n          return { vectorized: true, error: null };\n        }\n      }\n\n      // If we are here then we are going to embed and store a novel document.\n      // We have to do this manually as opposed to using LangChains `Chroma.fromDocuments`\n      // because we then cannot atomically control our namespace to granularly find/remove documents\n      // from vectordb.\n      const EmbedderEngine = getEmbeddingEngineSelection();\n      const textSplitter = new TextSplitter({\n        chunkSize: TextSplitter.determineMaxChunkSize(\n          await SystemSettings.getValueOrFallback({\n            label: \"text_splitter_chunk_size\",\n          }),\n          EmbedderEngine?.embeddingMaxChunkLength\n        ),\n        chunkOverlap: await SystemSettings.getValueOrFallback(\n          { label: \"text_splitter_chunk_overlap\" },\n          20\n        ),\n        chunkHeaderMeta: TextSplitter.buildHeaderMeta(metadata),\n        chunkPrefix: EmbedderEngine?.embeddingPrefix,\n      });\n      const textChunks = await textSplitter.splitText(pageContent);\n\n      this.logger(\"Snippets created from document:\", textChunks.length);\n      const documentVectors = [];\n      const vectors = [];\n      const vectorValues = await EmbedderEngine.embedChunks(textChunks);\n      const submission = {\n        ids: [],\n        embeddings: [],\n        metadatas: [],\n        documents: [],\n      };\n\n      if (!!vectorValues && vectorValues.length > 0) {\n        for (const [i, vector] of vectorValues.entries()) {\n          const vectorRecord = {\n            id: uuidv4(),\n            values: vector,\n            // [DO NOT REMOVE]\n            // LangChain will be unable to find your text if you embed manually and dont include the `text` key.\n            // https://github.com/hwchase17/langchainjs/blob/2def486af734c0ca87285a48f1a04c057ab74bdf/langchain/src/vectorstores/pinecone.ts#L64\n            metadata: { ...metadata, text: textChunks[i] },\n          };\n\n          submission.ids.push(vectorRecord.id);\n          submission.embeddings.push(vectorRecord.values);\n          submission.metadatas.push(metadata);\n          submission.documents.push(textChunks[i]);\n\n          vectors.push(vectorRecord);\n          documentVectors.push({ docId, vectorId: vectorRecord.id });\n        }\n      } else {\n        throw new Error(\n          \"Could not embed document chunks! This document will not be recorded.\"\n        );\n      }\n\n      const { client } = await this.connect();\n      const collection = await client.getOrCreateCollection({\n        name: this.normalize(namespace),\n        metadata: { \"hnsw:space\": \"cosine\" },\n      });\n\n      if (vectors.length > 0) {\n        const chunks = [];\n        this.logger(\"Inserting vectorized chunks into Chroma collection.\");\n        for (const chunk of toChunks(vectors, 500)) chunks.push(chunk);\n\n        try {\n          await this.smartAdd(collection, submission);\n          this.logger(\n            `Successfully added ${submission.ids.length} vectors to collection ${this.normalize(namespace)}`\n          );\n        } catch (error) {\n          this.logger(\"Error adding to ChromaDB:\", error);\n          throw new Error(`Error embedding into ChromaDB: ${error.message}`);\n        }\n\n        await storeVectorResult(chunks, fullFilePath);\n      }\n\n      await DocumentVectors.bulkInsert(documentVectors);\n      return { vectorized: true, error: null };\n    } catch (e) {\n      this.logger(\"addDocumentToNamespace\", e.message);\n      return { vectorized: false, error: e.message };\n    }\n  }\n\n  async deleteDocumentFromNamespace(namespace, docId) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace))) return;\n    const collection = await client.getCollection({\n      name: this.normalize(namespace),\n    });\n\n    const knownDocuments = await DocumentVectors.where({ docId });\n    if (knownDocuments.length === 0) return;\n\n    const vectorIds = knownDocuments.map((doc) => doc.vectorId);\n    await this.smartDelete(collection, vectorIds);\n\n    const indexes = knownDocuments.map((doc) => doc.id);\n    await DocumentVectors.deleteIds(indexes);\n    return true;\n  }\n\n  async performSimilaritySearch({\n    namespace = null,\n    input = \"\",\n    LLMConnector = null,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    if (!namespace || !input || !LLMConnector)\n      throw new Error(\"Invalid request to performSimilaritySearch.\");\n\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, this.normalize(namespace)))) {\n      return {\n        contextTexts: [],\n        sources: [],\n        message: \"Invalid query - no documents found for workspace!\",\n      };\n    }\n\n    const queryVector = await LLMConnector.embedTextInput(input);\n    const { contextTexts, sourceDocuments, scores } =\n      await this.similarityResponse({\n        client,\n        namespace,\n        queryVector,\n        similarityThreshold,\n        topN,\n        filterIdentifiers,\n      });\n\n    const sources = sourceDocuments.map((metadata, i) => ({\n      metadata: {\n        ...metadata,\n        text: contextTexts[i],\n        score: scores?.[i] || null,\n      },\n    }));\n\n    return {\n      contextTexts,\n      sources: this.curateSources(sources),\n      message: false,\n    };\n  }\n\n  async \"namespace-stats\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    if (!namespace) throw new Error(\"namespace required\");\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, this.normalize(namespace))))\n      throw new Error(\"Namespace by that name does not exist.\");\n    const stats = await this.namespace(client, this.normalize(namespace));\n    return stats\n      ? stats\n      : { message: \"No stats were able to be fetched from DB for namespace\" };\n  }\n\n  async \"delete-namespace\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, this.normalize(namespace))))\n      throw new Error(\"Namespace by that name does not exist.\");\n\n    const details = await this.namespace(client, this.normalize(namespace));\n    await this.deleteVectorsInNamespace(client, this.normalize(namespace));\n    return {\n      message: `Namespace ${namespace} was deleted along with ${details?.vectorCount} vectors.`,\n    };\n  }\n\n  async reset() {\n    const { client } = await this.connect();\n    await client.reset();\n    return { reset: true };\n  }\n\n  curateSources(sources = []) {\n    const documents = [];\n    for (const source of sources) {\n      const { metadata = {} } = source;\n      if (Object.keys(metadata).length > 0) {\n        documents.push({\n          ...metadata,\n          ...(source.hasOwnProperty(\"pageContent\")\n            ? { text: source.pageContent }\n            : {}),\n        });\n      }\n    }\n\n    return documents;\n  }\n\n  /**\n   * This method is a wrapper around the ChromaCollection.add method.\n   * It will return true if the add was successful, false otherwise.\n   * For local deployments, this will be the same as calling the add method directly since there are no limitations.\n   * @param {import(\"chromadb\").Collection} collection\n   * @param {{ids: string[], embeddings: number[], metadatas: Record<string, any>[], documents: string[]}[]} submissions\n   * @returns {Promise<boolean>} True if the add was successful, false otherwise.\n   */\n  async smartAdd(collection, submissions) {\n    await collection.add(submissions);\n    return true;\n  }\n\n  /**\n   * This method is a wrapper around the ChromaCollection.delete method.\n   * It will return the result of the delete method directly.\n   * For local deployments, this will be the same as calling the delete method directly since there are no limitations.\n   * @param {import(\"chromadb\").Collection} collection\n   * @param {string[]} vectorIds\n   * @returns {Promise<boolean>} True if the delete was successful, false otherwise.\n   */\n  async smartDelete(collection, vectorIds) {\n    await collection.delete({ ids: vectorIds });\n    return true;\n  }\n}\n\nmodule.exports.Chroma = Chroma;\n"
  },
  {
    "path": "server/utils/vectorDbProviders/chromacloud/index.js",
    "content": "const { CloudClient } = require(\"chromadb\");\nconst { Chroma } = require(\"../chroma\");\nconst { toChunks } = require(\"../../helpers\");\n\n/**\n * ChromaCloud works nearly the same as Chroma so we can just extend the\n * Chroma class and override the connect method to use the CloudClient for major differences in API functionality.\n */\nclass ChromaCloud extends Chroma {\n  constructor() {\n    super();\n  }\n\n  get name() {\n    return \"ChromaCloud\";\n  }\n\n  /**\n   * Basic quota/limitations for Chroma Cloud for accounts. Does not lookup client-specific limits.\n   * @see https://docs.trychroma.com/cloud/quotas-limits\n   */\n  limits = {\n    maxEmbeddingDim: 4_096,\n    maxDocumentBytes: 16_384,\n    maxMetadataBytes: 4_096,\n    maxRecordsPerWrite: 300,\n  };\n\n  async connect() {\n    if (process.env.VECTOR_DB !== \"chromacloud\")\n      throw new Error(\"ChromaCloud::Invalid ENV settings\");\n\n    const client = new CloudClient({\n      apiKey: process.env.CHROMACLOUD_API_KEY,\n      tenant: process.env.CHROMACLOUD_TENANT,\n      database: process.env.CHROMACLOUD_DATABASE,\n    });\n\n    const isAlive = await client.heartbeat();\n    if (!isAlive)\n      throw new Error(\n        \"ChromaCloud::Invalid Heartbeat received - is the instance online?\"\n      );\n    return { client };\n  }\n\n  /**\n   * Chroma Cloud has some basic limitations on upserts to protect performance and latency.\n   * Local deployments do not have these limitations since they are self-hosted.\n   *\n   * This method, if cloud, will do some simple logic/heuristics to ensure that the upserts are not too large.\n   * Otherwise, it may throw a 422.\n   * @param {import(\"chromadb\").Collection} collection\n   * @param {{ids: string[], embeddings: number[], metadatas: Record<string, any>[], documents: string[]}[]} submissions\n   * @returns {Promise<boolean>} True if the upsert was successful, false otherwise.\n   * If the upsert was not successful, the error message will be returned.\n   */\n  async smartAdd(collection, submission) {\n    const testSubmission = {\n      id: submission.ids[0],\n      embedding: submission.embeddings[0],\n      metadata: submission.metadatas[0],\n      document: submission.documents[0],\n    };\n\n    if (testSubmission.embedding.length > this.limits.maxEmbeddingDim)\n      console.warn(\n        `ChromaCloud::Embedding dimension too large (default max is ${this.limits.maxEmbeddingDim}). Got ${testSubmission.embedding.length}. Upsert may fail!`\n      );\n    if (testSubmission.document.length > this.limits.maxDocumentBytes)\n      console.warn(\n        `ChromaCloud::Document length too large (default max is ${this.limits.maxDocumentBytes}). Got ${testSubmission.document.length}. Upsert may fail!`\n      );\n    if (\n      JSON.stringify(testSubmission.metadata).length >\n      this.limits.maxMetadataBytes\n    )\n      console.warn(\n        `ChromaCloud::Metadata length too large (default max is ${this.limits.maxMetadataBytes}). Got ${JSON.stringify(testSubmission.metadata).length}. Upsert may fail!`\n      );\n\n    // If the submissions are not too large, just add them directly.\n    if (submission.ids.length <= this.limits.maxRecordsPerWrite) {\n      await collection.add(submission);\n      return true;\n    }\n\n    this.logger(\n      `Upsert Payload is too large (max is ${this.limits.maxRecordsPerWrite} records). Splitting into chunks of ${this.limits.maxRecordsPerWrite} records.`\n    );\n    const chunks = [];\n    let chunkedSubmission = {\n      ids: [],\n      embeddings: [],\n      metadatas: [],\n      documents: [],\n    };\n    for (let i = 0; i < submission.ids.length; i++) {\n      chunkedSubmission.ids.push(submission.ids[i]);\n      chunkedSubmission.embeddings.push(submission.embeddings[i]);\n      chunkedSubmission.metadatas.push(submission.metadatas[i]);\n      chunkedSubmission.documents.push(submission.documents[i]);\n      if (chunkedSubmission.ids.length === this.limits.maxRecordsPerWrite) {\n        this.logger(\n          `ChromaCloud::Adding chunk payload ${chunks.length + 1} of ${Math.ceil(submission.ids.length / this.limits.maxRecordsPerWrite)}`\n        );\n        chunks.push(chunkedSubmission);\n        chunkedSubmission = {\n          ids: [],\n          embeddings: [],\n          metadatas: [],\n          documents: [],\n        };\n      }\n    }\n    // Push remaining submissions to the last chunk\n    if (chunkedSubmission.ids.length > 0) chunks.push(chunkedSubmission);\n\n    let counter = 1;\n    for (const chunk of chunks) {\n      await collection.add(chunk);\n      //eslint-disable-next-line\n      counter++;\n    }\n    return true;\n  }\n\n  /**\n   * This method is a wrapper around the ChromaCollection.delete method.\n   * It will return the result of the delete method directly.\n   * Chroma Cloud has some basic limitations on deletes to protect performance and latency.\n   * Local deployments do not have these limitations since they are self-hosted.\n   *\n   * This method, if cloud, will do some simple logic/heuristics to ensure that the deletes are not too large.\n   * Otherwise, it may throw a 422.\n   * @param {import(\"chromadb\").Collection} collection\n   * @param {string[]} vectorIds\n   * @returns {Promise<boolean>} True if the delete was successful, false otherwise.\n   */\n  async smartDelete(collection, vectorIds) {\n    if (vectorIds.length <= this.limits.maxRecordsPerWrite)\n      return await collection.delete({ ids: vectorIds });\n\n    this.logger(\n      `Delete Payload is too large (max is ${this.limits.maxRecordsPerWrite} records). Splitting into chunks of ${this.limits.maxRecordsPerWrite} records.`\n    );\n    const chunks = toChunks(vectorIds, this.limits.maxRecordsPerWrite);\n    let counter = 1;\n    for (const chunk of chunks) {\n      this.logger(`Deleting chunk ${counter} of ${chunks.length}`);\n      await collection.delete({ ids: chunk });\n      counter++;\n    }\n    return true;\n  }\n}\n\nmodule.exports.ChromaCloud = ChromaCloud;\n"
  },
  {
    "path": "server/utils/vectorDbProviders/lance/index.js",
    "content": "const lancedb = require(\"@lancedb/lancedb\");\nconst { toChunks, getEmbeddingEngineSelection } = require(\"../../helpers\");\nconst { TextSplitter } = require(\"../../TextSplitter\");\nconst { SystemSettings } = require(\"../../../models/systemSettings\");\nconst { storeVectorResult, cachedVectorInformation } = require(\"../../files\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst { sourceIdentifier } = require(\"../../chats\");\nconst { NativeEmbeddingReranker } = require(\"../../EmbeddingRerankers/native\");\nconst { VectorDatabase } = require(\"../base\");\nconst path = require(\"path\");\n\n/**\n * LancedDB Client connection object\n * @typedef {import('@lancedb/lancedb').Connection} LanceClient\n */\n\nclass LanceDb extends VectorDatabase {\n  constructor() {\n    super();\n  }\n\n  get uri() {\n    const basePath = !!process.env.STORAGE_DIR\n      ? process.env.STORAGE_DIR\n      : path.resolve(__dirname, \"../../../storage\");\n    return path.resolve(basePath, \"lancedb\");\n  }\n\n  get name() {\n    return \"LanceDb\";\n  }\n\n  /** @returns {Promise<{client: LanceClient}>} */\n  async connect() {\n    const client = await lancedb.connect(this.uri);\n    return { client };\n  }\n\n  distanceToSimilarity(distance = null) {\n    if (distance === null || typeof distance !== \"number\") return 0.0;\n    if (distance >= 1.0) return 1;\n    if (distance < 0) return 1 - Math.abs(distance);\n    return 1 - distance;\n  }\n\n  async heartbeat() {\n    await this.connect();\n    return { heartbeat: Number(new Date()) };\n  }\n\n  async tables() {\n    const { client } = await this.connect();\n    return await client.tableNames();\n  }\n\n  async totalVectors() {\n    const { client } = await this.connect();\n    const tables = await client.tableNames();\n    let count = 0;\n    for (const tableName of tables) {\n      const table = await client.openTable(tableName);\n      count += await table.countRows();\n    }\n    return count;\n  }\n\n  async namespaceCount(_namespace = null) {\n    const { client } = await this.connect();\n    const exists = await this.namespaceExists(client, _namespace);\n    if (!exists) return 0;\n\n    const table = await client.openTable(_namespace);\n    return (await table.countRows()) || 0;\n  }\n\n  /**\n   * Performs a SimilaritySearch + Reranking on a namespace.\n   * @param {Object} params - The parameters for the rerankedSimilarityResponse.\n   * @param {Object} params.client - The vectorDB client.\n   * @param {string} params.namespace - The namespace to search in.\n   * @param {string} params.query - The query to search for (plain text).\n   * @param {number[]} params.queryVector - The vector of the query.\n   * @param {number} params.similarityThreshold - The threshold for similarity.\n   * @param {number} params.topN - the number of results to return from this process.\n   * @param {string[]} params.filterIdentifiers - The identifiers of the documents to filter out.\n   * @returns\n   */\n  async rerankedSimilarityResponse({\n    client,\n    namespace,\n    query,\n    queryVector,\n    topN = 4,\n    similarityThreshold = 0.25,\n    filterIdentifiers = [],\n  }) {\n    const reranker = new NativeEmbeddingReranker();\n    const collection = await client.openTable(namespace);\n    const totalEmbeddings = await this.namespaceCount(namespace);\n    const result = {\n      contextTexts: [],\n      sourceDocuments: [],\n      scores: [],\n    };\n\n    /**\n     * For reranking, we want to work with a larger number of results than the topN.\n     * This is because the reranker can only rerank the results it it given and we dont auto-expand the results.\n     * We want to give the reranker a larger number of results to work with.\n     *\n     * However, we cannot make this boundless as reranking is expensive and time consuming.\n     * So we limit the number of results to a maximum of 50 and a minimum of 10.\n     * This is a good balance between the number of results to rerank and the cost of reranking\n     * and ensures workspaces with 10K embeddings will still rerank within a reasonable timeframe on base level hardware.\n     *\n     * Benchmarks:\n     * On Intel Mac: 2.6 GHz 6-Core Intel Core i7 - 20 docs reranked in ~5.2 sec\n     */\n    const searchLimit = Math.max(\n      10,\n      Math.min(50, Math.ceil(totalEmbeddings * 0.1))\n    );\n    const vectorSearchResults = await collection\n      .vectorSearch(queryVector)\n      .distanceType(\"cosine\")\n      .limit(searchLimit)\n      .toArray();\n\n    await reranker\n      .rerank(query, vectorSearchResults, { topK: topN })\n      .then((rerankResults) => {\n        rerankResults.forEach((item) => {\n          if (this.distanceToSimilarity(item._distance) < similarityThreshold)\n            return;\n          const { vector: _, ...rest } = item;\n          if (filterIdentifiers.includes(sourceIdentifier(rest))) {\n            this.logger(\n              \"A source was filtered from context as it's parent document is pinned.\"\n            );\n            return;\n          }\n          const score =\n            item?.rerank_score || this.distanceToSimilarity(item._distance);\n\n          result.contextTexts.push(rest.text);\n          result.sourceDocuments.push({\n            ...rest,\n            score,\n          });\n          result.scores.push(score);\n        });\n      })\n      .catch((e) => {\n        this.logger(e);\n        this.logger(\"rerankedSimilarityResponse\", e.message);\n      });\n\n    return result;\n  }\n\n  /**\n   * Performs a SimilaritySearch on a give LanceDB namespace.\n   * @param {Object} params\n   * @param {LanceClient} params.client\n   * @param {string} params.namespace\n   * @param {number[]} params.queryVector\n   * @param {number} params.similarityThreshold\n   * @param {number} params.topN\n   * @param {string[]} params.filterIdentifiers\n   * @returns\n   */\n  async similarityResponse({\n    client,\n    namespace,\n    queryVector,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    const collection = await client.openTable(namespace);\n    const result = {\n      contextTexts: [],\n      sourceDocuments: [],\n      scores: [],\n    };\n\n    const response = await collection\n      .vectorSearch(queryVector)\n      .distanceType(\"cosine\")\n      .limit(topN)\n      .toArray();\n\n    response.forEach((item) => {\n      if (this.distanceToSimilarity(item._distance) < similarityThreshold)\n        return;\n      const { vector: _, ...rest } = item;\n      if (filterIdentifiers.includes(sourceIdentifier(rest))) {\n        this.logger(\n          \"A source was filtered from context as it's parent document is pinned.\"\n        );\n        return;\n      }\n\n      result.contextTexts.push(rest.text);\n      result.sourceDocuments.push({\n        ...rest,\n        score: this.distanceToSimilarity(item._distance),\n      });\n      result.scores.push(this.distanceToSimilarity(item._distance));\n    });\n\n    return result;\n  }\n\n  /**\n   *\n   * @param {LanceClient} client\n   * @param {string} namespace\n   * @returns\n   */\n  async namespace(client, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    const collection = await client.openTable(namespace).catch(() => false);\n    if (!collection) return null;\n\n    return {\n      ...collection,\n    };\n  }\n\n  /**\n   *\n   * @param {LanceClient} client\n   * @param {number[]} data\n   * @param {string} namespace\n   * @returns\n   */\n  async updateOrCreateCollection(client, data = [], namespace) {\n    const hasNamespace = await this.hasNamespace(namespace);\n    if (hasNamespace) {\n      const collection = await client.openTable(namespace);\n      await collection.add(data);\n      return true;\n    }\n\n    await client.createTable(namespace, data);\n    return true;\n  }\n\n  async hasNamespace(namespace = null) {\n    if (!namespace) return false;\n    const { client } = await this.connect();\n    const exists = await this.namespaceExists(client, namespace);\n    return exists;\n  }\n\n  /**\n   *\n   * @param {LanceClient} client\n   * @param {string} namespace\n   * @returns\n   */\n  async namespaceExists(client, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    const collections = await client.tableNames();\n    return collections.includes(namespace);\n  }\n\n  /**\n   *\n   * @param {LanceClient} client\n   * @param {string} namespace\n   * @returns\n   */\n  async deleteVectorsInNamespace(client, namespace = null) {\n    await client.dropTable(namespace);\n    return true;\n  }\n\n  async deleteDocumentFromNamespace(namespace, docId) {\n    const { client } = await this.connect();\n    const exists = await this.namespaceExists(client, namespace);\n    if (!exists) {\n      this.logger(\n        `deleteDocumentFromNamespace - namespace ${namespace} does not exist.`\n      );\n      return;\n    }\n\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    const table = await client.openTable(namespace);\n    const vectorIds = (await DocumentVectors.where({ docId })).map(\n      (record) => record.vectorId\n    );\n\n    if (vectorIds.length === 0) return;\n    await table.delete(`id IN (${vectorIds.map((v) => `'${v}'`).join(\",\")})`);\n    return true;\n  }\n\n  async addDocumentToNamespace(\n    namespace,\n    documentData = {},\n    fullFilePath = null,\n    skipCache = false\n  ) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    try {\n      const { pageContent, docId, ...metadata } = documentData;\n      if (!pageContent || pageContent.length == 0) return false;\n\n      this.logger(\"Adding new vectorized document into namespace\", namespace);\n      if (!skipCache) {\n        const cacheResult = await cachedVectorInformation(fullFilePath);\n        if (cacheResult.exists) {\n          const { client } = await this.connect();\n          const { chunks } = cacheResult;\n          const documentVectors = [];\n          const submissions = [];\n\n          for (const chunk of chunks) {\n            chunk.forEach((chunk) => {\n              const id = uuidv4();\n              const { id: _id, ...metadata } = chunk.metadata;\n              documentVectors.push({ docId, vectorId: id });\n              submissions.push({ id: id, vector: chunk.values, ...metadata });\n            });\n          }\n\n          await this.updateOrCreateCollection(client, submissions, namespace);\n          await DocumentVectors.bulkInsert(documentVectors);\n          return { vectorized: true, error: null };\n        }\n      }\n\n      // If we are here then we are going to embed and store a novel document.\n      // We have to do this manually as opposed to using LangChains `xyz.fromDocuments`\n      // because we then cannot atomically control our namespace to granularly find/remove documents\n      // from vectordb.\n      const EmbedderEngine = getEmbeddingEngineSelection();\n      const textSplitter = new TextSplitter({\n        chunkSize: TextSplitter.determineMaxChunkSize(\n          await SystemSettings.getValueOrFallback({\n            label: \"text_splitter_chunk_size\",\n          }),\n          EmbedderEngine?.embeddingMaxChunkLength\n        ),\n        chunkOverlap: await SystemSettings.getValueOrFallback(\n          { label: \"text_splitter_chunk_overlap\" },\n          20\n        ),\n        chunkHeaderMeta: TextSplitter.buildHeaderMeta(metadata),\n        chunkPrefix: EmbedderEngine?.embeddingPrefix,\n      });\n      const textChunks = await textSplitter.splitText(pageContent);\n\n      this.logger(\"Snippets created from document:\", textChunks.length);\n      const documentVectors = [];\n      const vectors = [];\n      const submissions = [];\n      const vectorValues = await EmbedderEngine.embedChunks(textChunks);\n\n      if (!!vectorValues && vectorValues.length > 0) {\n        for (const [i, vector] of vectorValues.entries()) {\n          const vectorRecord = {\n            id: uuidv4(),\n            values: vector,\n            // [DO NOT REMOVE]\n            // LangChain will be unable to find your text if you embed manually and dont include the `text` key.\n            // https://github.com/hwchase17/langchainjs/blob/2def486af734c0ca87285a48f1a04c057ab74bdf/langchain/src/vectorstores/pinecone.ts#L64\n            metadata: { ...metadata, text: textChunks[i] },\n          };\n\n          vectors.push(vectorRecord);\n          submissions.push({\n            ...vectorRecord.metadata,\n            id: vectorRecord.id,\n            vector: vectorRecord.values,\n          });\n          documentVectors.push({ docId, vectorId: vectorRecord.id });\n        }\n      } else {\n        throw new Error(\n          \"Could not embed document chunks! This document will not be recorded.\"\n        );\n      }\n\n      if (vectors.length > 0) {\n        const chunks = [];\n        for (const chunk of toChunks(vectors, 500)) chunks.push(chunk);\n\n        this.logger(\"Inserting vectorized chunks into LanceDB collection.\");\n        const { client } = await this.connect();\n        await this.updateOrCreateCollection(client, submissions, namespace);\n        await storeVectorResult(chunks, fullFilePath);\n      }\n\n      await DocumentVectors.bulkInsert(documentVectors);\n      return { vectorized: true, error: null };\n    } catch (e) {\n      this.logger(\"addDocumentToNamespace\", e.message);\n      return { vectorized: false, error: e.message };\n    }\n  }\n\n  async performSimilaritySearch({\n    namespace = null,\n    input = \"\",\n    LLMConnector = null,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n    rerank = false,\n  }) {\n    if (!namespace || !input || !LLMConnector)\n      throw new Error(\"Invalid request to performSimilaritySearch.\");\n\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace))) {\n      return {\n        contextTexts: [],\n        sources: [],\n        message: \"Invalid query - no documents found for workspace!\",\n      };\n    }\n\n    const queryVector = await LLMConnector.embedTextInput(input);\n    const result = rerank\n      ? await this.rerankedSimilarityResponse({\n          client,\n          namespace,\n          query: input,\n          queryVector,\n          similarityThreshold,\n          topN,\n          filterIdentifiers,\n        })\n      : await this.similarityResponse({\n          client,\n          namespace,\n          queryVector,\n          similarityThreshold,\n          topN,\n          filterIdentifiers,\n        });\n\n    const { contextTexts, sourceDocuments } = result;\n    const sources = sourceDocuments.map((metadata, i) => {\n      return { metadata: { ...metadata, text: contextTexts[i] } };\n    });\n    return {\n      contextTexts,\n      sources: this.curateSources(sources),\n      message: false,\n    };\n  }\n\n  async \"namespace-stats\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    if (!namespace) throw new Error(\"namespace required\");\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace)))\n      throw new Error(\"Namespace by that name does not exist.\");\n    const stats = await this.namespace(client, namespace);\n    return stats\n      ? stats\n      : { message: \"No stats were able to be fetched from DB for namespace\" };\n  }\n\n  async \"delete-namespace\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace)))\n      throw new Error(\"Namespace by that name does not exist.\");\n\n    await this.deleteVectorsInNamespace(client, namespace);\n    return {\n      message: `Namespace ${namespace} was deleted.`,\n    };\n  }\n\n  async reset() {\n    const { client } = await this.connect();\n    const fs = require(\"fs\");\n    fs.rm(`${client.uri}`, { recursive: true }, () => null);\n    return { reset: true };\n  }\n\n  curateSources(sources = []) {\n    const documents = [];\n    for (const source of sources) {\n      const { text, vector: _v, _distance: _d, ...rest } = source;\n      const metadata = rest.hasOwnProperty(\"metadata\") ? rest.metadata : rest;\n      if (Object.keys(metadata).length > 0) {\n        documents.push({\n          ...metadata,\n          ...(text ? { text } : {}),\n        });\n      }\n    }\n\n    return documents;\n  }\n}\n\nmodule.exports.LanceDb = LanceDb;\n"
  },
  {
    "path": "server/utils/vectorDbProviders/milvus/MILVUS_SETUP.md",
    "content": "# How to setup a local (or remote) Milvus Vector Database\n\n[Official Milvus Docs](https://milvus.io/docs/example_code.md) for reference.\n\n### How to get started\n\n**Requirements**\n\nChoose one of the following\n\n- Cloud\n\n  - [Cloud account](https://cloud.zilliz.com/)\n\n- Local\n  - Docker\n  - `git` available in your CLI/terminal\n\n**Instructions**\n\n- Cloud\n\n  - Create a Cluster on your cloud account\n  - Get connect Public Endpoint and Token\n  - Set .env.development variable in server\n\n- Local\n  - Download yaml file `wget https://github.com/milvus-io/milvus/releases/download/v2.3.4/milvus-standalone-docker-compose.yml -O docker-compose.yml`\n  - Start Milvus `sudo docker compose up -d`\n  - Check the containers are up and running `sudo docker compose ps`\n  - Get port number and set .env.development variable in server\n\neg: `server/.env.development`\n\n```\nVECTOR_DB=\"milvus\"\nMILVUS_ADDRESS=\"http://localhost:19530\"\nMILVUS_USERNAME=minioadmin # Whatever your username and password are\nMILVUS_PASSWORD=minioadmin\n```\n"
  },
  {
    "path": "server/utils/vectorDbProviders/milvus/index.js",
    "content": "const {\n  DataType,\n  MetricType,\n  IndexType,\n  MilvusClient,\n} = require(\"@zilliz/milvus2-sdk-node\");\nconst { TextSplitter } = require(\"../../TextSplitter\");\nconst { SystemSettings } = require(\"../../../models/systemSettings\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst { storeVectorResult, cachedVectorInformation } = require(\"../../files\");\nconst { toChunks, getEmbeddingEngineSelection } = require(\"../../helpers\");\nconst { sourceIdentifier } = require(\"../../chats\");\nconst { VectorDatabase } = require(\"../base\");\n\nclass Milvus extends VectorDatabase {\n  constructor() {\n    super();\n  }\n\n  get name() {\n    return \"Milvus\";\n  }\n\n  // Milvus/Zilliz only allows letters, numbers, and underscores in collection names\n  // so we need to enforce that by re-normalizing the names when communicating with\n  // the DB.\n  // If the first char of the collection is not an underscore or letter the collection name will be invalid.\n  normalize(inputString) {\n    let normalized = inputString.replace(/[^a-zA-Z0-9_]/g, \"_\");\n    if (new RegExp(/^[a-zA-Z_]/).test(normalized.slice(0, 1)))\n      normalized = `anythingllm_${normalized}`;\n    return normalized;\n  }\n\n  async connect() {\n    if (process.env.VECTOR_DB !== \"milvus\")\n      throw new Error(`${this.name}::Invalid ENV settings`);\n\n    const client = new MilvusClient({\n      address: process.env.MILVUS_ADDRESS,\n      username: process.env.MILVUS_USERNAME,\n      password: process.env.MILVUS_PASSWORD,\n    });\n\n    const { isHealthy } = await client.checkHealth();\n    if (!isHealthy)\n      throw new Error(\n        `${this.name}::Invalid Heartbeat received - is the instance online?`\n      );\n\n    return { client };\n  }\n\n  async heartbeat() {\n    await this.connect();\n    return { heartbeat: Number(new Date()) };\n  }\n\n  async totalVectors() {\n    const { client } = await this.connect();\n    const { collection_names } = await client.listCollections();\n    const total = collection_names.reduce(async (acc, collection_name) => {\n      const statistics = await client.getCollectionStatistics({\n        collection_name: this.normalize(collection_name),\n      });\n      return Number(acc) + Number(statistics?.data?.row_count ?? 0);\n    }, 0);\n    return total;\n  }\n\n  async namespaceCount(_namespace = null) {\n    const { client } = await this.connect();\n    const statistics = await client.getCollectionStatistics({\n      collection_name: this.normalize(_namespace),\n    });\n    return Number(statistics?.data?.row_count ?? 0);\n  }\n\n  async namespace(client, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    const collection = await client\n      .getCollectionStatistics({ collection_name: this.normalize(namespace) })\n      .catch(() => null);\n    return collection;\n  }\n\n  async hasNamespace(namespace = null) {\n    if (!namespace) return false;\n    const { client } = await this.connect();\n    return await this.namespaceExists(client, namespace);\n  }\n\n  async namespaceExists(client, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    const { value } = await client\n      .hasCollection({ collection_name: this.normalize(namespace) })\n      .catch((e) => {\n        console.error(`${this.name}::namespaceExists`, e.message);\n        return { value: false };\n      });\n    return value;\n  }\n\n  async deleteVectorsInNamespace(client, namespace = null) {\n    await client.dropCollection({ collection_name: this.normalize(namespace) });\n    return true;\n  }\n\n  // Milvus requires a dimension aspect for collection creation\n  // we pass this in from the first chunk to infer the dimensions like other\n  // providers do.\n  async getOrCreateCollection(client, namespace, dimensions = null) {\n    const isExists = await this.namespaceExists(client, namespace);\n    if (!isExists) {\n      if (!dimensions)\n        throw new Error(\n          `${this.name}::getOrCreateCollection Unable to infer vector dimension from input. Open an issue on GitHub for support.`\n        );\n\n      await client.createCollection({\n        collection_name: this.normalize(namespace),\n        fields: [\n          {\n            name: \"id\",\n            description: \"id\",\n            data_type: DataType.VarChar,\n            max_length: 255,\n            is_primary_key: true,\n          },\n          {\n            name: \"vector\",\n            description: \"vector\",\n            data_type: DataType.FloatVector,\n            dim: dimensions,\n          },\n          {\n            name: \"metadata\",\n            description: \"metadata\",\n            data_type: DataType.JSON,\n          },\n        ],\n      });\n      await client.createIndex({\n        collection_name: this.normalize(namespace),\n        field_name: \"vector\",\n        index_type: IndexType.AUTOINDEX,\n        metric_type: MetricType.COSINE,\n      });\n      await client.loadCollectionSync({\n        collection_name: this.normalize(namespace),\n      });\n    }\n  }\n\n  async addDocumentToNamespace(\n    namespace,\n    documentData = {},\n    fullFilePath = null,\n    skipCache = false\n  ) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    try {\n      let vectorDimension = null;\n      const { pageContent, docId, ...metadata } = documentData;\n      if (!pageContent || pageContent.length == 0) return false;\n\n      this.logger(\"Adding new vectorized document into namespace\", namespace);\n      if (!skipCache) {\n        const cacheResult = await cachedVectorInformation(fullFilePath);\n        if (cacheResult.exists) {\n          const { client } = await this.connect();\n          const { chunks } = cacheResult;\n          const documentVectors = [];\n          vectorDimension = chunks[0][0].values.length || null;\n\n          await this.getOrCreateCollection(client, namespace, vectorDimension);\n          try {\n            for (const chunk of chunks) {\n              // Before sending to Milvus and saving the records to our db\n              // we need to assign the id of each chunk that is stored in the cached file.\n              const newChunks = chunk.map((chunk) => {\n                const id = uuidv4();\n                documentVectors.push({ docId, vectorId: id });\n                return { id, vector: chunk.values, metadata: chunk.metadata };\n              });\n              const insertResult = await client.insert({\n                collection_name: this.normalize(namespace),\n                data: newChunks,\n              });\n\n              if (insertResult?.status.error_code !== \"Success\") {\n                throw new Error(\n                  `Error embedding into ${this.name}! Reason:${insertResult?.status.reason}`\n                );\n              }\n            }\n            await DocumentVectors.bulkInsert(documentVectors);\n            await client.flushSync({\n              collection_names: [this.normalize(namespace)],\n            });\n            return { vectorized: true, error: null };\n          } catch (insertError) {\n            console.error(\n              \"Error inserting cached chunks:\",\n              insertError.message\n            );\n            return { vectorized: false, error: insertError.message };\n          }\n        }\n      }\n\n      const EmbedderEngine = getEmbeddingEngineSelection();\n      const textSplitter = new TextSplitter({\n        chunkSize: TextSplitter.determineMaxChunkSize(\n          await SystemSettings.getValueOrFallback({\n            label: \"text_splitter_chunk_size\",\n          }),\n          EmbedderEngine?.embeddingMaxChunkLength\n        ),\n        chunkOverlap: await SystemSettings.getValueOrFallback(\n          { label: \"text_splitter_chunk_overlap\" },\n          20\n        ),\n        chunkHeaderMeta: TextSplitter.buildHeaderMeta(metadata),\n        chunkPrefix: EmbedderEngine?.embeddingPrefix,\n      });\n      const textChunks = await textSplitter.splitText(pageContent);\n\n      this.logger(\"Snippets created from document:\", textChunks.length);\n      const documentVectors = [];\n      const vectors = [];\n      const vectorValues = await EmbedderEngine.embedChunks(textChunks);\n\n      if (!!vectorValues && vectorValues.length > 0) {\n        for (const [i, vector] of vectorValues.entries()) {\n          if (!vectorDimension) vectorDimension = vector.length;\n          const vectorRecord = {\n            id: uuidv4(),\n            values: vector,\n            // [DO NOT REMOVE]\n            // LangChain will be unable to find your text if you embed manually and dont include the `text` key.\n            metadata: { ...metadata, text: textChunks[i] },\n          };\n\n          vectors.push(vectorRecord);\n          documentVectors.push({ docId, vectorId: vectorRecord.id });\n        }\n      } else {\n        throw new Error(\n          \"Could not embed document chunks! This document will not be recorded.\"\n        );\n      }\n\n      if (vectors.length > 0) {\n        const chunks = [];\n        const { client } = await this.connect();\n        await this.getOrCreateCollection(client, namespace, vectorDimension);\n\n        this.logger(`Inserting vectorized chunks into ${this.name}.`);\n        for (const chunk of toChunks(vectors, 100)) {\n          chunks.push(chunk);\n          const insertResult = await client.insert({\n            collection_name: this.normalize(namespace),\n            data: chunk.map((item) => ({\n              id: item.id,\n              vector: item.values,\n              metadata: item.metadata,\n            })),\n          });\n\n          if (insertResult?.status.error_code !== \"Success\") {\n            throw new Error(\n              `Error embedding into ${this.name}! Reason:${insertResult?.status.reason}`\n            );\n          }\n        }\n        await storeVectorResult(chunks, fullFilePath);\n        await client.flushSync({\n          collection_names: [this.normalize(namespace)],\n        });\n      }\n\n      await DocumentVectors.bulkInsert(documentVectors);\n      return { vectorized: true, error: null };\n    } catch (e) {\n      this.logger(\"addDocumentToNamespace\", e.message);\n      return { vectorized: false, error: e.message };\n    }\n  }\n\n  async deleteDocumentFromNamespace(namespace, docId) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace))) return;\n    const knownDocuments = await DocumentVectors.where({ docId });\n    if (knownDocuments.length === 0) return;\n\n    const vectorIds = knownDocuments.map((doc) => doc.vectorId);\n    const queryIn = vectorIds.map((v) => `'${v}'`).join(\",\");\n    await client.deleteEntities({\n      collection_name: this.normalize(namespace),\n      expr: `id in [${queryIn}]`,\n    });\n\n    const indexes = knownDocuments.map((doc) => doc.id);\n    await DocumentVectors.deleteIds(indexes);\n\n    // Even after flushing Milvus can take some time to re-calc the count\n    // so all we can hope to do is flushSync so that the count can be correct\n    // on a later call.\n    await client.flushSync({ collection_names: [this.normalize(namespace)] });\n    return true;\n  }\n\n  async performSimilaritySearch({\n    namespace = null,\n    input = \"\",\n    LLMConnector = null,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    if (!namespace || !input || !LLMConnector)\n      throw new Error(\"Invalid request to performSimilaritySearch.\");\n\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace))) {\n      return {\n        contextTexts: [],\n        sources: [],\n        message: \"Invalid query - no documents found for workspace!\",\n      };\n    }\n\n    const queryVector = await LLMConnector.embedTextInput(input);\n    const { contextTexts, sourceDocuments } = await this.similarityResponse({\n      client,\n      namespace,\n      queryVector,\n      similarityThreshold,\n      topN,\n      filterIdentifiers,\n    });\n\n    const sources = sourceDocuments.map((doc, i) => {\n      return { metadata: doc, text: contextTexts[i] };\n    });\n\n    return {\n      contextTexts,\n      sources: this.curateSources(sources),\n      message: false,\n    };\n  }\n\n  async similarityResponse({\n    client,\n    namespace,\n    queryVector,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    const result = {\n      contextTexts: [],\n      sourceDocuments: [],\n      scores: [],\n    };\n    const response = await client.search({\n      collection_name: this.normalize(namespace),\n      vectors: queryVector,\n      limit: topN,\n    });\n    response.results.forEach((match) => {\n      if (match.score < similarityThreshold) return;\n      if (filterIdentifiers.includes(sourceIdentifier(match.metadata))) {\n        this.logger(\n          `${this.name}: A source was filtered from context as its parent document is pinned.`\n        );\n        return;\n      }\n\n      result.contextTexts.push(match.metadata.text);\n      result.sourceDocuments.push({\n        ...match.metadata,\n        score: match.score,\n      });\n      result.scores.push(match.score);\n    });\n    return result;\n  }\n\n  async \"namespace-stats\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    if (!namespace) throw new Error(\"namespace required\");\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace)))\n      throw new Error(\"Namespace by that name does not exist.\");\n    const stats = await this.namespace(client, namespace);\n    return stats\n      ? stats\n      : { message: \"No stats were able to be fetched from DB for namespace\" };\n  }\n\n  async \"delete-namespace\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace)))\n      throw new Error(\"Namespace by that name does not exist.\");\n\n    const statistics = await this.namespace(client, namespace);\n    await this.deleteVectorsInNamespace(client, namespace);\n    const vectorCount = Number(statistics?.data?.row_count ?? 0);\n    return {\n      message: `Namespace ${namespace} was deleted along with ${vectorCount} vectors.`,\n    };\n  }\n\n  curateSources(sources = []) {\n    const documents = [];\n    for (const source of sources) {\n      const { metadata = {} } = source;\n      if (Object.keys(metadata).length > 0) {\n        documents.push({\n          ...metadata,\n          ...(source.text ? { text: source.text } : {}),\n        });\n      }\n    }\n    return documents;\n  }\n}\n\nmodule.exports.Milvus = Milvus;\n"
  },
  {
    "path": "server/utils/vectorDbProviders/pgvector/SETUP.md",
    "content": "# Setting up `PGVector` for AnythingLLM\n\nSetting up PGVector for anythingllm to use as your vector database is quite easy. At a minimum, you will need the following:\n\n- PostgreSQL v12+\n- [`pgvector`](https://github.com/pgvector/pgvector) extension installed on DB\n- User with DB table creation perms and READ access\n\n## Setup on Mac (example)\n\n### Install pgvector extension on PostgreSQL DB\n\n```bash\nbrew install postgresql\nbrew services start postgresql\nbrew install pgvector\n\n# assuming you have a database already set up + a user\npsql <database-name>\nCREATE EXTENSION vector;\n```\n\n### Set PG as your vector db\n\n_this can be done via the UI or by directly editing the `.env` file_\n\nFirst, obtain a valid connection string for the user, credentials, and db you want to target.\neg: `postgresql://dbuser:dbuserpass@localhost:5432/yourdb`\n\n> [!IMPORTANT]\n> If you have an existing table that you want to use as a vector database, AnythingLLM **requires** that the table be\n> at least minimally conform to the expected schema - this can be seen in the [index.js](./index.js) file.\n\n_optional_ - set a table name you wish to have AnythingLLM store vectors to. By default this is `anythingllm_vectors`\n\n## Common Questions\n\n### I cannot connect to the DB (Running AnythingLLM in Docker)\n\nIf you are running AnythingLLM in Docker, you will need to ensure that the DB is accessible from the container.\nIf you are running your DB in another Docker container **or** on the host machine, you will need to ensure that the container can access the DB.\n\n`localhost` will not work in this case as it will attempt to connect to the DB _inside the AnythingLLM container_ instead of the host machine or another container.\n\nYou will need to use the `host.docker.internal` (or `172.17.0.1` on Linux/Ubuntu) address.\n\n```\non Mac or Windows:\npostgresql://dbuser:dbuserpass@localhost:5432/yourdb => postgresql://dbuser:dbuserpass@host.docker.internal:5432/yourdb\n\non Linux:\npostgresql://dbuser:dbuserpass@localhost:5432/yourdb => postgresql://dbuser:dbuserpass@172.17.0.1:5432/yourdb\n```\n\n### Can I use an existing table as a vector database?\n\nYes, you can use an existing table as a vector database. However, AnythingLLM **requires** that the table be at least minimally conform to the expected schema - this can be seen in the [index.js](./index.js) file.\n\nIt is **absolutely critical** that the `embedding` column's `VECTOR(XXXX)` dimensions match the dimension of the embedder in AnythingLLM. The default embedding model is 384 dimensions. However, if you are using a custom embedder, you will need to ensure that the dimension value is set correctly.\n\n### Validate the connection to the database\n\nWhen setting the connection string in or table name via the AnythingLLM UI, the following validations will be attempted:\n\n- Validate the connection string\n- Validate the table name\n- Run test connection to ensure the table exists and is accessible by the connection string used\n- Check if the table name already exists and if so, validate that it is an embedding table with the correct schema\n\n### My embedding table is not present in the DB\n\nThe embedding storage table is created by AnythingLLM **on the first upsert** of a vector. If you have not yet embedding any documents, the table will not be present in the DB.\n\n### How do I reset my vector database?\n\n_at the workspace level in Settings > Vector Database_\n\nYou can use the \"Reset Vector Database\" button in the AnythingLLM UI to reset your vector database. This will drop all vectors within that workspace, but the table will remain in the DB.\n\n_reset the vector database at the db level_\n\nFor this, you will need to `DROP TABLE` from the command line or however you manage your DB. Once the table is dropped, it will be recreated by AnythingLLM on the next upsert.\n\n## Troubleshooting\n\n### Cannot connect to DB\n\n- Ensure the connection string is valid\n- Ensure the user has access to the database\n- Ensure the pgvector extension is installed\n\n### Cannot create table\n\n- Ensure the user has `CREATE TABLE` permissions\n\n### Cannot insert vector\n\n- Ensure the user has `INSERT` permissions in the database\n- Ensure the table has a dimension value set and this matches the dimension of the embedder in AnythingLLM\n- Ensure the table has a vector column set\n\n### Cannot query vector\n\n- Ensure the user has `SELECT` permissions in the database\n- Ensure the table has a vector column set\n- Ensure the table has a dimension value set and this matches the dimension of the embedder in AnythingLLM\n\n### \"type 'vector' does not exist\" issues with PGVector\n\nIf you are using the PGVector as your vector database, you may encounter an error similar to the following when embedding documents:\n\n```\ntype 'vector' does not exist\n```\n\nThis is due to the fact that the `vector` type is not installed on the PG database.\n\nFirst, follow the instructions in the [PGVector README](https://github.com/pgvector/pgvector#installation) to install the `vector` type on your database.\n\nThen, you will need to create the extension on the database. This can be done by running the following command:\n\n```bash\npsql <database-name>\nCREATE EXTENSION vector;\n```\n"
  },
  {
    "path": "server/utils/vectorDbProviders/pgvector/index.js",
    "content": "const pgsql = require(\"pg\");\nconst { toChunks, getEmbeddingEngineSelection } = require(\"../../helpers\");\nconst { TextSplitter } = require(\"../../TextSplitter\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst { sourceIdentifier } = require(\"../../chats\");\nconst { VectorDatabase } = require(\"../base\");\n\n/*\n Embedding Table Schema (table name defined by user)\n - id: UUID PRIMARY KEY\n - namespace: TEXT\n - embedding: vector(xxxx)\n - metadata: JSONB\n - created_at: TIMESTAMP\n*/\n\nclass PGVector extends VectorDatabase {\n  constructor() {\n    super();\n  }\n\n  get name() {\n    return \"PGVector\";\n  }\n\n  connectionTimeout = 30_000;\n  // Possible for this to be a user-configurable option in the future.\n  // Will require a handler per operator to ensure scores are normalized.\n  operator = {\n    l2: \"<->\",\n    innerProduct: \"<#>\",\n    cosine: \"<=>\",\n    l1: \"<+>\",\n    hamming: \"<~>\",\n    jaccard: \"<%>\",\n  };\n  getTablesSql =\n    \"SELECT * FROM pg_catalog.pg_tables WHERE schemaname = 'public'\";\n  getEmbeddingTableSchemaSql =\n    \"SELECT column_name,data_type FROM information_schema.columns WHERE table_name = $1\";\n  createExtensionSql = \"CREATE EXTENSION IF NOT EXISTS vector;\";\n\n  /**\n   * Get the table name for the PGVector database.\n   * - Defaults to \"anythingllm_vectors\" if no table name is provided.\n   * @returns {string}\n   */\n  static tableName() {\n    return process.env.PGVECTOR_TABLE_NAME || \"anythingllm_vectors\";\n  }\n\n  /**\n   * Get the connection string for the PGVector database.\n   * - Requires a connection string to be present in the environment variables.\n   * @returns {string | null}\n   */\n  static connectionString() {\n    return process.env.PGVECTOR_CONNECTION_STRING;\n  }\n\n  createTableSql(dimensions) {\n    return `CREATE TABLE IF NOT EXISTS \"${PGVector.tableName()}\" (id UUID PRIMARY KEY, namespace TEXT, embedding vector(${Number(dimensions)}), metadata JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`;\n  }\n\n  /**\n   * Recursively sanitize values intended for JSONB to prevent Postgres errors\n   * like \"unsupported Unicode escape sequence\". This primarily removes the\n   * NUL character (\\u0000) and other disallowed control characters from\n   * strings. Arrays and objects are traversed and sanitized deeply.\n   * @param {any} value\n   * @returns {any}\n   */\n  sanitizeForJsonb(value) {\n    // Fast path for null/undefined and primitives that do not need changes\n    if (value === null || value === undefined) return value;\n\n    // Strings: strip NUL and unsafe C0 control characters except common whitespace\n    if (typeof value === \"string\") {\n      // Build a sanitized string by excluding C0 control characters except\n      // horizontal tab (9), line feed (10), and carriage return (13).\n      let sanitized = \"\";\n      for (let i = 0; i < value.length; i++) {\n        const code = value.charCodeAt(i);\n        if (code === 9 || code === 10 || code === 13 || code >= 0x20) {\n          sanitized += value[i];\n        }\n      }\n      return sanitized;\n    }\n\n    // Arrays: sanitize each element\n    if (Array.isArray(value)) {\n      return value.map((item) => this.sanitizeForJsonb(item));\n    }\n\n    // Dates: keep as ISO string\n    if (value instanceof Date) {\n      return value.toISOString();\n    }\n\n    // Objects: sanitize each property value\n    if (typeof value === \"object\") {\n      const result = {};\n      for (const [k, v] of Object.entries(value)) {\n        result[k] = this.sanitizeForJsonb(v);\n      }\n      return result;\n    }\n\n    // Numbers, booleans, etc.\n    return value;\n  }\n\n  client(connectionString = null) {\n    return new pgsql.Client({\n      connectionString: connectionString || PGVector.connectionString(),\n    });\n  }\n\n  /**\n   * Validate the existing embedding table schema.\n   * @param {pgsql.Client} pgClient\n   * @param {string} tableName\n   * @returns {Promise<boolean>}\n   */\n  async validateExistingEmbeddingTableSchema(pgClient, tableName) {\n    const result = await pgClient.query(this.getEmbeddingTableSchemaSql, [\n      tableName,\n    ]);\n\n    // Minimum expected schema for an embedding table.\n    // Extra columns are allowed but the minimum exact columns are required\n    // to be present in the table.\n    const expectedSchema = [\n      {\n        column_name: \"id\",\n        expected: \"uuid\",\n        validation: function (dataType) {\n          return dataType.toLowerCase() === this.expected;\n        },\n      },\n      {\n        column_name: \"namespace\",\n        expected: \"text\",\n        validation: function (dataType) {\n          return dataType.toLowerCase() === this.expected;\n        },\n      },\n      {\n        column_name: \"embedding\",\n        expected: \"vector\",\n        validation: function (dataType) {\n          return !!dataType;\n        },\n      }, // just check if it exists\n      {\n        column_name: \"metadata\",\n        expected: \"jsonb\",\n        validation: function (dataType) {\n          return dataType.toLowerCase() === this.expected;\n        },\n      },\n      {\n        column_name: \"created_at\",\n        expected: \"timestamp\",\n        validation: function (dataType) {\n          return dataType.toLowerCase().includes(this.expected);\n        },\n      },\n    ];\n\n    if (result.rows.length === 0)\n      throw new Error(\n        `The table '${tableName}' was found but does not contain any columns or cannot be accessed by role. It cannot be used as an embedding table in AnythingLLM.`\n      );\n\n    for (const rowDef of expectedSchema) {\n      const column = result.rows.find(\n        (c) => c.column_name === rowDef.column_name\n      );\n      if (!column)\n        throw new Error(\n          `The column '${rowDef.column_name}' was expected but not found in the table '${tableName}'.`\n        );\n      if (!rowDef.validation(column.data_type))\n        throw new Error(\n          `Invalid data type for column: '${column.column_name}'. Got '${column.data_type}' but expected '${rowDef.expected}'`\n        );\n    }\n\n    this.logger(\n      `✅ The pgvector table '${tableName}' was found and meets the minimum expected schema for an embedding table.`\n    );\n    return true;\n  }\n\n  /**\n   * Validate the connection to the database and verify that the table does not already exist.\n   * so that anythingllm can manage the table directly.\n   *\n   * @param {{connectionString: string | null, tableName: string | null}} params\n   * @returns {Promise<{error: string | null, success: boolean}>}\n   */\n  static async validateConnection({\n    connectionString = null,\n    tableName = null,\n  }) {\n    if (!connectionString) throw new Error(\"No connection string provided\");\n    const instance = new PGVector();\n\n    try {\n      const timeoutPromise = new Promise((resolve) => {\n        setTimeout(() => {\n          resolve({\n            error: `Connection timeout (${(instance.connectionTimeout / 1000).toFixed(0)}s). Please check your connection string and try again.`,\n            success: false,\n          });\n        }, instance.connectionTimeout);\n      });\n\n      const connectionPromise = new Promise(async (resolve) => {\n        let pgClient = null;\n        try {\n          pgClient = instance.client(connectionString);\n          await pgClient.connect();\n          const result = await pgClient.query(instance.getTablesSql);\n\n          if (result.rows.length !== 0 && !!tableName) {\n            const tableExists = result.rows.some(\n              (row) => row.tablename === tableName\n            );\n            if (tableExists)\n              await instance.validateExistingEmbeddingTableSchema(\n                pgClient,\n                tableName\n              );\n          }\n          resolve({ error: null, success: true });\n        } catch (err) {\n          resolve({ error: err.message, success: false });\n        } finally {\n          if (pgClient) await pgClient.end();\n        }\n      });\n\n      // Race the connection attempt against the timeout\n      const result = await Promise.race([connectionPromise, timeoutPromise]);\n      return result;\n    } catch (err) {\n      instance.logger(\"Validation Error:\", err.message);\n      let readableError = err.message;\n      switch (true) {\n        case err.message.includes(\"ECONNREFUSED\"):\n          readableError =\n            \"The host could not be reached. Please check your connection string and try again.\";\n          break;\n        default:\n          break;\n      }\n      return { error: readableError, success: false };\n    }\n  }\n\n  /**\n   * Test the connection to the database directly.\n   * @returns {{error: string | null, success: boolean}}\n   */\n  async testConnectionToDB() {\n    try {\n      const pgClient = await this.connect();\n      await pgClient.query(this.getTablesSql);\n      await pgClient.end();\n      return { error: null, success: true };\n    } catch (err) {\n      return { error: err.message, success: false };\n    }\n  }\n\n  /**\n   * Connect to the database.\n   * - Throws an error if the connection string or table name is not provided.\n   * @returns {Promise<pgsql.Client>}\n   */\n  async connect() {\n    if (!PGVector.connectionString())\n      throw new Error(\"No connection string provided\");\n    if (!PGVector.tableName()) throw new Error(\"No table name provided\");\n\n    const client = this.client();\n    await client.connect();\n    return client;\n  }\n\n  /**\n   * Test the connection to the database with already set credentials via ENV\n   * @returns {{error: string | null, success: boolean}}\n   */\n  async heartbeat() {\n    return this.testConnectionToDB();\n  }\n\n  /**\n   * Check if the anythingllm embedding table exists in the database\n   * @returns {Promise<boolean>}\n   */\n  async dbTableExists() {\n    let connection = null;\n    try {\n      connection = await this.connect();\n      const tables = await connection.query(this.getTablesSql);\n      if (tables.rows.length === 0) return false;\n      const tableExists = tables.rows.some(\n        (row) => row.tablename === PGVector.tableName()\n      );\n      return !!tableExists;\n    } catch {\n      return false;\n    } finally {\n      if (connection) await connection.end();\n    }\n  }\n\n  async totalVectors() {\n    if (!(await this.dbTableExists())) return 0;\n    let connection = null;\n    try {\n      connection = await this.connect();\n      const result = await connection.query(\n        `SELECT COUNT(id) FROM \"${PGVector.tableName()}\"`\n      );\n      return result.rows[0].count;\n    } catch {\n      return 0;\n    } finally {\n      if (connection) await connection.end();\n    }\n  }\n\n  // Distance for cosine is just the distance for pgvector.\n  distanceToSimilarity(distance = null) {\n    if (distance === null || typeof distance !== \"number\") return 0.0;\n    if (distance >= 1.0) return 1;\n    if (distance < 0) return 1 - Math.abs(distance);\n    return 1 - distance;\n  }\n\n  async namespaceCount(namespace = null) {\n    if (!(await this.dbTableExists())) return 0;\n    let connection = null;\n    try {\n      connection = await this.connect();\n      const result = await connection.query(\n        `SELECT COUNT(id) FROM \"${PGVector.tableName()}\" WHERE namespace = $1`,\n        [namespace]\n      );\n      return result.rows[0].count;\n    } catch {\n      return 0;\n    } finally {\n      if (connection) await connection.end();\n    }\n  }\n\n  /**\n   * Performs a SimilaritySearch on a given PGVector namespace.\n   * @param {Object} params\n   * @param {pgsql.Client} params.client\n   * @param {string} params.namespace\n   * @param {number[]} params.queryVector\n   * @param {number} params.similarityThreshold\n   * @param {number} params.topN\n   * @param {string[]} params.filterIdentifiers\n   * @returns\n   */\n  async similarityResponse({\n    client,\n    namespace,\n    queryVector,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    const result = {\n      contextTexts: [],\n      sourceDocuments: [],\n      scores: [],\n    };\n\n    const embedding = `[${queryVector.map(Number).join(\",\")}]`;\n    const response = await client.query(\n      `SELECT embedding ${this.operator.cosine} $1 AS _distance, metadata FROM \"${PGVector.tableName()}\" WHERE namespace = $2 ORDER BY _distance ASC LIMIT $3`,\n      [embedding, namespace, topN]\n    );\n    response.rows.forEach((item) => {\n      if (this.distanceToSimilarity(item._distance) < similarityThreshold)\n        return;\n      if (filterIdentifiers.includes(sourceIdentifier(item.metadata))) {\n        this.logger(\n          \"A source was filtered from context as it's parent document is pinned.\"\n        );\n        return;\n      }\n\n      result.contextTexts.push(item.metadata.text);\n      result.sourceDocuments.push({\n        ...item.metadata,\n        score: this.distanceToSimilarity(item._distance),\n      });\n      result.scores.push(this.distanceToSimilarity(item._distance));\n    });\n\n    return result;\n  }\n\n  normalizeVector(vector) {\n    const magnitude = Math.sqrt(\n      vector.reduce((sum, val) => sum + val * val, 0)\n    );\n    if (magnitude === 0) return vector; // Avoid division by zero\n    return vector.map((val) => val / magnitude);\n  }\n\n  /**\n   * Update or create a collection in the database\n   * @param {Object} params\n   * @param {pgsql.Connection} params.connection\n   * @param {{id: number, vector: number[], metadata: Object}[]} params.submissions\n   * @param {string} params.namespace\n   * @param {number} params.dimensions\n   * @returns {Promise<boolean>}\n   */\n  async updateOrCreateCollection({\n    connection,\n    submissions,\n    namespace,\n    dimensions = 384,\n  }) {\n    await this.createTableIfNotExists(connection, dimensions);\n    this.logger(`Updating or creating collection ${namespace}`);\n\n    try {\n      // Create a transaction of all inserts\n      await connection.query(`BEGIN`);\n      for (const submission of submissions) {\n        const embedding = `[${submission.vector.map(Number).join(\",\")}]`; // stringify the vector for pgvector\n        const sanitizedMetadata = this.sanitizeForJsonb(submission.metadata);\n        await connection.query(\n          `INSERT INTO \"${PGVector.tableName()}\" (id, namespace, embedding, metadata) VALUES ($1, $2, $3, $4)`,\n          [submission.id, namespace, embedding, sanitizedMetadata]\n        );\n      }\n      this.logger(`Committing ${submissions.length} vectors to ${namespace}`);\n      await connection.query(`COMMIT`);\n    } catch (err) {\n      this.logger(\n        `Rolling back ${submissions.length} vectors to ${namespace}`,\n        err\n      );\n      await connection.query(`ROLLBACK`);\n    }\n    return true;\n  }\n\n  /**\n   * create a table if it doesn't exist\n   * @param {pgsql.Client} connection\n   * @param {number} dimensions\n   * @returns\n   */\n  async createTableIfNotExists(connection, dimensions = 384) {\n    this.logger(`Creating embedding table with ${dimensions} dimensions`);\n    await connection.query(this.createExtensionSql);\n    await connection.query(this.createTableSql(dimensions));\n    return true;\n  }\n\n  /**\n   * Get the namespace from the database\n   * @param {pgsql.Client} connection\n   * @param {string} namespace\n   * @returns {Promise<{name: string, vectorCount: number}>}\n   */\n  async namespace(connection, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace provided\");\n    const result = await connection.query(\n      `SELECT COUNT(id) FROM \"${PGVector.tableName()}\" WHERE namespace = $1`,\n      [namespace]\n    );\n    return { name: namespace, vectorCount: result.rows[0].count };\n  }\n\n  /**\n   * Check if the namespace exists in the database\n   * @param {string} namespace\n   * @returns {Promise<boolean>}\n   */\n  async hasNamespace(namespace = null) {\n    if (!namespace) throw new Error(\"No namespace provided\");\n    let connection = null;\n    try {\n      connection = await this.connect();\n      return await this.namespaceExists(connection, namespace);\n    } catch {\n      return false;\n    } finally {\n      if (connection) await connection.end();\n    }\n  }\n\n  /**\n   * Check if the namespace exists in the database\n   * @param {pgsql.Client} connection\n   * @param {string} namespace\n   * @returns {Promise<boolean>}\n   */\n  async namespaceExists(connection, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace provided\");\n    const result = await connection.query(\n      `SELECT COUNT(id) FROM \"${PGVector.tableName()}\" WHERE namespace = $1 LIMIT 1`,\n      [namespace]\n    );\n    return result.rows[0].count > 0;\n  }\n\n  /**\n   * Delete all vectors in the namespace\n   * @param {pgsql.Client} connection\n   * @param {string} namespace\n   * @returns {Promise<boolean>}\n   */\n  async deleteVectorsInNamespace(connection, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace provided\");\n    await connection.query(\n      `DELETE FROM \"${PGVector.tableName()}\" WHERE namespace = $1`,\n      [namespace]\n    );\n    return true;\n  }\n\n  async addDocumentToNamespace(\n    namespace,\n    documentData = {},\n    fullFilePath = null,\n    skipCache = false\n  ) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    const {\n      storeVectorResult,\n      cachedVectorInformation,\n    } = require(\"../../files\");\n    let connection = null;\n\n    try {\n      const { pageContent, docId, ...metadata } = documentData;\n      if (!pageContent || pageContent.length == 0) return false;\n      connection = await this.connect();\n\n      this.logger(\"Adding new vectorized document into namespace\", namespace);\n      if (!skipCache) {\n        const cacheResult = await cachedVectorInformation(fullFilePath);\n        let vectorDimensions;\n        if (cacheResult.exists) {\n          const { chunks } = cacheResult;\n          const documentVectors = [];\n          const submissions = [];\n\n          for (const chunk of chunks.flat()) {\n            if (!vectorDimensions) vectorDimensions = chunk.values.length;\n            const id = uuidv4();\n            const { id: _id, ...metadata } = chunk.metadata;\n            documentVectors.push({ docId, vectorId: id });\n            submissions.push({ id: id, vector: chunk.values, metadata });\n          }\n\n          await this.updateOrCreateCollection({\n            connection,\n            submissions,\n            namespace,\n            dimensions: vectorDimensions,\n          });\n          await DocumentVectors.bulkInsert(documentVectors);\n          return { vectorized: true, error: null };\n        }\n      }\n\n      // If we are here then we are going to embed and store a novel document.\n      // We have to do this manually as opposed to using LangChains `xyz.fromDocuments`\n      // because we then cannot atomically control our namespace to granularly find/remove documents\n      // from vectordb.\n      const { SystemSettings } = require(\"../../../models/systemSettings\");\n      const EmbedderEngine = getEmbeddingEngineSelection();\n      const textSplitter = new TextSplitter({\n        chunkSize: TextSplitter.determineMaxChunkSize(\n          await SystemSettings.getValueOrFallback({\n            label: \"text_splitter_chunk_size\",\n          }),\n          EmbedderEngine?.embeddingMaxChunkLength\n        ),\n        chunkOverlap: await SystemSettings.getValueOrFallback(\n          { label: \"text_splitter_chunk_overlap\" },\n          20\n        ),\n        chunkHeaderMeta: TextSplitter.buildHeaderMeta(metadata),\n        chunkPrefix: EmbedderEngine?.embeddingPrefix,\n      });\n      const textChunks = await textSplitter.splitText(pageContent);\n\n      this.logger(\"Snippets created from document:\", textChunks.length);\n      const documentVectors = [];\n      const vectors = [];\n      const submissions = [];\n      const vectorValues = await EmbedderEngine.embedChunks(textChunks);\n      let vectorDimensions;\n\n      if (!!vectorValues && vectorValues.length > 0) {\n        for (const [i, vector] of vectorValues.entries()) {\n          if (!vectorDimensions) vectorDimensions = vector.length;\n          const vectorRecord = {\n            id: uuidv4(),\n            values: vector,\n            metadata: { ...metadata, text: textChunks[i] },\n          };\n\n          vectors.push(vectorRecord);\n          submissions.push({\n            id: vectorRecord.id,\n            vector: vectorRecord.values,\n            metadata: vectorRecord.metadata,\n          });\n          documentVectors.push({ docId, vectorId: vectorRecord.id });\n        }\n      } else {\n        throw new Error(\n          \"Could not embed document chunks! This document will not be recorded.\"\n        );\n      }\n\n      if (vectors.length > 0) {\n        const chunks = [];\n        for (const chunk of toChunks(vectors, 500)) chunks.push(chunk);\n\n        this.logger(\"Inserting vectorized chunks into PGVector collection.\");\n        await this.updateOrCreateCollection({\n          connection,\n          submissions,\n          namespace,\n          dimensions: vectorDimensions,\n        });\n        await storeVectorResult(chunks, fullFilePath);\n      }\n\n      await DocumentVectors.bulkInsert(documentVectors);\n      return { vectorized: true, error: null };\n    } catch (err) {\n      this.logger(\"addDocumentToNamespace\", err.message);\n      return { vectorized: false, error: err.message };\n    } finally {\n      if (connection) await connection.end();\n    }\n  }\n\n  /**\n   * Delete a document from the namespace\n   * @param {string} namespace\n   * @param {string} docId\n   * @returns {Promise<boolean>}\n   */\n  async deleteDocumentFromNamespace(namespace, docId) {\n    if (!namespace) throw new Error(\"No namespace provided\");\n    if (!docId) throw new Error(\"No docId provided\");\n\n    let connection = null;\n    try {\n      connection = await this.connect();\n      const exists = await this.namespaceExists(connection, namespace);\n      if (!exists)\n        throw new Error(\n          `PGVector:deleteDocumentFromNamespace - namespace ${namespace} does not exist.`\n        );\n\n      const { DocumentVectors } = require(\"../../../models/vectors\");\n      const vectorIds = (await DocumentVectors.where({ docId })).map(\n        (record) => record.vectorId\n      );\n      if (vectorIds.length === 0) return;\n\n      try {\n        await connection.query(`BEGIN`);\n        for (const vectorId of vectorIds)\n          await connection.query(\n            `DELETE FROM \"${PGVector.tableName()}\" WHERE id = $1`,\n            [vectorId]\n          );\n        await connection.query(`COMMIT`);\n      } catch (err) {\n        await connection.query(`ROLLBACK`);\n        throw err;\n      }\n\n      this.logger(\n        `Deleted ${vectorIds.length} vectors from namespace ${namespace}`\n      );\n      return true;\n    } catch (err) {\n      this.logger(\n        `Error deleting document from namespace ${namespace}: ${err.message}`\n      );\n      return false;\n    } finally {\n      if (connection) await connection.end();\n    }\n  }\n\n  async performSimilaritySearch({\n    namespace = null,\n    input = \"\",\n    LLMConnector = null,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    let connection = null;\n    if (!namespace || !input || !LLMConnector)\n      throw new Error(\"Invalid request to performSimilaritySearch.\");\n\n    try {\n      connection = await this.connect();\n      const exists = await this.namespaceExists(connection, namespace);\n      if (!exists) {\n        this.logger(\n          `The namespace ${namespace} does not exist or has no vectors. Returning empty results.`\n        );\n        return {\n          contextTexts: [],\n          sources: [],\n          message: null,\n        };\n      }\n\n      const queryVector = await LLMConnector.embedTextInput(input);\n      const result = await this.similarityResponse({\n        client: connection,\n        namespace,\n        queryVector,\n        similarityThreshold,\n        topN,\n        filterIdentifiers,\n      });\n\n      const { contextTexts, sourceDocuments } = result;\n      const sources = sourceDocuments.map((metadata, i) => {\n        return { metadata: { ...metadata, text: contextTexts[i] } };\n      });\n      return {\n        contextTexts,\n        sources: this.curateSources(sources),\n        message: false,\n      };\n    } catch (err) {\n      return { error: err.message, success: false };\n    } finally {\n      if (connection) await connection.end();\n    }\n  }\n\n  async \"namespace-stats\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    if (!namespace) throw new Error(\"namespace required\");\n    if (!(await this.dbTableExists()))\n      return { message: \"No table found in database\" };\n\n    let connection = null;\n    try {\n      connection = await this.connect();\n      if (!(await this.namespaceExists(connection, namespace)))\n        throw new Error(\"Namespace by that name does not exist.\");\n      const stats = await this.namespace(connection, namespace);\n      return stats\n        ? stats\n        : { message: \"No stats were able to be fetched from DB for namespace\" };\n    } catch (err) {\n      return {\n        message: `Error fetching stats for namespace ${namespace}: ${err.message}`,\n      };\n    } finally {\n      if (connection) await connection.end();\n    }\n  }\n\n  async \"delete-namespace\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    if (!namespace) throw new Error(\"No namespace provided\");\n\n    let connection = null;\n    try {\n      const existingCount = await this.namespaceCount(namespace);\n      if (existingCount === 0)\n        return {\n          message: `Namespace ${namespace} does not exist or has no vectors.`,\n        };\n\n      connection = await this.connect();\n      await this.deleteVectorsInNamespace(connection, namespace);\n      return {\n        message: `Namespace ${namespace} was deleted along with ${existingCount} vectors.`,\n      };\n    } catch (err) {\n      return {\n        message: `Error deleting namespace ${namespace}: ${err.message}`,\n      };\n    } finally {\n      if (connection) await connection.end();\n    }\n  }\n\n  /**\n   * Reset the entire vector database table associated with anythingllm\n   * @returns {Promise<{reset: boolean}>}\n   */\n  async reset() {\n    let connection = null;\n    try {\n      connection = await this.connect();\n      await connection.query(`DROP TABLE IF EXISTS \"${PGVector.tableName()}\"`);\n      return { reset: true };\n    } catch {\n      return { reset: false };\n    } finally {\n      if (connection) await connection.end();\n    }\n  }\n\n  curateSources(sources = []) {\n    const documents = [];\n    for (const source of sources) {\n      const { text, vector: _v, _distance: _d, ...rest } = source;\n      const metadata = rest.hasOwnProperty(\"metadata\") ? rest.metadata : rest;\n      if (Object.keys(metadata).length > 0) {\n        documents.push({\n          ...metadata,\n          ...(text ? { text } : {}),\n        });\n      }\n    }\n\n    return documents;\n  }\n}\n\nmodule.exports.PGVector = PGVector;\n"
  },
  {
    "path": "server/utils/vectorDbProviders/pinecone/PINECONE_SETUP.md",
    "content": "# How to setup Pinecone Vector Database for AnythingLLM\n\n[Official Pinecone Docs](https://docs.pinecone.io/docs/overview) for reference.\n\n### How to get started\n\n**Requirements**\n\n- Pinecone account with index that allows namespaces.\n\n**Note:** [Namespaces are not supported in `gcp-starter` environments](https://docs.pinecone.io/docs/namespaces) and are required to work with AnythingLLM.\n\n**Instructions**\n\n- Create an index on your Pinecone account. Name can be anything eg: `my-primary-index`\n- Metric `cosine`\n- Dimensions `1536` since we use OpenAI for embeddings\n- 1 pod, all other default settings are fine.\n\n```\nVECTOR_DB=\"pinecone\"\nPINECONE_API_KEY=sklive-123xyz\nPINECONE_INDEX=my-primary-index # the value from the first instruction!\n```\n"
  },
  {
    "path": "server/utils/vectorDbProviders/pinecone/index.js",
    "content": "const { Pinecone } = require(\"@pinecone-database/pinecone\");\nconst { TextSplitter } = require(\"../../TextSplitter\");\nconst { SystemSettings } = require(\"../../../models/systemSettings\");\nconst { storeVectorResult, cachedVectorInformation } = require(\"../../files\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst { toChunks, getEmbeddingEngineSelection } = require(\"../../helpers\");\nconst { sourceIdentifier } = require(\"../../chats\");\nconst { VectorDatabase } = require(\"../base\");\n\nclass PineconeDB extends VectorDatabase {\n  constructor() {\n    super();\n  }\n\n  get name() {\n    return \"Pinecone\";\n  }\n\n  async connect() {\n    if (process.env.VECTOR_DB !== \"pinecone\")\n      throw new Error(\"Pinecone::Invalid ENV settings\");\n\n    const client = new Pinecone({\n      apiKey: process.env.PINECONE_API_KEY,\n    });\n\n    const pineconeIndex = client.Index(process.env.PINECONE_INDEX);\n    const { status } = await client.describeIndex(process.env.PINECONE_INDEX);\n\n    if (!status.ready) throw new Error(\"Pinecone::Index not ready.\");\n    return { client, pineconeIndex, indexName: process.env.PINECONE_INDEX };\n  }\n\n  async totalVectors() {\n    const { pineconeIndex } = await this.connect();\n    const { namespaces } = await pineconeIndex.describeIndexStats();\n\n    return Object.values(namespaces).reduce(\n      (a, b) => a + (b?.recordCount || 0),\n      0\n    );\n  }\n\n  async namespaceCount(_namespace = null) {\n    const { pineconeIndex } = await this.connect();\n    const namespace = await this.namespace(pineconeIndex, _namespace);\n    return namespace?.recordCount || 0;\n  }\n\n  async similarityResponse({\n    client,\n    namespace,\n    queryVector,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    const result = {\n      contextTexts: [],\n      sourceDocuments: [],\n      scores: [],\n    };\n\n    const pineconeNamespace = client.namespace(namespace);\n    const response = await pineconeNamespace.query({\n      vector: queryVector,\n      topK: topN,\n      includeMetadata: true,\n    });\n\n    response.matches.forEach((match) => {\n      if (match.score < similarityThreshold) return;\n      if (filterIdentifiers.includes(sourceIdentifier(match.metadata))) {\n        this.logger(\n          \"Pinecone: A source was filtered from context as it's parent document is pinned.\"\n        );\n        return;\n      }\n\n      result.contextTexts.push(match.metadata.text);\n      result.sourceDocuments.push({\n        ...match.metadata,\n        score: match.score,\n      });\n      result.scores.push(match.score);\n    });\n\n    return result;\n  }\n\n  async namespace(index, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    const { namespaces } = await index.describeIndexStats();\n    return namespaces.hasOwnProperty(namespace) ? namespaces[namespace] : null;\n  }\n\n  async hasNamespace(namespace = null) {\n    if (!namespace) return false;\n    const { pineconeIndex } = await this.connect();\n    return await this.namespaceExists(pineconeIndex, namespace);\n  }\n\n  async namespaceExists(index, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    const { namespaces } = await index.describeIndexStats();\n    return namespaces.hasOwnProperty(namespace);\n  }\n\n  async deleteVectorsInNamespace(index, namespace = null) {\n    const pineconeNamespace = index.namespace(namespace);\n    await pineconeNamespace.deleteAll();\n    return true;\n  }\n\n  async addDocumentToNamespace(\n    namespace,\n    documentData = {},\n    fullFilePath = null,\n    skipCache = false\n  ) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    try {\n      const { pageContent, docId, ...metadata } = documentData;\n      if (!pageContent || pageContent.length == 0) return false;\n\n      this.logger(\"Adding new vectorized document into namespace\", namespace);\n      if (!skipCache) {\n        const cacheResult = await cachedVectorInformation(fullFilePath);\n        if (cacheResult.exists) {\n          const { pineconeIndex } = await this.connect();\n          const pineconeNamespace = pineconeIndex.namespace(namespace);\n          const { chunks } = cacheResult;\n          const documentVectors = [];\n\n          for (const chunk of chunks) {\n            // Before sending to Pinecone and saving the records to our db\n            // we need to assign the id of each chunk that is stored in the cached file.\n            const newChunks = chunk.map((chunk) => {\n              const id = uuidv4();\n              documentVectors.push({ docId, vectorId: id });\n              return { ...chunk, id };\n            });\n            await pineconeNamespace.upsert([...newChunks]);\n          }\n\n          await DocumentVectors.bulkInsert(documentVectors);\n          return { vectorized: true, error: null };\n        }\n      }\n\n      // If we are here then we are going to embed and store a novel document.\n      // We have to do this manually as opposed to using LangChains `PineconeStore.fromDocuments`\n      // because we then cannot atomically control our namespace to granularly find/remove documents\n      // from vectordb.\n      // https://github.com/hwchase17/langchainjs/blob/2def486af734c0ca87285a48f1a04c057ab74bdf/langchain/src/vectorstores/pinecone.ts#L167\n      const EmbedderEngine = getEmbeddingEngineSelection();\n      const textSplitter = new TextSplitter({\n        chunkSize: TextSplitter.determineMaxChunkSize(\n          await SystemSettings.getValueOrFallback({\n            label: \"text_splitter_chunk_size\",\n          }),\n          EmbedderEngine?.embeddingMaxChunkLength\n        ),\n        chunkOverlap: await SystemSettings.getValueOrFallback(\n          { label: \"text_splitter_chunk_overlap\" },\n          20\n        ),\n        chunkHeaderMeta: TextSplitter.buildHeaderMeta(metadata),\n        chunkPrefix: EmbedderEngine?.embeddingPrefix,\n      });\n      const textChunks = await textSplitter.splitText(pageContent);\n\n      this.logger(\"Snippets created from document:\", textChunks.length);\n      const documentVectors = [];\n      const vectors = [];\n      const vectorValues = await EmbedderEngine.embedChunks(textChunks);\n\n      if (!!vectorValues && vectorValues.length > 0) {\n        for (const [i, vector] of vectorValues.entries()) {\n          const vectorRecord = {\n            id: uuidv4(),\n            values: vector,\n            // [DO NOT REMOVE]\n            // LangChain will be unable to find your text if you embed manually and dont include the `text` key.\n            // https://github.com/hwchase17/langchainjs/blob/2def486af734c0ca87285a48f1a04c057ab74bdf/langchain/src/vectorstores/pinecone.ts#L64\n            metadata: { ...metadata, text: textChunks[i] },\n          };\n\n          vectors.push(vectorRecord);\n          documentVectors.push({ docId, vectorId: vectorRecord.id });\n        }\n      } else {\n        throw new Error(\n          \"Could not embed document chunks! This document will not be recorded.\"\n        );\n      }\n\n      if (vectors.length > 0) {\n        const chunks = [];\n        const { pineconeIndex } = await this.connect();\n        const pineconeNamespace = pineconeIndex.namespace(namespace);\n        this.logger(\"Inserting vectorized chunks into Pinecone.\");\n        for (const chunk of toChunks(vectors, 100)) {\n          chunks.push(chunk);\n          await pineconeNamespace.upsert([...chunk]);\n        }\n        await storeVectorResult(chunks, fullFilePath);\n      }\n\n      await DocumentVectors.bulkInsert(documentVectors);\n      return { vectorized: true, error: null };\n    } catch (e) {\n      this.logger(\"addDocumentToNamespace\", e.message);\n      return { vectorized: false, error: e.message };\n    }\n  }\n\n  async deleteDocumentFromNamespace(namespace, docId) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    const { pineconeIndex } = await this.connect();\n    if (!(await this.namespaceExists(pineconeIndex, namespace))) return;\n\n    const knownDocuments = await DocumentVectors.where({ docId });\n    if (knownDocuments.length === 0) return;\n\n    const vectorIds = knownDocuments.map((doc) => doc.vectorId);\n\n    const pineconeNamespace = pineconeIndex.namespace(namespace);\n    for (const batchOfVectorIds of toChunks(vectorIds, 1000)) {\n      await pineconeNamespace.deleteMany(batchOfVectorIds);\n    }\n\n    const indexes = knownDocuments.map((doc) => doc.id);\n    await DocumentVectors.deleteIds(indexes);\n    return true;\n  }\n\n  async \"namespace-stats\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    if (!namespace) throw new Error(\"namespace required\");\n    const { pineconeIndex } = await this.connect();\n    if (!(await this.namespaceExists(pineconeIndex, namespace)))\n      throw new Error(\"Namespace by that name does not exist.\");\n    const stats = await this.namespace(pineconeIndex, namespace);\n    return stats\n      ? stats\n      : { message: \"No stats were able to be fetched from DB\" };\n  }\n\n  async \"delete-namespace\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    const { pineconeIndex } = await this.connect();\n    if (!(await this.namespaceExists(pineconeIndex, namespace)))\n      throw new Error(\"Namespace by that name does not exist.\");\n\n    const details = await this.namespace(pineconeIndex, namespace);\n    await this.deleteVectorsInNamespace(pineconeIndex, namespace);\n    return {\n      message: `Namespace ${namespace} was deleted along with ${details.vectorCount} vectors.`,\n    };\n  }\n\n  async performSimilaritySearch({\n    namespace = null,\n    input = \"\",\n    LLMConnector = null,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    if (!namespace || !input || !LLMConnector)\n      throw new Error(\"Invalid request to performSimilaritySearch.\");\n\n    const { pineconeIndex } = await this.connect();\n    if (!(await this.namespaceExists(pineconeIndex, namespace)))\n      throw new Error(\n        \"Invalid namespace - has it been collected and populated yet?\"\n      );\n\n    const queryVector = await LLMConnector.embedTextInput(input);\n    const { contextTexts, sourceDocuments } = await this.similarityResponse({\n      client: pineconeIndex,\n      namespace,\n      queryVector,\n      similarityThreshold,\n      topN,\n      filterIdentifiers,\n    });\n\n    const sources = sourceDocuments.map((doc, i) => {\n      return { metadata: doc, text: contextTexts[i] };\n    });\n    return {\n      contextTexts,\n      sources: this.curateSources(sources),\n      message: false,\n    };\n  }\n\n  curateSources(sources = []) {\n    const documents = [];\n    for (const source of sources) {\n      const { metadata = {} } = source;\n      if (Object.keys(metadata).length > 0) {\n        documents.push({\n          ...metadata,\n          ...(source.hasOwnProperty(\"pageContent\")\n            ? { text: source.pageContent }\n            : {}),\n        });\n      }\n    }\n    return documents;\n  }\n}\n\nmodule.exports.Pinecone = PineconeDB;\n"
  },
  {
    "path": "server/utils/vectorDbProviders/qdrant/QDRANT_SETUP.md",
    "content": "# How to setup a local (or cloud) QDrant Vector Database\n\n[Get a QDrant Cloud instance](https://cloud.qdrant.io/).\n[Set up QDrant locally on Docker](https://github.com/qdrant/qdrant/blob/master/docs/QUICK_START.md).\n\nFill out the variables in the \"Vector Database\" tab of settings. Select Qdrant as your provider and fill out the appropriate fields\nwith the information from either of the above steps.\n\n### How to get started _Development mode only_\n\nAfter setting up either the Qdrant cloud or local dockerized instance you just need to set these variable in `.env.development` or defined them at runtime via the UI.\n\n```\n# VECTOR_DB=\"qdrant\"\n# QDRANT_ENDPOINT=\"https://<YOUR_CLOUD_INSTANCE_URL>.qdrant.io:6333\"\n# QDRANT_API_KEY=\"abc...123xyz\"\n```\n"
  },
  {
    "path": "server/utils/vectorDbProviders/qdrant/index.js",
    "content": "const { QdrantClient } = require(\"@qdrant/js-client-rest\");\nconst { TextSplitter } = require(\"../../TextSplitter\");\nconst { SystemSettings } = require(\"../../../models/systemSettings\");\nconst { storeVectorResult, cachedVectorInformation } = require(\"../../files\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst { toChunks, getEmbeddingEngineSelection } = require(\"../../helpers\");\nconst { sourceIdentifier } = require(\"../../chats\");\nconst { VectorDatabase } = require(\"../base\");\n\nclass QDrant extends VectorDatabase {\n  constructor() {\n    super();\n  }\n\n  get name() {\n    return \"QDrant\";\n  }\n\n  async connect() {\n    if (process.env.VECTOR_DB !== \"qdrant\")\n      throw new Error(\"QDrant::Invalid ENV settings\");\n\n    const client = new QdrantClient({\n      url: process.env.QDRANT_ENDPOINT,\n      ...(process.env.QDRANT_API_KEY\n        ? { apiKey: process.env.QDRANT_API_KEY }\n        : {}),\n    });\n\n    const isAlive = (await client.api(\"cluster\")?.clusterStatus())?.ok || false;\n    if (!isAlive)\n      throw new Error(\n        \"QDrant::Invalid Heartbeat received - is the instance online?\"\n      );\n\n    return { client };\n  }\n\n  async heartbeat() {\n    await this.connect();\n    return { heartbeat: Number(new Date()) };\n  }\n\n  async totalVectors() {\n    const { client } = await this.connect();\n    const { collections } = await client.getCollections();\n    var totalVectors = 0;\n    for (const collection of collections) {\n      if (!collection || !collection.name) continue;\n      totalVectors +=\n        (await this.namespace(client, collection.name))?.vectorCount || 0;\n    }\n    return totalVectors;\n  }\n\n  async namespaceCount(_namespace = null) {\n    const { client } = await this.connect();\n    const namespace = await this.namespace(client, _namespace);\n    return namespace?.vectorCount || 0;\n  }\n\n  async similarityResponse({\n    client,\n    namespace,\n    queryVector,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    const result = {\n      contextTexts: [],\n      sourceDocuments: [],\n      scores: [],\n    };\n\n    const responses = await client.search(namespace, {\n      vector: queryVector,\n      limit: topN,\n      with_payload: true,\n    });\n\n    responses.forEach((response) => {\n      if (response.score < similarityThreshold) return;\n      if (filterIdentifiers.includes(sourceIdentifier(response?.payload))) {\n        this.logger(\n          \"QDrant: A source was filtered from context as it's parent document is pinned.\"\n        );\n        return;\n      }\n\n      result.contextTexts.push(response?.payload?.text || \"\");\n      result.sourceDocuments.push({\n        ...(response?.payload || {}),\n        id: response.id,\n        score: response.score,\n      });\n      result.scores.push(response.score);\n    });\n\n    return result;\n  }\n\n  async namespace(client, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    const collection = await client.getCollection(namespace).catch(() => null);\n    if (!collection) return null;\n\n    return {\n      name: namespace,\n      ...collection,\n      vectorCount: (await client.count(namespace, { exact: true })).count,\n    };\n  }\n\n  async hasNamespace(namespace = null) {\n    if (!namespace) return false;\n    const { client } = await this.connect();\n    return await this.namespaceExists(client, namespace);\n  }\n\n  async namespaceExists(client, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    const collection = await client.getCollection(namespace).catch((e) => {\n      this.logger(\"namespaceExists\", e.message);\n      return null;\n    });\n    return !!collection;\n  }\n\n  async deleteVectorsInNamespace(client, namespace = null) {\n    await client.deleteCollection(namespace);\n    return true;\n  }\n\n  // QDrant requires a dimension aspect for collection creation\n  // we pass this in from the first chunk to infer the dimensions like other\n  // providers do.\n  async getOrCreateCollection(client, namespace, dimensions = null) {\n    if (await this.namespaceExists(client, namespace)) {\n      return await client.getCollection(namespace);\n    }\n    if (!dimensions)\n      throw new Error(\n        `Qdrant:getOrCreateCollection Unable to infer vector dimension from input. Open an issue on GitHub for support.`\n      );\n    await client.createCollection(namespace, {\n      vectors: {\n        size: dimensions,\n        distance: \"Cosine\",\n      },\n    });\n    return await client.getCollection(namespace);\n  }\n\n  async addDocumentToNamespace(\n    namespace,\n    documentData = {},\n    fullFilePath = null,\n    skipCache = false\n  ) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    try {\n      let vectorDimension = null;\n      const { pageContent, docId, ...metadata } = documentData;\n      if (!pageContent || pageContent.length == 0) return false;\n\n      this.logger(\"Adding new vectorized document into namespace\", namespace);\n      if (!skipCache) {\n        const cacheResult = await cachedVectorInformation(fullFilePath);\n        if (cacheResult.exists) {\n          const { client } = await this.connect();\n          const { chunks } = cacheResult;\n          const documentVectors = [];\n          vectorDimension =\n            chunks[0][0]?.vector?.length ??\n            chunks[0][0]?.values?.length ??\n            null;\n\n          const collection = await this.getOrCreateCollection(\n            client,\n            namespace,\n            vectorDimension\n          );\n          if (!collection)\n            throw new Error(\"Failed to create new QDrant collection!\", {\n              namespace,\n            });\n\n          for (const chunk of chunks) {\n            const submission = {\n              ids: [],\n              vectors: [],\n              payloads: [],\n            };\n\n            // Before sending to Qdrant and saving the records to our db\n            // we need to assign the id of each chunk that is stored in the cached file.\n            // The id property must be defined or else it will be unable to be managed by ALLM.\n            chunk.forEach((chunk) => {\n              const id = uuidv4();\n              if (chunk?.payload?.hasOwnProperty(\"id\")) {\n                const { id: _id, ...payload } = chunk.payload;\n                documentVectors.push({ docId, vectorId: id });\n                submission.ids.push(id);\n                submission.vectors.push(chunk.vector);\n                submission.payloads.push(payload);\n              } else {\n                console.error(\n                  \"The 'id' property is not defined in chunk.payload - it will be omitted from being inserted in QDrant collection.\"\n                );\n              }\n            });\n\n            const additionResult = await client.upsert(namespace, {\n              wait: true,\n              batch: { ...submission },\n            });\n            if (additionResult?.status !== \"completed\")\n              throw new Error(\"Error embedding into QDrant\", additionResult);\n          }\n\n          await DocumentVectors.bulkInsert(documentVectors);\n          return { vectorized: true, error: null };\n        }\n      }\n\n      // If we are here then we are going to embed and store a novel document.\n      // We have to do this manually as opposed to using LangChains `Qdrant.fromDocuments`\n      // because we then cannot atomically control our namespace to granularly find/remove documents\n      // from vectordb.\n      const EmbedderEngine = getEmbeddingEngineSelection();\n      const textSplitter = new TextSplitter({\n        chunkSize: TextSplitter.determineMaxChunkSize(\n          await SystemSettings.getValueOrFallback({\n            label: \"text_splitter_chunk_size\",\n          }),\n          EmbedderEngine?.embeddingMaxChunkLength\n        ),\n        chunkOverlap: await SystemSettings.getValueOrFallback(\n          { label: \"text_splitter_chunk_overlap\" },\n          20\n        ),\n        chunkHeaderMeta: TextSplitter.buildHeaderMeta(metadata),\n        chunkPrefix: EmbedderEngine?.embeddingPrefix,\n      });\n      const textChunks = await textSplitter.splitText(pageContent);\n\n      this.logger(\"Snippets created from document:\", textChunks.length);\n      const documentVectors = [];\n      const vectors = [];\n      const vectorValues = await EmbedderEngine.embedChunks(textChunks);\n      const submission = {\n        ids: [],\n        vectors: [],\n        payloads: [],\n      };\n\n      if (!!vectorValues && vectorValues.length > 0) {\n        for (const [i, vector] of vectorValues.entries()) {\n          if (!vectorDimension) vectorDimension = vector.length;\n          const vectorRecord = {\n            id: uuidv4(),\n            vector: vector,\n            // [DO NOT REMOVE]\n            // LangChain will be unable to find your text if you embed manually and dont include the `text` key.\n            // https://github.com/hwchase17/langchainjs/blob/2def486af734c0ca87285a48f1a04c057ab74bdf/langchain/src/vectorstores/pinecone.ts#L64\n            payload: { ...metadata, text: textChunks[i] },\n          };\n\n          submission.ids.push(vectorRecord.id);\n          submission.vectors.push(vectorRecord.vector);\n          submission.payloads.push(vectorRecord.payload);\n\n          vectors.push(vectorRecord);\n          documentVectors.push({ docId, vectorId: vectorRecord.id });\n        }\n      } else {\n        throw new Error(\n          \"Could not embed document chunks! This document will not be recorded.\"\n        );\n      }\n\n      const { client } = await this.connect();\n      const collection = await this.getOrCreateCollection(\n        client,\n        namespace,\n        vectorDimension\n      );\n      if (!collection)\n        throw new Error(\"Failed to create new QDrant collection!\", {\n          namespace,\n        });\n\n      if (vectors.length > 0) {\n        const chunks = [];\n\n        this.logger(\"Inserting vectorized chunks into QDrant collection.\");\n        for (const chunk of toChunks(vectors, 500)) {\n          const batchIds = [],\n            batchVectors = [],\n            batchPayloads = [];\n          chunks.push(chunk);\n          chunk.forEach((v) => {\n            batchIds.push(v.id);\n            batchVectors.push(v.vector);\n            batchPayloads.push(v.payload);\n          });\n\n          const additionResult = await client.upsert(namespace, {\n            wait: true,\n            batch: {\n              ids: batchIds,\n              vectors: batchVectors,\n              payloads: batchPayloads,\n            },\n          });\n          if (additionResult?.status !== \"completed\")\n            throw new Error(\"Error embedding into QDrant\", additionResult);\n        }\n\n        await storeVectorResult(chunks, fullFilePath);\n      }\n\n      await DocumentVectors.bulkInsert(documentVectors);\n      return { vectorized: true, error: null };\n    } catch (e) {\n      this.logger(\"addDocumentToNamespace\", e.message);\n      return { vectorized: false, error: e.message };\n    }\n  }\n\n  async deleteDocumentFromNamespace(namespace, docId) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace))) return;\n\n    const knownDocuments = await DocumentVectors.where({ docId });\n    if (knownDocuments.length === 0) return;\n\n    const vectorIds = knownDocuments.map((doc) => doc.vectorId);\n    await client.delete(namespace, {\n      wait: true,\n      points: vectorIds,\n    });\n\n    const indexes = knownDocuments.map((doc) => doc.id);\n    await DocumentVectors.deleteIds(indexes);\n    return true;\n  }\n\n  async performSimilaritySearch({\n    namespace = null,\n    input = \"\",\n    LLMConnector = null,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    if (!namespace || !input || !LLMConnector)\n      throw new Error(\"Invalid request to performSimilaritySearch.\");\n\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace))) {\n      return {\n        contextTexts: [],\n        sources: [],\n        message: \"Invalid query - no documents found for workspace!\",\n      };\n    }\n\n    const queryVector = await LLMConnector.embedTextInput(input);\n    const { contextTexts, sourceDocuments } = await this.similarityResponse({\n      client,\n      namespace,\n      queryVector,\n      similarityThreshold,\n      topN,\n      filterIdentifiers,\n    });\n\n    const sources = sourceDocuments.map((metadata, i) => {\n      return { ...metadata, text: contextTexts[i] };\n    });\n    return {\n      contextTexts,\n      sources: this.curateSources(sources),\n      message: false,\n    };\n  }\n\n  async \"namespace-stats\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    if (!namespace) throw new Error(\"namespace required\");\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace)))\n      throw new Error(\"Namespace by that name does not exist.\");\n    const stats = await this.namespace(client, namespace);\n    return stats\n      ? stats\n      : { message: \"No stats were able to be fetched from DB for namespace\" };\n  }\n\n  async \"delete-namespace\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace)))\n      throw new Error(\"Namespace by that name does not exist.\");\n\n    const details = await this.namespace(client, namespace);\n    await this.deleteVectorsInNamespace(client, namespace);\n    return {\n      message: `Namespace ${namespace} was deleted along with ${details?.vectorCount} vectors.`,\n    };\n  }\n\n  async reset() {\n    const { client } = await this.connect();\n    const response = await client.getCollections();\n    for (const collection of response.collections) {\n      await client.deleteCollection(collection.name);\n    }\n    return { reset: true };\n  }\n\n  curateSources(sources = []) {\n    const documents = [];\n    for (const source of sources) {\n      if (Object.keys(source).length > 0) {\n        const metadata = source.hasOwnProperty(\"metadata\")\n          ? source.metadata\n          : source;\n        documents.push({\n          ...metadata,\n        });\n      }\n    }\n\n    return documents;\n  }\n}\n\nmodule.exports.QDrant = QDrant;\n"
  },
  {
    "path": "server/utils/vectorDbProviders/weaviate/WEAVIATE_SETUP.md",
    "content": "# How to setup a local (or cloud) Weaviate Vector Database\n\n[Get a Weaviate Cloud instance](https://weaviate.io/developers/weaviate/quickstart#create-an-instance).\n[Set up Weaviate locally on Docker](https://weaviate.io/developers/weaviate/installation/docker-compose).\n\nFill out the variables in the \"Vector Database\" tab of settings. Select Weaviate as your provider and fill out the appropriate fields\nwith the information from either of the above steps.\n\n### How to get started _Development mode only_\n\nAfter setting up either the Weaviate cloud or local dockerized instance you just need to set these variable in `.env.development` or defined them at runtime via the UI.\n\n```\nVECTOR_DB=\"weaviate\"\nWEAVIATE_ENDPOINT='http://localhost:8080'\nWEAVIATE_API_KEY= # Optional\n```\n"
  },
  {
    "path": "server/utils/vectorDbProviders/weaviate/index.js",
    "content": "const { default: weaviate } = require(\"weaviate-ts-client\");\nconst { TextSplitter } = require(\"../../TextSplitter\");\nconst { SystemSettings } = require(\"../../../models/systemSettings\");\nconst { storeVectorResult, cachedVectorInformation } = require(\"../../files\");\nconst { v4: uuidv4 } = require(\"uuid\");\nconst { toChunks, getEmbeddingEngineSelection } = require(\"../../helpers\");\nconst { camelCase } = require(\"../../helpers/camelcase\");\nconst { sourceIdentifier } = require(\"../../chats\");\nconst { VectorDatabase } = require(\"../base\");\n\nclass Weaviate extends VectorDatabase {\n  constructor() {\n    super();\n  }\n\n  get name() {\n    return \"Weaviate\";\n  }\n\n  async connect() {\n    if (process.env.VECTOR_DB !== \"weaviate\")\n      throw new Error(\"Weaviate::Invalid ENV settings\");\n\n    const weaviateUrl = new URL(process.env.WEAVIATE_ENDPOINT);\n    const options = {\n      scheme: weaviateUrl.protocol?.replace(\":\", \"\") || \"http\",\n      host: weaviateUrl?.host,\n      ...(process.env?.WEAVIATE_API_KEY?.length > 0\n        ? { apiKey: new weaviate.ApiKey(process.env?.WEAVIATE_API_KEY) }\n        : {}),\n    };\n    const client = weaviate.client(options);\n    const isAlive = await await client.misc.liveChecker().do();\n    if (!isAlive)\n      throw new Error(\n        \"Weaviate::Invalid Alive signal received - is the service online?\"\n      );\n    return { client };\n  }\n\n  async heartbeat() {\n    await this.connect();\n    return { heartbeat: Number(new Date()) };\n  }\n\n  async totalVectors() {\n    const { client } = await this.connect();\n    const collectionNames = await this.allNamespaces(client);\n    var totalVectors = 0;\n    for (const name of collectionNames) {\n      totalVectors += await this.namespaceCountWithClient(client, name);\n    }\n    return totalVectors;\n  }\n\n  async namespaceCountWithClient(client, namespace) {\n    try {\n      const response = await client.graphql\n        .aggregate()\n        .withClassName(camelCase(namespace))\n        .withFields(\"meta { count }\")\n        .do();\n      return (\n        response?.data?.Aggregate?.[camelCase(namespace)]?.[0]?.meta?.count || 0\n      );\n    } catch (e) {\n      this.logger(`namespaceCountWithClient`, e.message);\n      return 0;\n    }\n  }\n\n  async namespaceCount(namespace = null) {\n    try {\n      const { client } = await this.connect();\n      const response = await client.graphql\n        .aggregate()\n        .withClassName(camelCase(namespace))\n        .withFields(\"meta { count }\")\n        .do();\n\n      return (\n        response?.data?.Aggregate?.[camelCase(namespace)]?.[0]?.meta?.count || 0\n      );\n    } catch (e) {\n      this.logger(`namespaceCountWithClient`, e.message);\n      return 0;\n    }\n  }\n\n  async similarityResponse({\n    client,\n    namespace,\n    queryVector,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    const result = {\n      contextTexts: [],\n      sourceDocuments: [],\n      scores: [],\n    };\n\n    const weaviateClass = await this.namespace(client, namespace);\n    const fields =\n      weaviateClass.properties?.map((prop) => prop.name)?.join(\" \") ?? \"\";\n    const queryResponse = await client.graphql\n      .get()\n      .withClassName(camelCase(namespace))\n      .withFields(`${fields} _additional { id certainty }`)\n      .withNearVector({ vector: queryVector })\n      .withLimit(topN)\n      .do();\n\n    const responses = queryResponse?.data?.Get?.[camelCase(namespace)];\n    responses.forEach((response) => {\n      // In Weaviate we have to pluck id from _additional and spread it into the rest\n      // of the properties.\n      const {\n        _additional: { id, certainty },\n        ...rest\n      } = response;\n      if (certainty < similarityThreshold) return;\n      if (filterIdentifiers.includes(sourceIdentifier(rest))) {\n        this.logger(\n          \"A source was filtered from context as it's parent document is pinned.\"\n        );\n        return;\n      }\n      result.contextTexts.push(rest.text);\n      result.sourceDocuments.push({ ...rest, id, score: certainty });\n      result.scores.push(certainty);\n    });\n\n    return result;\n  }\n\n  async allNamespaces(client) {\n    try {\n      const { classes = [] } = await client.schema.getter().do();\n      return classes.map((classObj) => classObj.class);\n    } catch (e) {\n      this.logger(\"AllNamespace\", e);\n      return [];\n    }\n  }\n\n  async namespace(client, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    if (!(await this.namespaceExists(client, namespace))) return null;\n\n    const weaviateClass = await client.schema\n      .classGetter()\n      .withClassName(camelCase(namespace))\n      .do();\n\n    return {\n      ...weaviateClass,\n      vectorCount: await this.namespaceCount(namespace),\n    };\n  }\n\n  async addVectors(client, vectors = []) {\n    const response = { success: true, errors: new Set([]) };\n    const results = await client.batch\n      .objectsBatcher()\n      .withObjects(...vectors)\n      .do();\n\n    results.forEach((res) => {\n      const { status, errors = [] } = res.result;\n      if (status === \"SUCCESS\" || errors.length === 0) return;\n      response.success = false;\n      response.errors.add(errors.error?.[0]?.message || null);\n    });\n\n    response.errors = [...response.errors];\n    return response;\n  }\n\n  async hasNamespace(namespace = null) {\n    if (!namespace) return false;\n    const { client } = await this.connect();\n    const weaviateClasses = await this.allNamespaces(client);\n    return weaviateClasses.includes(camelCase(namespace));\n  }\n\n  async namespaceExists(client, namespace = null) {\n    if (!namespace) throw new Error(\"No namespace value provided.\");\n    const weaviateClasses = await this.allNamespaces(client);\n    return weaviateClasses.includes(camelCase(namespace));\n  }\n\n  async deleteVectorsInNamespace(client, namespace = null) {\n    await client.schema.classDeleter().withClassName(camelCase(namespace)).do();\n    return true;\n  }\n\n  async addDocumentToNamespace(\n    namespace,\n    documentData = {},\n    fullFilePath = null,\n    skipCache = false\n  ) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    try {\n      const {\n        pageContent,\n        docId,\n        id: _id, // Weaviate will abort if `id` is present in properties\n        ...metadata\n      } = documentData;\n      if (!pageContent || pageContent.length == 0) return false;\n\n      this.logger(\"Adding new vectorized document into namespace\", namespace);\n      if (!skipCache) {\n        const cacheResult = await cachedVectorInformation(fullFilePath);\n        if (cacheResult.exists) {\n          const { client } = await this.connect();\n          const weaviateClassExits = await this.hasNamespace(namespace);\n          if (!weaviateClassExits) {\n            await client.schema\n              .classCreator()\n              .withClass({\n                class: camelCase(namespace),\n                description: `Class created by AnythingLLM named ${camelCase(\n                  namespace\n                )}`,\n                vectorizer: \"none\",\n              })\n              .do();\n          }\n\n          const { chunks } = cacheResult;\n          const documentVectors = [];\n          const vectors = [];\n\n          for (const chunk of chunks) {\n            // Before sending to Weaviate and saving the records to our db\n            // we need to assign the id of each chunk that is stored in the cached file.\n            chunk.forEach((chunk) => {\n              const id = uuidv4();\n              const flattenedMetadata = this.flattenObjectForWeaviate(\n                chunk.properties ?? chunk.metadata\n              );\n              documentVectors.push({ docId, vectorId: id });\n              const vectorRecord = {\n                id,\n                class: camelCase(namespace),\n                vector: chunk.vector || chunk.values || [],\n                properties: { ...flattenedMetadata },\n              };\n              vectors.push(vectorRecord);\n            });\n\n            const { success: additionResult, errors = [] } =\n              await this.addVectors(client, vectors);\n            if (!additionResult) {\n              this.logger(\"addVectors failed to insert\", errors);\n              throw new Error(\"Error embedding into Weaviate\");\n            }\n          }\n\n          await DocumentVectors.bulkInsert(documentVectors);\n          return { vectorized: true, error: null };\n        }\n      }\n\n      // If we are here then we are going to embed and store a novel document.\n      // We have to do this manually as opposed to using LangChains `Chroma.fromDocuments`\n      // because we then cannot atomically control our namespace to granularly find/remove documents\n      // from vectordb.\n      const EmbedderEngine = getEmbeddingEngineSelection();\n      const textSplitter = new TextSplitter({\n        chunkSize: TextSplitter.determineMaxChunkSize(\n          await SystemSettings.getValueOrFallback({\n            label: \"text_splitter_chunk_size\",\n          }),\n          EmbedderEngine?.embeddingMaxChunkLength\n        ),\n        chunkOverlap: await SystemSettings.getValueOrFallback(\n          { label: \"text_splitter_chunk_overlap\" },\n          20\n        ),\n        chunkHeaderMeta: TextSplitter.buildHeaderMeta(metadata),\n        chunkPrefix: EmbedderEngine?.embeddingPrefix,\n      });\n      const textChunks = await textSplitter.splitText(pageContent);\n\n      this.logger(\"Snippets created from document:\", textChunks.length);\n      const documentVectors = [];\n      const vectors = [];\n      const vectorValues = await EmbedderEngine.embedChunks(textChunks);\n      const submission = {\n        ids: [],\n        vectors: [],\n        properties: [],\n      };\n\n      if (!!vectorValues && vectorValues.length > 0) {\n        for (const [i, vector] of vectorValues.entries()) {\n          const flattenedMetadata = this.flattenObjectForWeaviate(metadata);\n          const vectorRecord = {\n            class: camelCase(namespace),\n            id: uuidv4(),\n            vector: vector,\n            // [DO NOT REMOVE]\n            // LangChain will be unable to find your text if you embed manually and dont include the `text` key.\n            // https://github.com/hwchase17/langchainjs/blob/5485c4af50c063e257ad54f4393fa79e0aff6462/langchain/src/vectorstores/weaviate.ts#L133\n            properties: { ...flattenedMetadata, text: textChunks[i] },\n          };\n\n          submission.ids.push(vectorRecord.id);\n          submission.vectors.push(vectorRecord.values);\n          submission.properties.push(metadata);\n\n          vectors.push(vectorRecord);\n          documentVectors.push({ docId, vectorId: vectorRecord.id });\n        }\n      } else {\n        throw new Error(\n          \"Could not embed document chunks! This document will not be recorded.\"\n        );\n      }\n\n      const { client } = await this.connect();\n      const weaviateClassExits = await this.hasNamespace(namespace);\n      if (!weaviateClassExits) {\n        await client.schema\n          .classCreator()\n          .withClass({\n            class: camelCase(namespace),\n            description: `Class created by AnythingLLM named ${camelCase(\n              namespace\n            )}`,\n            vectorizer: \"none\",\n          })\n          .do();\n      }\n\n      if (vectors.length > 0) {\n        const chunks = [];\n        for (const chunk of toChunks(vectors, 500)) chunks.push(chunk);\n\n        this.logger(\"Inserting vectorized chunks into Weaviate collection.\");\n        const { success: additionResult, errors = [] } = await this.addVectors(\n          client,\n          vectors\n        );\n        if (!additionResult) {\n          this.logger(\"addVectors failed to insert\", errors);\n          throw new Error(\"Error embedding into Weaviate\");\n        }\n        await storeVectorResult(chunks, fullFilePath);\n      }\n\n      await DocumentVectors.bulkInsert(documentVectors);\n      return { vectorized: true, error: null };\n    } catch (e) {\n      this.logger(\"addDocumentToNamespace\", e.message);\n      return { vectorized: false, error: e.message };\n    }\n  }\n\n  async deleteDocumentFromNamespace(namespace, docId) {\n    const { DocumentVectors } = require(\"../../../models/vectors\");\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace))) return;\n\n    const knownDocuments = await DocumentVectors.where({ docId });\n    if (knownDocuments.length === 0) return;\n\n    for (const doc of knownDocuments) {\n      await client.data\n        .deleter()\n        .withClassName(camelCase(namespace))\n        .withId(doc.vectorId)\n        .do();\n    }\n\n    const indexes = knownDocuments.map((doc) => doc.id);\n    await DocumentVectors.deleteIds(indexes);\n    return true;\n  }\n\n  async performSimilaritySearch({\n    namespace = null,\n    input = \"\",\n    LLMConnector = null,\n    similarityThreshold = 0.25,\n    topN = 4,\n    filterIdentifiers = [],\n  }) {\n    if (!namespace || !input || !LLMConnector)\n      throw new Error(\"Invalid request to performSimilaritySearch.\");\n\n    const { client } = await this.connect();\n    if (!(await this.namespaceExists(client, namespace))) {\n      return {\n        contextTexts: [],\n        sources: [],\n        message: \"Invalid query - no documents found for workspace!\",\n      };\n    }\n\n    const queryVector = await LLMConnector.embedTextInput(input);\n    const { contextTexts, sourceDocuments } = await this.similarityResponse({\n      client,\n      namespace,\n      queryVector,\n      similarityThreshold,\n      topN,\n      filterIdentifiers,\n    });\n\n    const sources = sourceDocuments.map((metadata, i) => {\n      return { ...metadata, text: contextTexts[i] };\n    });\n    return {\n      contextTexts,\n      sources: this.curateSources(sources),\n      message: false,\n    };\n  }\n\n  async \"namespace-stats\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    if (!namespace) throw new Error(\"namespace required\");\n    const { client } = await this.connect();\n    const stats = await this.namespace(client, namespace);\n    return stats\n      ? stats\n      : { message: \"No stats were able to be fetched from DB for namespace\" };\n  }\n\n  async \"delete-namespace\"(reqBody = {}) {\n    const { namespace = null } = reqBody;\n    const { client } = await this.connect();\n    const details = await this.namespace(client, namespace);\n    await this.deleteVectorsInNamespace(client, namespace);\n    return {\n      message: `Namespace ${camelCase(namespace)} was deleted along with ${\n        details?.vectorCount\n      } vectors.`,\n    };\n  }\n\n  async reset() {\n    const { client } = await this.connect();\n    const weaviateClasses = await this.allNamespaces(client);\n    for (const weaviateClass of weaviateClasses) {\n      await client.schema.classDeleter().withClassName(weaviateClass).do();\n    }\n    return { reset: true };\n  }\n\n  curateSources(sources = []) {\n    const documents = [];\n    for (const source of sources) {\n      if (Object.keys(source).length > 0) {\n        const metadata = source.hasOwnProperty(\"metadata\")\n          ? source.metadata\n          : source;\n        documents.push({ ...metadata });\n      }\n    }\n\n    return documents;\n  }\n\n  flattenObjectForWeaviate(obj = {}) {\n    // Note this function is not generic, it is designed specifically for Weaviate\n    // https://weaviate.io/developers/weaviate/config-refs/datatypes#introduction\n    // Credit to LangchainJS\n    // https://github.com/hwchase17/langchainjs/blob/5485c4af50c063e257ad54f4393fa79e0aff6462/langchain/src/vectorstores/weaviate.ts#L11C1-L50C3\n    const flattenedObject = {};\n\n    for (const key in obj) {\n      if (!Object.hasOwn(obj, key) || key === \"id\") {\n        continue;\n      }\n      const value = obj[key];\n      if (typeof obj[key] === \"object\" && !Array.isArray(value)) {\n        const recursiveResult = this.flattenObjectForWeaviate(value);\n\n        for (const deepKey in recursiveResult) {\n          if (Object.hasOwn(obj, key)) {\n            flattenedObject[`${key}_${deepKey}`] = recursiveResult[deepKey];\n          }\n        }\n      } else if (Array.isArray(value)) {\n        if (\n          value.length > 0 &&\n          typeof value[0] !== \"object\" &&\n          value.every((el) => typeof el === typeof value[0])\n        ) {\n          // Weaviate only supports arrays of primitive types,\n          // where all elements are of the same type\n          flattenedObject[key] = value;\n        }\n      } else {\n        flattenedObject[key] = value;\n      }\n    }\n\n    return flattenedObject;\n  }\n}\n\nmodule.exports.Weaviate = Weaviate;\n"
  },
  {
    "path": "server/utils/vectorDbProviders/zilliz/index.js",
    "content": "const { MilvusClient } = require(\"@zilliz/milvus2-sdk-node\");\nconst { Milvus } = require(\"../milvus\");\n\n/**\n * Zilliz is the cloud version of Milvus so we can just extend the\n * Milvus class and override the connect method\n */\nclass Zilliz extends Milvus {\n  constructor() {\n    super();\n  }\n\n  get name() {\n    return \"Zilliz\";\n  }\n\n  async connect() {\n    if (process.env.VECTOR_DB !== \"zilliz\")\n      throw new Error(`${this.name}::Invalid ENV settings`);\n\n    const client = new MilvusClient({\n      address: process.env.ZILLIZ_ENDPOINT,\n      token: process.env.ZILLIZ_API_TOKEN,\n    });\n\n    const { isHealthy } = await client.checkHealth();\n    if (!isHealthy)\n      throw new Error(\n        `${this.name}::Invalid Heartbeat received - is the instance online?`\n      );\n\n    return { client };\n  }\n}\n\nmodule.exports.Zilliz = Zilliz;\n"
  },
  {
    "path": "server/utils/vectorStore/resetAllVectorStores.js",
    "content": "const { Workspace } = require(\"../../models/workspace\");\nconst { Document } = require(\"../../models/documents\");\nconst { DocumentVectors } = require(\"../../models/vectors\");\nconst { EventLogs } = require(\"../../models/eventLogs\");\nconst { purgeEntireVectorCache } = require(\"../files\");\nconst { getVectorDbClass } = require(\"../helpers\");\n\n/**\n * Resets all vector database and associated content:\n * - Purges the entire vector-cache folder.\n * - Deletes all document vectors from the database.\n * - Deletes all documents from the database.\n * - Deletes all vector db namespaces for each workspace.\n * - Logs an event indicating the reset.\n * @param {string} vectorDbKey - The _previous_ vector database provider name that we will be resetting.\n * @returns {Promise<boolean>} - True if successful, false otherwise.\n */\nasync function resetAllVectorStores({ vectorDbKey }) {\n  try {\n    const workspaces = await Workspace.where();\n    purgeEntireVectorCache(); // Purges the entire vector-cache folder.\n    await DocumentVectors.delete(); // Deletes all document vectors from the database.\n    await Document.delete(); // Deletes all documents from the database.\n    await EventLogs.logEvent(\"workspace_vectors_reset\", {\n      reason: \"System vector configuration changed\",\n    });\n\n    console.log(\n      \"Resetting anythingllm managed vector namespaces for\",\n      vectorDbKey\n    );\n    const VectorDb = getVectorDbClass(vectorDbKey);\n\n    if (vectorDbKey === \"pgvector\") {\n      /*\n      pgvector has a reset method that drops the entire embedding table\n      which is required since if this function is called we will need to\n      reset the embedding column VECTOR dimension value and you cannot change\n      the dimension value of an existing vector column.\n      */\n      await VectorDb.reset();\n    } else {\n      for (const workspace of workspaces) {\n        try {\n          await VectorDb[\"delete-namespace\"]({ namespace: workspace.slug });\n        } catch (e) {\n          console.error(e.message);\n        }\n      }\n    }\n\n    return true;\n  } catch (error) {\n    console.error(\"Failed to reset vector stores:\", error);\n    return false;\n  }\n}\n\nmodule.exports = { resetAllVectorStores };\n"
  }
]